Peers nearby improvements

This commit is contained in:
Ilya Laktyushin 2019-06-19 00:32:27 +02:00
parent 3adf49cde0
commit 1a42a4c389
52 changed files with 2985 additions and 9466 deletions

View File

@ -4398,12 +4398,12 @@ Any member of this group will be able to see messages in the channel.";
"Contacts.AddPeopleNearby" = "Add People Nearby"; "Contacts.AddPeopleNearby" = "Add People Nearby";
"PeopleNearby.Title" = "People Nearby"; "PeopleNearby.Title" = "People Nearby";
"PeopleNearby.Description" = "Use this section to quickly find people and groups near you."; "PeopleNearby.Description" = "Ask your friend nearby to open this page to exchange phone numbers.";
"PeopleNearby.Users" = "People Around You"; "PeopleNearby.Users" = "People Nearby";
"PeopleNearby.UsersEmpty" = "No one else is viewing \"People Nearby\" around you now"; "PeopleNearby.UsersEmpty" = "Looking for users around you...";
"PeopleNearby.Groups" = "Groups Around You"; "PeopleNearby.Groups" = "Groups Nearby";
"PeopleNearby.CreateGroup" = "Start a Group Chat Here"; "PeopleNearby.CreateGroup" = "Create a Group Here";
"PeopleNearby.Channels" = "Channels Around You"; "PeopleNearby.Channels" = "Channels Nearby";
"Channel.Management.LabelOwner" = "Owner"; "Channel.Management.LabelOwner" = "Owner";
"Channel.Management.LabelAdministrator" = "Administrator"; "Channel.Management.LabelAdministrator" = "Administrator";
@ -4412,12 +4412,8 @@ Any member of this group will be able to see messages in the channel.";
"Common.ActionNotAllowedError" = "Sorry, you are not allowed to do this."; "Common.ActionNotAllowedError" = "Sorry, you are not allowed to do this.";
"Group.Location.Title" = "Location"; "Group.Location.Title" = "Location";
"Group.Location.SetLocation" = "Set Location";
"Group.Location.ChangeLocation" = "Change Location"; "Group.Location.ChangeLocation" = "Change Location";
"Group.Location.RemoveLocation" = "Remove Location"; "Group.Location.Info" = "People can find your group using People Nearby section.";
"Group.Location.Info" = "People will be able to find your group in the Groups Nearby section (Contacts > Add People Nearby).";
"Group.Username.Title" = "Username";
"Channel.AdminLog.MessageTransferedName" = "transferred ownership to %1$@"; "Channel.AdminLog.MessageTransferedName" = "transferred ownership to %1$@";
"Channel.AdminLog.MessageTransferedNameUsername" = "transferred ownership to %1$@ (%2$@)"; "Channel.AdminLog.MessageTransferedNameUsername" = "transferred ownership to %1$@ (%2$@)";
@ -4432,9 +4428,16 @@ Any member of this group will be able to see messages in the channel.";
"Permissions.PeopleNearbyAllow.v0" = "Allow Access"; "Permissions.PeopleNearbyAllow.v0" = "Allow Access";
"Permissions.PeopleNearbyAllowInSettings.v0" = "Allow in Settings"; "Permissions.PeopleNearbyAllowInSettings.v0" = "Allow in Settings";
"Conversation.ReportGroupLocation" = "Group unrelated to tocation?"; "Conversation.ReportGroupLocation" = "Group unrelated to location?";
"ReportGroupLocation.Title" = "Report Unrelated Group"; "ReportGroupLocation.Title" = "Report Unrelated Group";
"ReportGroupLocation.Text" = "Please tell us if this group is not related to this location."; "ReportGroupLocation.Text" = "Please tell us if this group is not related to this location.";
"ReportGroupLocation.Report" = "Report"; "ReportGroupLocation.Report" = "Report";
"Group.Setup.TypePublicWithLocationHelp" = "Public groups can be found in search, chat history is available to everyone and anyone can join.\n\nTo make your group public, set a username or add a location."; "LocalGroup.Title" = "Create a Local Group";
"LocalGroup.Text" = "Anyone close to this location (neighbors, co-workers, fellow students, event attendees, visitors of a venue) will see your group in the People Nearby section.";
"LocalGroup.ButtonTitle" = "Start Group";
"LocalGroup.IrrelevantWarning" = "If you start an unrelated group at this location, you may get restricted in creating new location-based groups.";
"GroupInfo.Location" = "Location";
"GroupInfo.PublicLink" = "Public Link";
"GroupInfo.PublicLinkAdd" = "Add";

View File

@ -14,7 +14,7 @@ typedef enum {
@interface TGLocationPickerController : TGLocationMapViewController @interface TGLocationPickerController : TGLocationMapViewController
@property (nonatomic, copy) void (^locationPicked)(CLLocationCoordinate2D coordinate, TGVenueAttachment *venue); @property (nonatomic, copy) void (^locationPicked)(CLLocationCoordinate2D coordinate, TGVenueAttachment *venue, NSString *address);
@property (nonatomic, copy) SSignal *(^nearbyPlacesSignal)(NSString *query, CLLocation *coordinate); @property (nonatomic, copy) SSignal *(^nearbyPlacesSignal)(NSString *query, CLLocation *coordinate);

View File

@ -345,7 +345,7 @@ const CGPoint TGLocationPickerPinOffset = { 0.0f, 33.0f };
coordinate = [self mapCenterCoordinateForPickerPin]; coordinate = [self mapCenterCoordinateForPickerPin];
if (self.locationPicked != nil) if (self.locationPicked != nil)
self.locationPicked(coordinate, nil); self.locationPicked(coordinate, nil, _customAddress);
} }
- (void)searchButtonPressed - (void)searchButtonPressed
@ -520,7 +520,7 @@ const CGPoint TGLocationPickerPinOffset = { 0.0f, 33.0f };
NSString *address = @""; NSString *address = @"";
if (result != nil) if (result != nil)
address = result.displayAddress; address = result.fullAddress;
strongSelf->_customAddress = address; strongSelf->_customAddress = address;
[strongSelf updateCurrentLocationCell]; [strongSelf updateCurrentLocationCell];
@ -662,9 +662,9 @@ const CGPoint TGLocationPickerPinOffset = { 0.0f, 33.0f };
_ownLocationView.hidden = true; _ownLocationView.hidden = true;
_pickerPinWrapper.hidden = false; _pickerPinWrapper.hidden = false;
if (_intent != TGLocationPickerControllerCustomLocationIntent) { //if (_intent != TGLocationPickerControllerCustomLocationIntent) {
[_pickerPinView setCustomPin:true animated:true]; [_pickerPinView setCustomPin:true animated:true];
} //}
_mapView.tapEnabled = false; _mapView.tapEnabled = false;
_mapView.longPressAsTapEnabled = false; _mapView.longPressAsTapEnabled = false;
@ -1128,7 +1128,7 @@ const CGPoint TGLocationPickerPinOffset = { 0.0f, 33.0f };
} }
if (self.locationPicked != nil) if (self.locationPicked != nil)
self.locationPicked(venue.coordinate, [venue venueAttachment]); self.locationPicked(venue.coordinate, [venue venueAttachment], _customAddress);
} }
} }

View File

@ -435,6 +435,7 @@ NSString *const TGLocationPinAnnotationKind = @"TGLocationPinAnnotation";
- (void)setPinRaised:(bool)raised avatar:(bool)avatar animated:(bool)animated completion:(void (^)(void))completion - (void)setPinRaised:(bool)raised avatar:(bool)avatar animated:(bool)animated completion:(void (^)(void))completion
{ {
_pinRaised = raised; _pinRaised = raised;
avatar = false;
[_shadowView.layer removeAllAnimations]; [_shadowView.layer removeAllAnimations];
if (iosMajorVersion() < 7) if (iosMajorVersion() < 7)
@ -491,6 +492,9 @@ NSString *const TGLocationPinAnnotationKind = @"TGLocationPinAnnotation";
_iconView.image = image; _iconView.image = image;
[_backgroundView addSubview:_avatarView]; [_backgroundView addSubview:_avatarView];
_avatarView.center = CGPointMake(_backgroundView.frame.size.width / 2.0f, _backgroundView.frame.size.height / 2.0f - 5.0f); _avatarView.center = CGPointMake(_backgroundView.frame.size.width / 2.0f, _backgroundView.frame.size.height / 2.0f - 5.0f);
_shadowView.center = CGPointMake(TGScreenPixel, -36.0f);
_backgroundView.center = CGPointMake(_shadowView.frame.size.width / 2.0f, _shadowView.frame.size.height / 2.0f);
_iconView.center = CGPointMake(_shadowView.frame.size.width / 2.0f, _shadowView.frame.size.height / 2.0f - 5.0f);
TGDispatchAfter(0.01, dispatch_get_main_queue(), ^ TGDispatchAfter(0.01, dispatch_get_main_queue(), ^
{ {
@ -504,8 +508,11 @@ NSString *const TGLocationPinAnnotationKind = @"TGLocationPinAnnotation";
if (!customPin) if (!customPin)
[self addSubview:_avatarView]; [self addSubview:_avatarView];
_animating = false; _animating = false;
[self setNeedsLayout];
}]; }];
}); });
[self setNeedsLayout];
} }
else else
{ {

View File

@ -14,6 +14,8 @@
@property (nonatomic, readonly) NSString *city; @property (nonatomic, readonly) NSString *city;
@property (nonatomic, readonly) NSString *district; @property (nonatomic, readonly) NSString *district;
@property (nonatomic, readonly) NSString *street; @property (nonatomic, readonly) NSString *street;
@property (nonatomic, readonly) NSString *fullAddress;
+ (TGLocationReverseGeocodeResult *)reverseGeocodeResultWithDictionary:(NSDictionary *)dictionary; + (TGLocationReverseGeocodeResult *)reverseGeocodeResultWithDictionary:(NSDictionary *)dictionary;
+ (TGLocationReverseGeocodeResult *)reverseGeocodeResultWithPlacemark:(CLPlacemark *)placemark; + (TGLocationReverseGeocodeResult *)reverseGeocodeResultWithPlacemark:(CLPlacemark *)placemark;

View File

@ -69,5 +69,17 @@
return nil; return nil;
} }
- (NSString *)fullAddress
{
NSMutableArray *components = [[NSMutableArray alloc] init];
if (self.street.length > 0)
[components addObject:self.street];
if (self.city.length > 0)
[components addObject:self.city];
if (self.country.length > 0)
[components addObject:self.country];
return [components componentsJoinedByString:@", "];
}
@end @end

View File

@ -3073,22 +3073,6 @@ public extension Api {
}) })
} }
public static func createChannel(flags: Int32, title: String, about: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(-192332417)
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false)
serializeString(about, buffer: buffer, boxed: false)
return (FunctionDescription(name: "channels.createChannel", parameters: [("flags", flags), ("title", title), ("about", about)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Updates
}
return result
})
}
public static func editTitle(channel: Api.InputChannel, title: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) { public static func editTitle(channel: Api.InputChannel, title: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(1450044624) buffer.appendInt32(1450044624)
@ -3241,20 +3225,6 @@ public extension Api {
}) })
} }
public static func getAdminedPublicChannels() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Chats>) {
let buffer = Buffer()
buffer.appendInt32(-1920105769)
return (FunctionDescription(name: "channels.getAdminedPublicChannels", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Chats? in
let reader = BufferReader(buffer)
var result: Api.messages.Chats?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.Chats
}
return result
})
}
public static func getAdminLog(flags: Int32, channel: Api.InputChannel, q: String, eventsFilter: Api.ChannelAdminLogEventsFilter?, admins: [Api.InputUser]?, maxId: Int64, minId: Int64, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.channels.AdminLogResults>) { public static func getAdminLog(flags: Int32, channel: Api.InputChannel, q: String, eventsFilter: Api.ChannelAdminLogEventsFilter?, admins: [Api.InputUser]?, maxId: Int64, minId: Int64, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.channels.AdminLogResults>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(870184064) buffer.appendInt32(870184064)
@ -3503,6 +3473,38 @@ public extension Api {
return result return result
}) })
} }
public static func createChannel(flags: Int32, title: String, about: String, geoPoint: Api.InputGeoPoint?, address: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(1029681423)
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false)
serializeString(about, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 2) != 0 {geoPoint!.serialize(buffer, true)}
if Int(flags) & Int(1 << 2) != 0 {serializeString(address!, buffer: buffer, boxed: false)}
return (FunctionDescription(name: "channels.createChannel", parameters: [("flags", flags), ("title", title), ("about", about), ("geoPoint", geoPoint), ("address", address)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Updates
}
return result
})
}
public static func getAdminedPublicChannels(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Chats>) {
let buffer = Buffer()
buffer.appendInt32(-122669393)
serializeInt32(flags, buffer: buffer, boxed: false)
return (FunctionDescription(name: "channels.getAdminedPublicChannels", parameters: [("flags", flags)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Chats? in
let reader = BufferReader(buffer)
var result: Api.messages.Chats?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.Chats
}
return result
})
}
} }
public struct payments { public struct payments {
public static func getPaymentForm(msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.PaymentForm>) { public static func getPaymentForm(msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.PaymentForm>) {

View File

@ -157,8 +157,13 @@ public func updateAddressName(account: Account, domain: AddressNameDomain, name:
} |> mapError { _ -> UpdateAddressNameError in return .generic } |> switchToLatest } |> mapError { _ -> UpdateAddressNameError in return .generic } |> switchToLatest
} }
public func adminedPublicChannels(account: Account) -> Signal<[Peer], NoError> { public func adminedPublicChannels(account: Account, location: Bool = false) -> Signal<[Peer], NoError> {
return account.network.request(Api.functions.channels.getAdminedPublicChannels()) var flags: Int32 = 0
if location {
flags |= (1 << 0)
}
return account.network.request(Api.functions.channels.getAdminedPublicChannels(flags: flags))
|> retryRequest |> retryRequest
|> mapToSignal { result -> Signal<[Peer], NoError> in |> mapToSignal { result -> Signal<[Peer], NoError> in
var peers: [Peer] = [] var peers: [Peer] = []

View File

@ -22,6 +22,7 @@ public struct CachedChannelFlags: OptionSet {
public static let canSetStickerSet = CachedChannelFlags(rawValue: 1 << 2) public static let canSetStickerSet = CachedChannelFlags(rawValue: 1 << 2)
public static let preHistoryEnabled = CachedChannelFlags(rawValue: 1 << 3) public static let preHistoryEnabled = CachedChannelFlags(rawValue: 1 << 3)
public static let canViewStats = CachedChannelFlags(rawValue: 1 << 4) public static let canViewStats = CachedChannelFlags(rawValue: 1 << 4)
public static let canChangePeerGeoLocation = CachedChannelFlags(rawValue: 1 << 5)
} }
public struct CachedChannelParticipantsSummary: PostboxCoding, Equatable { public struct CachedChannelParticipantsSummary: PostboxCoding, Equatable {

View File

@ -14,9 +14,29 @@ import Foundation
#endif #endif
import TelegramApi import TelegramApi
private func createChannel(account: Account, title: String, description: String?, isSupergroup:Bool) -> Signal<PeerId, CreateChannelError> { public enum CreateChannelError {
case generic
case restricted
}
private func createChannel(account: Account, title: String, description: String?, isSupergroup:Bool, location: (latitude: Double, longitude: Double, address: String)? = nil) -> Signal<PeerId, CreateChannelError> {
return account.postbox.transaction { transaction -> Signal<PeerId, CreateChannelError> in return account.postbox.transaction { transaction -> Signal<PeerId, CreateChannelError> in
return account.network.request(Api.functions.channels.createChannel(flags: isSupergroup ? 1 << 1 : 1 << 0, title: title, about: description ?? ""), automaticFloodWait: false) var flags: Int32 = 0
if isSupergroup {
flags |= (1 << 1)
} else {
flags |= (1 << 0)
}
var geoPoint: Api.InputGeoPoint?
var address: String?
if let location = location {
flags |= (1 << 2)
geoPoint = .inputGeoPoint(lat: location.latitude, long: location.longitude)
address = location.address
}
return account.network.request(Api.functions.channels.createChannel(flags: flags, title: title, about: description ?? "", geoPoint: geoPoint, address: address), automaticFloodWait: false)
|> mapError { error -> CreateChannelError in |> mapError { error -> CreateChannelError in
if error.errorDescription == "USER_RESTRICTED" { if error.errorDescription == "USER_RESTRICTED" {
return .restricted return .restricted
@ -46,17 +66,12 @@ private func createChannel(account: Account, title: String, description: String?
|> switchToLatest |> switchToLatest
} }
public enum CreateChannelError {
case generic
case restricted
}
public func createChannel(account: Account, title: String, description: String?) -> Signal<PeerId, CreateChannelError> { public func createChannel(account: Account, title: String, description: String?) -> Signal<PeerId, CreateChannelError> {
return createChannel(account: account, title: title, description: description, isSupergroup: false) return createChannel(account: account, title: title, description: description, isSupergroup: false)
} }
public func createSupergroup(account: Account, title: String, description: String?) -> Signal<PeerId, CreateChannelError> { public func createSupergroup(account: Account, title: String, description: String?, location: (latitude: Double, longitude: Double, address: String)? = nil) -> Signal<PeerId, CreateChannelError> {
return createChannel(account: account, title: title, description: description, isSupergroup: true) return createChannel(account: account, title: title, description: description, isSupergroup: true, location: location)
} }
public enum DeleteChannelError { public enum DeleteChannelError {

View File

@ -90,7 +90,13 @@ public func updateChannelOwnership(postbox: Postbox, network: Network, accountSt
} }
let checkPassword = twoStepAuthData(network) let checkPassword = twoStepAuthData(network)
|> mapError { _ in ChannelOwnershipTransferError.generic } |> mapError { error -> ChannelOwnershipTransferError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded
} else {
return .generic
}
}
|> mapToSignal { authData -> Signal<Api.InputCheckPasswordSRP, ChannelOwnershipTransferError> in |> mapToSignal { authData -> Signal<Api.InputCheckPasswordSRP, ChannelOwnershipTransferError> in
if let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData { if let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData {
guard let kdfResult = passwordKDF(password: password, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) else { guard let kdfResult = passwordKDF(password: password, derivation: currentPasswordDerivation, srpSessionData: srpSessionData) else {

View File

@ -18,7 +18,7 @@ public struct PeerStatusSettings: OptionSet {
public static let canBlock = PeerStatusSettings(rawValue: 1 << 3) public static let canBlock = PeerStatusSettings(rawValue: 1 << 3)
public static let canAddContact = PeerStatusSettings(rawValue: 1 << 4) public static let canAddContact = PeerStatusSettings(rawValue: 1 << 4)
public static let addExceptionWhenAddingContact = PeerStatusSettings(rawValue: 1 << 5) public static let addExceptionWhenAddingContact = PeerStatusSettings(rawValue: 1 << 5)
public static let canReportIrrelevantGeoLocation = PeerStatusSettings(rawValue: 1 << 5) public static let canReportIrrelevantGeoLocation = PeerStatusSettings(rawValue: 1 << 6)
} }
extension PeerStatusSettings { extension PeerStatusSettings {

View File

@ -16,11 +16,11 @@ public struct PeerNearby {
public final class PeersNearbyContext { public final class PeersNearbyContext {
private let queue: Queue = Queue.mainQueue() private let queue: Queue = Queue.mainQueue()
private var subscribers = Bag<([PeerNearby]) -> Void>() private var subscribers = Bag<([PeerNearby]?) -> Void>()
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
private var timer: SwiftSignalKit.Timer? private var timer: SwiftSignalKit.Timer?
private var entries: [PeerNearby] = [] private var entries: [PeerNearby]?
public init(network: Network, accountStateManager: AccountStateManager, coordinate: (latitude: Double, longitude: Double)) { public init(network: Network, accountStateManager: AccountStateManager, coordinate: (latitude: Double, longitude: Double)) {
self.disposable.set((network.request(Api.functions.contacts.getLocated(geoPoint: .inputGeoPoint(lat: coordinate.latitude, long: coordinate.longitude))) self.disposable.set((network.request(Api.functions.contacts.getLocated(geoPoint: .inputGeoPoint(lat: coordinate.latitude, long: coordinate.longitude)))
@ -53,7 +53,7 @@ public final class PeersNearbyContext {
} }
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
var entries = strongSelf.entries.filter { Double($0.expires) > timestamp } var entries = strongSelf.entries?.filter { Double($0.expires) > timestamp } ?? []
let updatedEntries = updatedEntries.filter { Double($0.expires) > timestamp } let updatedEntries = updatedEntries.filter { Double($0.expires) > timestamp }
var existingPeerIds: [PeerId: Int] = [:] var existingPeerIds: [PeerId: Int] = [:]
@ -82,7 +82,7 @@ public final class PeersNearbyContext {
} }
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
strongSelf.entries = strongSelf.entries.filter { Double($0.expires) > timestamp } strongSelf.entries = strongSelf.entries?.filter { Double($0.expires) > timestamp }
}, queue: self.queue) }, queue: self.queue)
self.timer?.start() self.timer?.start()
} }
@ -92,7 +92,7 @@ public final class PeersNearbyContext {
self.timer?.invalidate() self.timer?.invalidate()
} }
public func get() -> Signal<[PeerNearby], NoError> { public func get() -> Signal<[PeerNearby]?, NoError> {
let queue = self.queue let queue = self.queue
return Signal { [weak self] subscriber in return Signal { [weak self] subscriber in
if let strongSelf = self { if let strongSelf = self {

View File

@ -296,6 +296,9 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI
if (flags & (1 << 7)) != 0 { if (flags & (1 << 7)) != 0 {
channelFlags.insert(.canSetStickerSet) channelFlags.insert(.canSetStickerSet)
} }
if (flags & (1 << 16)) != 0 {
channelFlags.insert(.canChangePeerGeoLocation)
}
let linkedDiscussionPeerId: PeerId? let linkedDiscussionPeerId: PeerId?
if let linkedChatId = linkedChatId, linkedChatId != 0 { if let linkedChatId = linkedChatId, linkedChatId != 0 {

View File

@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "createlocalgroup@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "createlocalgroup@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "map_dark@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "map_dark@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "map_day@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "map_day@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -569,7 +569,7 @@ final class AuthorizedApplicationContext {
strongSelf.currentPermissionsController = controller strongSelf.currentPermissionsController = controller
} }
controller.setState(state, animated: didAppear) controller.setState(.permission(state), animated: didAppear)
controller.proceed = { resolved in controller.proceed = { resolved in
permissionsPosition.set(position + 1) permissionsPosition.set(position + 1)
switch state { switch state {

View File

@ -235,7 +235,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI
guard let peer = peer else { guard let peer = peer else {
return return
} }
pushControllerImpl?(createGroupController(context: context, peerIds: [], initialTitle: peer.displayTitle + " Chat", type: .supergroup, completion: { groupId, dismiss in pushControllerImpl?(createGroupController(context: context, peerIds: [], initialTitle: peer.displayTitle + " Chat", mode: .supergroup, completion: { groupId, dismiss in
var applySignal = updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: groupId) var applySignal = updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: groupId)
var cancelImpl: (() -> Void)? var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in let progressSignal = Signal<Never, NoError> { subscriber in

View File

@ -19,10 +19,8 @@ private final class ChannelVisibilityControllerArguments {
let copyPrivateLink: () -> Void let copyPrivateLink: () -> Void
let revokePrivateLink: () -> Void let revokePrivateLink: () -> Void
let sharePrivateLink: () -> Void let sharePrivateLink: () -> Void
let setLocation: () -> Void
let removeLocation: () -> Void
init(account: Account, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, copyPrivateLink: @escaping () -> Void, revokePrivateLink: @escaping () -> Void, sharePrivateLink: @escaping () -> Void, setLocation: @escaping () -> Void, removeLocation: @escaping () -> Void) { init(account: Account, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, copyPrivateLink: @escaping () -> Void, revokePrivateLink: @escaping () -> Void, sharePrivateLink: @escaping () -> Void) {
self.account = account self.account = account
self.updateCurrentType = updateCurrentType self.updateCurrentType = updateCurrentType
self.updatePublicLinkText = updatePublicLinkText self.updatePublicLinkText = updatePublicLinkText
@ -33,8 +31,6 @@ private final class ChannelVisibilityControllerArguments {
self.copyPrivateLink = copyPrivateLink self.copyPrivateLink = copyPrivateLink
self.revokePrivateLink = revokePrivateLink self.revokePrivateLink = revokePrivateLink
self.sharePrivateLink = sharePrivateLink self.sharePrivateLink = sharePrivateLink
self.setLocation = setLocation
self.removeLocation = removeLocation
} }
} }
@ -78,12 +74,6 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
case existingLinksInfo(PresentationTheme, String) case existingLinksInfo(PresentationTheme, String)
case existingLinkPeerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, ItemListPeerItemEditing, Bool) case existingLinkPeerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, ItemListPeerItemEditing, Bool)
case locationHeader(PresentationTheme, String)
case location(PresentationTheme, PeerGeoLocation)
case locationSetup(PresentationTheme, String)
case locationRemove(PresentationTheme, String)
case locationInfo(PresentationTheme, String)
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .typeHeader, .typePublic, .typePrivate, .typeInfo: case .typeHeader, .typePublic, .typePrivate, .typeInfo:
@ -94,8 +84,6 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
return ChannelVisibilitySection.linkActions.rawValue return ChannelVisibilitySection.linkActions.rawValue
case .existingLinksInfo, .existingLinkPeerItem: case .existingLinksInfo, .existingLinkPeerItem:
return ChannelVisibilitySection.link.rawValue return ChannelVisibilitySection.link.rawValue
case .locationHeader, .location, .locationSetup, .locationRemove, .locationInfo:
return ChannelVisibilitySection.location.rawValue
} }
} }
@ -133,16 +121,6 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
return 14 return 14
case let .existingLinkPeerItem(index, _, _, _, _, _, _, _): case let .existingLinkPeerItem(index, _, _, _, _, _, _, _):
return 15 + index return 15 + index
case .locationHeader:
return 1000
case .location:
return 1001
case .locationSetup:
return 1002
case .locationRemove:
return 1003
case .locationInfo:
return 1004
} }
} }
@ -268,36 +246,6 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .locationHeader(lhsTheme, lhsTitle):
if case let .locationHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .location(lhsTheme, lhsLocation):
if case let .location(rhsTheme, rhsLocation) = rhs, lhsTheme === rhsTheme, lhsLocation == rhsLocation {
return true
} else {
return false
}
case let .locationSetup(lhsTheme, lhsTitle):
if case let .locationSetup(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .locationRemove(lhsTheme, lhsTitle):
if case let .locationRemove(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .locationInfo(lhsTheme, lhsTitle):
if case let .locationInfo(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
} }
} }
@ -386,21 +334,6 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
}, removePeer: { peerId in }, removePeer: { peerId in
arguments.revokePeerId(peerId) arguments.revokePeerId(peerId)
}) })
case let .locationHeader(theme, title):
return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section)
case let .location(theme, location):
let imageSignal = chatMapSnapshotImage(account: arguments.account, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90))
return ItemListAddressItem(theme: theme, label: "", text: location.address.replacingOccurrences(of: ", ", with: "\n"), imageSignal: imageSignal, selected: nil, sectionId: self.section, style: .blocks, action: nil)
case let .locationSetup(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.setLocation()
}, clearHighlightAutomatically: false)
case let .locationRemove(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.removeLocation()
})
case let .locationInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
} }
} }
} }
@ -526,6 +459,8 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
} else { } else {
if let addressName = peer.addressName, !addressName.isEmpty { if let addressName = peer.addressName, !addressName.isEmpty {
selectedType = .publicChannel selectedType = .publicChannel
} else if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil {
selectedType = .publicChannel
} else { } else {
selectedType = .privateChannel selectedType = .privateChannel
} }
@ -543,30 +478,33 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
} }
} }
switch mode { if let _ = (view.cachedData as? CachedChannelData)?.peerGeoLocation {
case .privateLink: } else {
break switch mode {
case .initialSetup, .generic: case .privateLink:
entries.append(.typeHeader(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_TypeHeader : presentationData.strings.Channel_Edit_LinkItem)) break
entries.append(.typePublic(presentationData.theme, presentationData.strings.Channel_Setup_TypePublic, selectedType == .publicChannel)) case .initialSetup, .generic:
entries.append(.typePrivate(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivate, selectedType == .privateChannel)) entries.append(.typeHeader(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_TypeHeader : presentationData.strings.Channel_Edit_LinkItem))
entries.append(.typePublic(presentationData.theme, presentationData.strings.Channel_Setup_TypePublic, selectedType == .publicChannel))
switch selectedType { entries.append(.typePrivate(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivate, selectedType == .privateChannel))
case .publicChannel:
if isGroup { switch selectedType {
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePublicWithLocationHelp)) case .publicChannel:
} else { if isGroup {
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePublicHelp)) entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePublicHelp))
} } else {
case .privateChannel: entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePublicHelp))
if isGroup { }
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePrivateHelp)) case .privateChannel:
} else { if isGroup {
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivateHelp)) entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePrivateHelp))
} } else {
} entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivateHelp))
}
}
}
} }
switch selectedType { switch selectedType {
case .publicChannel: case .publicChannel:
var displayAvailability = false var displayAvailability = false
@ -596,7 +534,6 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp, true)) entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp, true))
} }
} else { } else {
entries.append(.publicLinkHeader(presentationData.theme, presentationData.strings.Group_Username_Title.uppercased()))
entries.append(.editablePublicLink(presentationData.theme, currentAddressName)) entries.append(.editablePublicLink(presentationData.theme, currentAddressName))
if let status = state.addressNameValidationStatus { if let status = state.addressNameValidationStatus {
let text: String let text: String
@ -639,26 +576,6 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
} }
if isGroup { if isGroup {
entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp)) entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp))
entries.append(.locationHeader(presentationData.theme, presentationData.strings.Group_Location_Title.uppercased()))
if let currentEditingLocation = state.editingLocation {
if case .removed = currentEditingLocation {
entries.append(.locationSetup(presentationData.theme, presentationData.strings.Group_Location_SetLocation))
} else if case let .location(location) = currentEditingLocation {
entries.append(.location(presentationData.theme, location))
entries.append(.locationSetup(presentationData.theme, presentationData.strings.Group_Location_ChangeLocation))
entries.append(.locationRemove(presentationData.theme, presentationData.strings.Group_Location_RemoveLocation))
}
} else {
if let location = (view.cachedData as? CachedChannelData)?.peerGeoLocation {
entries.append(.location(presentationData.theme, location))
entries.append(.locationSetup(presentationData.theme, presentationData.strings.Group_Location_ChangeLocation))
entries.append(.locationRemove(presentationData.theme, presentationData.strings.Group_Location_RemoveLocation))
} else {
entries.append(.locationSetup(presentationData.theme, presentationData.strings.Group_Location_SetLocation))
}
}
entries.append(.locationInfo(presentationData.theme, presentationData.strings.Group_Location_Info))
} else { } else {
entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Channel_Username_CreatePublicLinkHelp)) entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Channel_Username_CreatePublicLinkHelp))
} }
@ -727,7 +644,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
entries.append(.typePublic(presentationData.theme, presentationData.strings.Channel_Setup_TypePublic, selectedType == .publicChannel)) entries.append(.typePublic(presentationData.theme, presentationData.strings.Channel_Setup_TypePublic, selectedType == .publicChannel))
entries.append(.typePrivate(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivate, selectedType == .privateChannel)) entries.append(.typePrivate(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivate, selectedType == .privateChannel))
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePublicWithLocationHelp)) entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePublicHelp))
switch selectedType { switch selectedType {
case .publicChannel: case .publicChannel:
@ -755,7 +672,6 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp, true)) entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp, true))
} }
} else { } else {
entries.append(.publicLinkHeader(presentationData.theme, presentationData.strings.Group_Username_Title.uppercased()))
entries.append(.editablePublicLink(presentationData.theme, currentAddressName)) entries.append(.editablePublicLink(presentationData.theme, currentAddressName))
if let status = state.addressNameValidationStatus { if let status = state.addressNameValidationStatus {
let text: String let text: String
@ -789,19 +705,6 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
entries.append(.publicLinkStatus(presentationData.theme, text, status)) entries.append(.publicLinkStatus(presentationData.theme, text, status))
} }
entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp)) entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp))
entries.append(.locationHeader(presentationData.theme, presentationData.strings.Group_Location_Title.uppercased()))
if let currentEditingLocation = state.editingLocation {
if case .removed = currentEditingLocation {
entries.append(.locationSetup(presentationData.theme, presentationData.strings.Group_Location_SetLocation))
} else if case let .location(location) = currentEditingLocation {
entries.append(.location(presentationData.theme, location))
entries.append(.locationSetup(presentationData.theme, presentationData.strings.Group_Location_ChangeLocation))
entries.append(.locationRemove(presentationData.theme, presentationData.strings.Group_Location_RemoveLocation))
}
} else {
entries.append(.locationSetup(presentationData.theme, presentationData.strings.Group_Location_SetLocation))
}
} }
case .privateChannel: case .privateChannel:
let link = (view.cachedData as? CachedGroupData)?.exportedInvitation?.link let link = (view.cachedData as? CachedGroupData)?.exportedInvitation?.link
@ -902,8 +805,8 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
let peersDisablingAddressNameAssignment = Promise<[Peer]?>() let peersDisablingAddressNameAssignment = Promise<[Peer]?>()
peersDisablingAddressNameAssignment.set(.single(nil) |> then(channelAddressNameAssignmentAvailability(account: context.account, peerId: peerId.namespace == Namespaces.Peer.CloudChannel ? peerId : nil) |> mapToSignal { result -> Signal<[Peer]?, NoError> in peersDisablingAddressNameAssignment.set(.single(nil) |> then(channelAddressNameAssignmentAvailability(account: context.account, peerId: peerId.namespace == Namespaces.Peer.CloudChannel ? peerId : nil) |> mapToSignal { result -> Signal<[Peer]?, NoError> in
if case .addressNameLimitReached = result { if case .addressNameLimitReached = result {
return adminedPublicChannels(account: context.account) return adminedPublicChannels(account: context.account, location: false)
|> map(Optional.init) |> map(Optional.init)
} else { } else {
return .single([]) return .single([])
} }
@ -1059,60 +962,6 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
presentControllerImpl?(shareController, nil) presentControllerImpl?(shareController, nil)
} }
}) })
}, setLocation: {
dismissInputImpl?()
let _ = (context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
} |> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = legacyLocationPickerController(context: context, selfPeer: peer, peer: peer, sendLocation: { coordinate, _ in
updateState { state in
return state.withUpdatedEditingLocation(.location(PeerGeoLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, address: "")))
}
let _ = (reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|> deliverOnMainQueue).start(next: { placemark in
updateState { state in
let address: String
if let placemark = placemark {
address = placemark.fullAddress
} else {
address = "\(coordinate.latitude), \(coordinate.longitude)"
}
return state.withUpdatedEditingLocation(.location(PeerGeoLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, address: address)))
}
})
}, sendLiveLocation: { _, _ in }, theme: presentationData.theme, customLocationPicker: true, presentationCompleted: {
clearHighlightImpl?()
})
presentControllerImpl?(controller, nil)
})
}, removeLocation: {
dismissInputImpl?()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationTheme: presentationData.theme)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Group_Location_RemoveLocation, color: .destructive, action: {
dismissAction()
updateState { state in
return state.withUpdatedEditingLocation(.removed)
}
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}) })
let peerView = context.account.viewTracker.peerView(peerId) let peerView = context.account.viewTracker.peerView(peerId)
@ -1134,14 +983,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
break break
case .publicChannel: case .publicChannel:
var hasLocation = false var hasLocation = false
if let editingLocation = state.editingLocation { if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil {
switch editingLocation {
case .location:
hasLocation = true
case .removed:
hasLocation = false
}
} else if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil {
hasLocation = true hasLocation = true
} }
@ -1247,16 +1089,6 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
case .privateChannel: case .privateChannel:
break break
case .publicChannel: case .publicChannel:
var hasLocation = false
if let editingLocation = state.editingLocation {
switch editingLocation {
case .location:
hasLocation = true
case .removed:
hasLocation = false
}
}
if let addressNameValidationStatus = state.addressNameValidationStatus { if let addressNameValidationStatus = state.addressNameValidationStatus {
switch addressNameValidationStatus { switch addressNameValidationStatus {
case .availability(.available): case .availability(.available):
@ -1265,7 +1097,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
doneEnabled = false doneEnabled = false
} }
} else { } else {
doneEnabled = !(peer.addressName?.isEmpty ?? true) || hasLocation doneEnabled = !(peer.addressName?.isEmpty ?? true)
} }
} }
} }

View File

@ -4744,7 +4744,7 @@ public final class ChatController: TelegramController, GalleryHiddenMediaTarget,
} }
strongSelf.chatDisplayNode.dismissInput() strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(legacyLocationPickerController(context: strongSelf.context, selfPeer: selfPeer, peer: peer, sendLocation: { coordinate, venue in strongSelf.present(legacyLocationPickerController(context: strongSelf.context, selfPeer: selfPeer, peer: peer, sendLocation: { coordinate, venue, _ in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }

View File

@ -169,7 +169,15 @@ public class ComposeController: ViewController {
self.contactsNode.openCreateNewChannel = { [weak self] in self.contactsNode.openCreateNewChannel = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
(strongSelf.navigationController as? NavigationController)?.pushViewController(legacyChannelIntroController(context: strongSelf.context, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings), completion: { [weak self] in let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let controller = PermissionController(context: strongSelf.context, splashScreen: true)
controller.setState(.custom(icon: PermissionControllerCustomIcon(light: UIImage(bundleImageName: "Chat/Intro/ChannelIntro"), dark: nil), title: presentationData.strings.ChannelIntro_Title, subtitle: nil, text: presentationData.strings.ChannelIntro_Text, buttonTitle: presentationData.strings.ChannelIntro_CreateChannel, footerText: nil), animated: false)
controller.proceed = { [weak self] result in
if let strongSelf = self {
(strongSelf.navigationController as? NavigationController)?.replaceTopController(createChannelController(context: strongSelf.context), animated: true)
}
}
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller, completion: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
} }

View File

@ -855,7 +855,7 @@ final class ContactListNode: ASDisplayNode {
var authorizeImpl: (() -> Void)? var authorizeImpl: (() -> Void)?
var openPrivacyPolicyImpl: (() -> Void)? var openPrivacyPolicyImpl: (() -> Void)?
self.authorizationNode = PermissionContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, kind: .contacts, icon: UIImage(bundleImageName: "Settings/Permissions/Contacts"), title: self.presentationData.strings.Contacts_PermissionsTitle, text: self.presentationData.strings.Contacts_PermissionsText, buttonTitle: self.presentationData.strings.Contacts_PermissionsAllow, buttonAction: { self.authorizationNode = PermissionContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, kind: PermissionKind.contacts.rawValue, icon: UIImage(bundleImageName: "Settings/Permissions/Contacts"), title: self.presentationData.strings.Contacts_PermissionsTitle, text: self.presentationData.strings.Contacts_PermissionsText, buttonTitle: self.presentationData.strings.Contacts_PermissionsAllow, buttonAction: {
authorizeImpl?() authorizeImpl?()
}, openPrivacyPolicy: { }, openPrivacyPolicy: {
openPrivacyPolicyImpl?() openPrivacyPolicyImpl?()
@ -1256,7 +1256,7 @@ final class ContactListNode: ASDisplayNode {
let authorizationPreviousHidden = strongSelf.authorizationNode.isHidden let authorizationPreviousHidden = strongSelf.authorizationNode.isHidden
strongSelf.authorizationNode.removeFromSupernode() strongSelf.authorizationNode.removeFromSupernode()
strongSelf.authorizationNode = PermissionContentNode(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, kind: .contacts, icon: UIImage(bundleImageName: "Settings/Permissions/Contacts"), title: strongSelf.presentationData.strings.Contacts_PermissionsTitle, text: strongSelf.presentationData.strings.Contacts_PermissionsText, buttonTitle: strongSelf.presentationData.strings.Contacts_PermissionsAllow, buttonAction: { strongSelf.authorizationNode = PermissionContentNode(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, kind: PermissionKind.contacts.rawValue, icon: UIImage(bundleImageName: "Settings/Permissions/Contacts"), title: strongSelf.presentationData.strings.Contacts_PermissionsTitle, text: strongSelf.presentationData.strings.Contacts_PermissionsText, buttonTitle: strongSelf.presentationData.strings.Contacts_PermissionsAllow, buttonAction: {
authorizeImpl?() authorizeImpl?()
}, openPrivacyPolicy: { }, openPrivacyPolicy: {
openPrivacyPolicyImpl?() openPrivacyPolicyImpl?()

View File

@ -277,7 +277,7 @@ public class ContactsController: ViewController {
presentPeersNearby() presentPeersNearby()
default: default:
let controller = PermissionController(context: strongSelf.context, splashScreen: false) let controller = PermissionController(context: strongSelf.context, splashScreen: false)
controller.setState(.nearbyLocation(status: PermissionRequestStatus(accessType: status)), animated: false) controller.setState(.permission(.nearbyLocation(status: PermissionRequestStatus(accessType: status))), animated: false)
controller.proceed = { result in controller.proceed = { result in
if result { if result {
presentPeersNearby() presentPeersNearby()

View File

@ -8,12 +8,19 @@ import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import LegacyComponents import LegacyComponents
public enum CreateGroupMode {
case generic
case supergroup
case locatedGroup(latitude: Double, longitude: Double, address: String?)
}
private struct CreateGroupArguments { private struct CreateGroupArguments {
let account: Account let account: Account
let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void
let done: () -> Void let done: () -> Void
let changeProfilePhoto: () -> Void let changeProfilePhoto: () -> Void
let changeLocation: () -> Void
} }
private enum CreateGroupSection: Int32 { private enum CreateGroupSection: Int32 {
@ -47,6 +54,8 @@ private enum CreateGroupEntry: ItemListNodeEntry {
case member(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, PeerPresence?) case member(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, PeerPresence?)
case locationHeader(PresentationTheme, String) case locationHeader(PresentationTheme, String)
case location(PresentationTheme, PeerGeoLocation) case location(PresentationTheme, PeerGeoLocation)
case changeLocation(PresentationTheme, String)
case locationInfo(PresentationTheme, String)
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
@ -54,7 +63,7 @@ private enum CreateGroupEntry: ItemListNodeEntry {
return CreateGroupSection.info.rawValue return CreateGroupSection.info.rawValue
case .member: case .member:
return CreateGroupSection.members.rawValue return CreateGroupSection.members.rawValue
case .locationHeader, .location: case .locationHeader, .location, .changeLocation, .locationInfo:
return CreateGroupSection.location.rawValue return CreateGroupSection.location.rawValue
} }
} }
@ -71,6 +80,10 @@ private enum CreateGroupEntry: ItemListNodeEntry {
return 10000 return 10000
case .location: case .location:
return 10001 return 10001
case .changeLocation:
return 10002
case .locationInfo:
return 10003
} }
} }
@ -153,6 +166,18 @@ private enum CreateGroupEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .changeLocation(lhsTheme, lhsTitle):
if case let .changeLocation(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .locationInfo(lhsTheme, lhsText):
if case let .locationInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
} }
} }
@ -178,6 +203,12 @@ private enum CreateGroupEntry: ItemListNodeEntry {
case let .location(theme, location): case let .location(theme, location):
let imageSignal = chatMapSnapshotImage(account: arguments.account, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90)) let imageSignal = chatMapSnapshotImage(account: arguments.account, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90))
return ItemListAddressItem(theme: theme, label: "", text: location.address.replacingOccurrences(of: ", ", with: "\n"), imageSignal: imageSignal, selected: nil, sectionId: self.section, style: .blocks, action: nil) return ItemListAddressItem(theme: theme, label: "", text: location.address.replacingOccurrences(of: ", ", with: "\n"), imageSignal: imageSignal, selected: nil, sectionId: self.section, style: .blocks, action: nil)
case let .changeLocation(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.changeLocation()
}, clearHighlightAutomatically: false)
case let .locationInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
} }
} }
} }
@ -186,6 +217,7 @@ private struct CreateGroupState: Equatable {
var creating: Bool var creating: Bool
var editingName: ItemListAvatarAndNameInfoItemName var editingName: ItemListAvatarAndNameInfoItemName
var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?
var location: PeerGeoLocation?
static func ==(lhs: CreateGroupState, rhs: CreateGroupState) -> Bool { static func ==(lhs: CreateGroupState, rhs: CreateGroupState) -> Bool {
if lhs.creating != rhs.creating { if lhs.creating != rhs.creating {
@ -197,12 +229,14 @@ private struct CreateGroupState: Equatable {
if lhs.avatar != rhs.avatar { if lhs.avatar != rhs.avatar {
return false return false
} }
if lhs.location != rhs.location {
return false
}
return true return true
} }
} }
private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView, geoLocation: PeerGeoLocation?) -> [CreateGroupEntry] { private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView) -> [CreateGroupEntry] {
var entries: [CreateGroupEntry] = [] var entries: [CreateGroupEntry] = []
let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil) let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil)
@ -243,22 +277,23 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat
entries.append(.member(Int32(i), presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peers[i], view.presences[peers[i].id])) entries.append(.member(Int32(i), presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peers[i], view.presences[peers[i].id]))
} }
if let geoLocation = geoLocation { if let location = state.location {
entries.append(.locationHeader(presentationData.theme, presentationData.strings.Group_Location_Title.uppercased())) entries.append(.locationHeader(presentationData.theme, presentationData.strings.Group_Location_Title.uppercased()))
entries.append(.location(presentationData.theme, geoLocation)) entries.append(.location(presentationData.theme, location))
entries.append(.changeLocation(presentationData.theme, presentationData.strings.Group_Location_ChangeLocation))
entries.append(.locationInfo(presentationData.theme, presentationData.strings.Group_Location_Info))
} }
return entries return entries
} }
public enum CreateGroupType { public func createGroupController(context: AccountContext, peerIds: [PeerId], initialTitle: String? = nil, mode: CreateGroupMode = .generic, completion: ((PeerId, @escaping () -> Void) -> Void)? = nil) -> ViewController {
case generic var location: PeerGeoLocation?
case supergroup if case let .locatedGroup(latitude, longitude, address) = mode {
case locatedGroup(latitude: Double, longitude: Double) location = PeerGeoLocation(latitude: latitude, longitude: longitude, address: address ?? "")
} }
public func createGroupController(context: AccountContext, peerIds: [PeerId], initialTitle: String? = nil, type: CreateGroupType = .generic, completion: ((PeerId, @escaping () -> Void) -> Void)? = nil) -> ViewController { let initialState = CreateGroupState(creating: false, editingName: .title(title: initialTitle ?? "", type: .group), avatar: nil, location: location)
let initialState = CreateGroupState(creating: false, editingName: .title(title: initialTitle ?? "", type: .group), avatar: nil)
let statePromise = ValuePromise(initialState, ignoreRepeated: true) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState) let stateValue = Atomic(value: initialState)
let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in
@ -269,6 +304,7 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)?
var endEditingImpl: (() -> Void)? var endEditingImpl: (() -> Void)?
var clearHighlightImpl: (() -> Void)?
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
@ -276,9 +312,16 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
let uploadedAvatar = Promise<UploadedPeerPhotoData>() let uploadedAvatar = Promise<UploadedPeerPhotoData>()
let placemarkPromise = Promise<ReverseGeocodedPlacemark?>() let addressPromise = Promise<String?>(nil)
if case let .locatedGroup(latitude, longitude) = type { if case let .locatedGroup(latitude, longitude, address) = mode {
placemarkPromise.set(reverseGeocodeLocation(latitude: latitude, longitude: longitude)) if let address = address {
addressPromise.set(.single(address))
} else {
addressPromise.set(reverseGeocodeLocation(latitude: latitude, longitude: longitude)
|> map { placemark in
return placemark?.fullAddress ?? "\(latitude), \(longitude)"
})
}
} }
let arguments = CreateGroupArguments(account: context.account, updateEditingName: { editingName in let arguments = CreateGroupArguments(account: context.account, updateEditingName: { editingName in
@ -288,8 +331,8 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
return current return current
} }
}, done: { }, done: {
let (creating, title) = stateValue.with { state -> (Bool, String) in let (creating, title, location) = stateValue.with { state -> (Bool, String, PeerGeoLocation?) in
return (state.creating, state.editingName.composedTitle) return (state.creating, state.editingName.composedTitle, state.location)
} }
if !creating && !title.isEmpty { if !creating && !title.isEmpty {
@ -301,7 +344,7 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
endEditingImpl?() endEditingImpl?()
let createSignal: Signal<PeerId?, CreateGroupError> let createSignal: Signal<PeerId?, CreateGroupError>
switch type { switch mode {
case .generic: case .generic:
createSignal = createGroup(account: context.account, title: title, peerIds: peerIds) createSignal = createGroup(account: context.account, title: title, peerIds: peerIds)
case .supergroup: case .supergroup:
@ -315,28 +358,25 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
return .restricted return .restricted
} }
} }
case let .locatedGroup(latitude, longitude): case .locatedGroup:
createSignal = createSupergroup(account: context.account, title: title, description: nil) guard let location = location else {
|> map(Optional.init) return
|> mapError { error -> CreateGroupError in
switch error {
case .generic:
return .generic
case .restricted:
return .restricted
}
} }
|> mapToSignal { peerId in
guard let peerId = peerId else { createSignal = addressPromise.get()
return .single(nil) |> introduceError(CreateGroupError.self)
|> mapToSignal { address -> Signal<PeerId?, CreateGroupError> in
guard let address = address else {
return .complete()
} }
return placemarkPromise.get() return createSupergroup(account: context.account, title: title, description: nil, location: (location.latitude, location.longitude, address))
|> introduceError(CreateGroupError.self) |> map(Optional.init)
|> mapToSignal { placemark in |> mapError { error -> CreateGroupError in
return updateChannelGeoLocation(postbox: context.account.postbox, network: context.account.network, channelId: peerId, coordinate: (latitude, longitude), address: placemark?.fullAddress ?? "\(latitude), \(longitude)") switch error {
|> introduceError(CreateGroupError.self) case .generic:
|> map { _ in return .generic
return peerId case .restricted:
return .restricted
} }
} }
} }
@ -475,10 +515,43 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
} }
} }
}) })
}, changeLocation: {
endEditingImpl?()
let peer = TelegramChannel(id: PeerId(0), accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(TelegramChannelGroupInfo(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = legacyLocationPickerController(context: context, selfPeer: peer, peer: peer, sendLocation: { coordinate, _, address in
let addressSignal: Signal<String, NoError>
if let address = address {
addressSignal = .single(address)
} else {
addressSignal = reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|> map { placemark in
if let placemark = placemark {
return placemark.fullAddress
} else {
return "\(coordinate.latitude), \(coordinate.longitude)"
}
}
}
let _ = (addressSignal
|> deliverOnMainQueue).start(next: { address in
addressPromise.set(.single(address))
updateState { current in
var current = current
current.location = PeerGeoLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, address: address)
return current
}
})
}, sendLiveLocation: { _, _ in }, theme: presentationData.theme, customLocationPicker: true, presentationCompleted: {
clearHighlightImpl?()
})
presentControllerImpl?(controller, nil)
}) })
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), context.account.postbox.multiplePeersView(peerIds), .single(nil) |> then(placemarkPromise.get())) let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), context.account.postbox.multiplePeersView(peerIds), .single(nil) |> then(addressPromise.get()))
|> map { presentationData, state, view, placemark -> (ItemListControllerState, (ItemListNodeState<CreateGroupEntry>, CreateGroupEntry.ItemGenerationArguments)) in |> map { presentationData, state, view, address -> (ItemListControllerState, (ItemListNodeState<CreateGroupEntry>, CreateGroupEntry.ItemGenerationArguments)) in
let rightNavigationButton: ItemListNavigationButton let rightNavigationButton: ItemListNavigationButton
if state.creating { if state.creating {
@ -489,17 +562,8 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
}) })
} }
var geoLocation: PeerGeoLocation?
if case let .locatedGroup(latitude, longitude) = type {
if let placemark = placemark {
geoLocation = PeerGeoLocation(latitude: latitude, longitude: longitude, address: placemark.fullAddress)
} else {
geoLocation = PeerGeoLocation(latitude: latitude, longitude: longitude, address: "")
}
}
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Compose_NewGroupTitle), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Compose_NewGroupTitle), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(entries: createGroupEntries(presentationData: presentationData, state: state, peerIds: peerIds, view: view, geoLocation: geoLocation), style: .blocks, focusItemTag: CreateGroupEntryTag.info) let listState = ItemListNodeState(entries: createGroupEntries(presentationData: presentationData, state: state, peerIds: peerIds, view: view), style: .blocks, focusItemTag: CreateGroupEntryTag.info)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -526,5 +590,8 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
[weak controller] in [weak controller] in
controller?.view.endEditing(true) controller?.view.endEditing(true)
} }
clearHighlightImpl = { [weak controller] in
controller?.clearItemNodesHighlight(animated: true)
}
return controller return controller
} }

View File

@ -39,9 +39,10 @@ private final class GroupInfoArguments {
let openGroupTypeSetup: () -> Void let openGroupTypeSetup: () -> Void
let openLinkedChannelSetup: () -> Void let openLinkedChannelSetup: () -> Void
let openLocation: (PeerGeoLocation) -> Void let openLocation: (PeerGeoLocation) -> Void
let changeLocation: () -> Void
let displayLocationContextMenu: (String) -> Void let displayLocationContextMenu: (String) -> Void
init(context: AccountContext, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, openPreHistory: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdministrators: @escaping () -> Void, openPermissions: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, promotePeer: @escaping (RenderedChannelParticipant) -> Void, restrictPeer: @escaping (RenderedChannelParticipant) -> Void, removePeer: @escaping (PeerId) -> Void, leave: @escaping () -> Void, displayUsernameShareMenu: @escaping (String) -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, openStickerPackSetup: @escaping () -> Void, openGroupTypeSetup: @escaping () -> Void, openLinkedChannelSetup: @escaping () -> Void, openLocation: @escaping (PeerGeoLocation) -> Void, displayLocationContextMenu: @escaping (String) -> Void) { init(context: AccountContext, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, openPreHistory: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdministrators: @escaping () -> Void, openPermissions: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, promotePeer: @escaping (RenderedChannelParticipant) -> Void, restrictPeer: @escaping (RenderedChannelParticipant) -> Void, removePeer: @escaping (PeerId) -> Void, leave: @escaping () -> Void, displayUsernameShareMenu: @escaping (String) -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, openStickerPackSetup: @escaping () -> Void, openGroupTypeSetup: @escaping () -> Void, openLinkedChannelSetup: @escaping () -> Void, openLocation: @escaping (PeerGeoLocation) -> Void, changeLocation: @escaping () -> Void, displayLocationContextMenu: @escaping (String) -> Void) {
self.context = context self.context = context
self.avatarAndNameInfoContext = avatarAndNameInfoContext self.avatarAndNameInfoContext = avatarAndNameInfoContext
self.tapAvatarAction = tapAvatarAction self.tapAvatarAction = tapAvatarAction
@ -69,6 +70,7 @@ private final class GroupInfoArguments {
self.openGroupTypeSetup = openGroupTypeSetup self.openGroupTypeSetup = openGroupTypeSetup
self.openLinkedChannelSetup = openLinkedChannelSetup self.openLinkedChannelSetup = openLinkedChannelSetup
self.openLocation = openLocation self.openLocation = openLocation
self.changeLocation = changeLocation
self.displayLocationContextMenu = displayLocationContextMenu self.displayLocationContextMenu = displayLocationContextMenu
} }
} }
@ -141,9 +143,10 @@ private enum GroupInfoEntry: ItemListNodeEntry {
case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?)
case setGroupPhoto(PresentationTheme, String) case setGroupPhoto(PresentationTheme, String)
case groupDescriptionSetup(PresentationTheme, String, String) case groupDescriptionSetup(PresentationTheme, String, String)
case aboutHeader(PresentationTheme, String)
case about(PresentationTheme, String) case about(PresentationTheme, String)
case locationHeader(PresentationTheme, String)
case location(PresentationTheme, PeerGeoLocation) case location(PresentationTheme, PeerGeoLocation)
case changeLocation(PresentationTheme, String)
case link(PresentationTheme, String) case link(PresentationTheme, String)
case sharedMedia(PresentationTheme, String) case sharedMedia(PresentationTheme, String)
case notifications(PresentationTheme, String, String) case notifications(PresentationTheme, String, String)
@ -159,9 +162,9 @@ private enum GroupInfoEntry: ItemListNodeEntry {
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .info, .setGroupPhoto, .groupDescriptionSetup: case .info, .setGroupPhoto, .groupDescriptionSetup, .about:
return GroupInfoSection.info.rawValue return GroupInfoSection.info.rawValue
case .aboutHeader, .about, .link, .location: case .locationHeader, .location, .changeLocation, .link:
return GroupInfoSection.about.rawValue return GroupInfoSection.about.rawValue
case .groupTypeSetup, .linkedChannelSetup, .preHistory, .stickerPack: case .groupTypeSetup, .linkedChannelSetup, .preHistory, .stickerPack:
return GroupInfoSection.infoManagement.rawValue return GroupInfoSection.infoManagement.rawValue
@ -237,20 +240,14 @@ private enum GroupInfoEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .aboutHeader(lhsTheme, lhsText):
if case let .aboutHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .about(lhsTheme, lhsText): case let .about(lhsTheme, lhsText):
if case let .about(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .about(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
} else { } else {
return false return false
} }
case let .link(lhsTheme, lhsText): case let .locationHeader(lhsTheme, lhsText):
if case let .link(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .locationHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
} else { } else {
return false return false
@ -261,6 +258,18 @@ private enum GroupInfoEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .changeLocation(lhsTheme, lhsText):
if case let .changeLocation(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .link(lhsTheme, lhsText):
if case let .link(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .notifications(lhsTheme, lhsTitle, lhsText): case let .notifications(lhsTheme, lhsTitle, lhsText):
if case let .notifications(rhsTheme, rhsTitle, rhsText) = rhs { if case let .notifications(rhsTheme, rhsTitle, rhsText) = rhs {
if lhsTheme !== rhsTheme { if lhsTheme !== rhsTheme {
@ -399,13 +408,15 @@ private enum GroupInfoEntry: ItemListNodeEntry {
return 1 return 1
case .groupDescriptionSetup: case .groupDescriptionSetup:
return 2 return 2
case .aboutHeader:
return 4
case .about: case .about:
return 5 return 3
case .link: case .locationHeader:
return 6 return 4
case .location: case .location:
return 5
case .changeLocation:
return 6
case .link:
return 7 return 7
case .groupTypeSetup: case .groupTypeSetup:
return 8 return 8
@ -424,11 +435,11 @@ private enum GroupInfoEntry: ItemListNodeEntry {
case .administrators: case .administrators:
return 15 return 15
case .addMember: case .addMember:
return 17 return 16
case let .member(_, _, _, _, index, _, _, _, _, _, _, _, _, _): case let .member(_, _, _, _, index, _, _, _, _, _, _, _, _, _):
return 20 + index return 20 + index
case .leave: case .leave:
return 100000 + 1 return 200000 + 1
} }
} }
@ -448,27 +459,31 @@ private enum GroupInfoEntry: ItemListNodeEntry {
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.changeProfilePhoto() arguments.changeProfilePhoto()
}) })
case let .aboutHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .about(theme, text): case let .about(theme, text):
return ItemListMultilineTextItem(theme: theme, text: foldMultipleLineBreaks(text), enabledEntitiyTypes: [.url, .mention, .hashtag], sectionId: self.section, style: .blocks, longTapAction: { return ItemListMultilineTextItem(theme: theme, text: foldMultipleLineBreaks(text), enabledEntitiyTypes: [.url, .mention, .hashtag], sectionId: self.section, style: .blocks, longTapAction: {
arguments.displayAboutContextMenu(text) arguments.displayAboutContextMenu(text)
}, linkItemAction: { action, itemLink in }, linkItemAction: { action, itemLink in
arguments.aboutLinkAction(action, itemLink) arguments.aboutLinkAction(action, itemLink)
}, tag: GroupInfoEntryTag.about) }, tag: GroupInfoEntryTag.about)
case let .link(theme, url): case let .locationHeader(theme, text):
return ItemListActionItem(theme: theme, title: url, kind: .neutral, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
arguments.displayUsernameShareMenu(url)
}, longTapAction: {
arguments.displayUsernameContextMenu(url)
}, tag: GroupInfoEntryTag.link)
case let .location(theme, location): case let .location(theme, location):
let imageSignal = chatMapSnapshotImage(account: arguments.context.account, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90)) let imageSignal = chatMapSnapshotImage(account: arguments.context.account, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90))
return ItemListAddressItem(theme: theme, label: "", text: location.address, imageSignal: imageSignal, selected: nil, sectionId: self.section, style: .blocks, action: { return ItemListAddressItem(theme: theme, label: "", text: location.address.replacingOccurrences(of: ", ", with: "\n"), imageSignal: imageSignal, selected: nil, sectionId: self.section, style: .blocks, action: {
arguments.openLocation(location) arguments.openLocation(location)
}, longTapAction: { }, longTapAction: {
arguments.displayLocationContextMenu(location.address.replacingOccurrences(of: "\n", with: ", ")) arguments.displayLocationContextMenu(location.address.replacingOccurrences(of: "\n", with: ", "))
}, tag: GroupInfoEntryTag.location) }, tag: GroupInfoEntryTag.location)
case let .changeLocation(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.changeLocation()
}, clearHighlightAutomatically: false)
case let .link(theme, url):
return ItemListActionItem(theme: theme, title: url, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.displayUsernameShareMenu(url)
}, longTapAction: {
arguments.displayUsernameContextMenu(url)
}, tag: GroupInfoEntryTag.link)
case let .notifications(theme, title, text): case let .notifications(theme, title, text):
return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: {
arguments.changeNotificationMuteSettings() arguments.changeNotificationMuteSettings()
@ -806,21 +821,33 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa
entries.append(.administrators(presentationData.theme, presentationData.strings.GroupInfo_Administrators, "")) entries.append(.administrators(presentationData.theme, presentationData.strings.GroupInfo_Administrators, ""))
} }
} else if let channel = view.peers[view.peerId] as? TelegramChannel, let cachedChannelData = view.cachedData as? CachedChannelData { } else if let channel = view.peers[view.peerId] as? TelegramChannel, let cachedChannelData = view.cachedData as? CachedChannelData {
if isCreator || (channel.adminRights != nil && channel.hasPermission(.pinMessages)) { if isCreator, let location = cachedChannelData.peerGeoLocation {
if cachedChannelData.flags.contains(.canChangeUsername) { entries.append(.locationHeader(presentationData.theme, presentationData.strings.GroupInfo_Location.uppercased()))
entries.append(GroupInfoEntry.groupTypeSetup(presentationData.theme, presentationData.strings.GroupInfo_GroupType, isPublic ? presentationData.strings.Channel_Setup_TypePublic : presentationData.strings.Channel_Setup_TypePrivate)) entries.append(.location(presentationData.theme, location))
if let linkedDiscussionPeerId = cachedChannelData.linkedDiscussionPeerId, let peer = view.peers[linkedDiscussionPeerId] { if cachedChannelData.flags.contains(.canChangePeerGeoLocation) {
let peerTitle: String entries.append(.changeLocation(presentationData.theme, presentationData.strings.Group_Location_ChangeLocation))
if let addressName = peer.addressName, !addressName.isEmpty {
peerTitle = "@\(addressName)"
} else {
peerTitle = peer.displayTitle
}
entries.append(GroupInfoEntry.linkedChannelSetup(presentationData.theme, presentationData.strings.Group_LinkedChannel, peerTitle))
}
} }
if !isPublic && cachedChannelData.linkedDiscussionPeerId == nil { }
entries.append(GroupInfoEntry.preHistory(presentationData.theme, presentationData.strings.GroupInfo_GroupHistory, cachedChannelData.flags.contains(.preHistoryEnabled) ? presentationData.strings.GroupInfo_GroupHistoryVisible : presentationData.strings.GroupInfo_GroupHistoryHidden))
if isCreator || (channel.adminRights != nil && channel.hasPermission(.pinMessages)) {
if cachedChannelData.peerGeoLocation != nil {
entries.append(GroupInfoEntry.groupTypeSetup(presentationData.theme, presentationData.strings.GroupInfo_PublicLink, channel.addressName ?? presentationData.strings.GroupInfo_PublicLinkAdd))
} else {
if cachedChannelData.flags.contains(.canChangeUsername) {
entries.append(GroupInfoEntry.groupTypeSetup(presentationData.theme, presentationData.strings.GroupInfo_GroupType, isPublic ? presentationData.strings.Channel_Setup_TypePublic : presentationData.strings.Channel_Setup_TypePrivate))
if let linkedDiscussionPeerId = cachedChannelData.linkedDiscussionPeerId, let peer = view.peers[linkedDiscussionPeerId] {
let peerTitle: String
if let addressName = peer.addressName, !addressName.isEmpty {
peerTitle = "@\(addressName)"
} else {
peerTitle = peer.displayTitle
}
entries.append(GroupInfoEntry.linkedChannelSetup(presentationData.theme, presentationData.strings.Group_LinkedChannel, peerTitle))
}
}
if !isPublic && cachedChannelData.linkedDiscussionPeerId == nil {
entries.append(GroupInfoEntry.preHistory(presentationData.theme, presentationData.strings.GroupInfo_GroupHistory, cachedChannelData.flags.contains(.preHistoryEnabled) ? presentationData.strings.GroupInfo_GroupHistoryVisible : presentationData.strings.GroupInfo_GroupHistoryHidden))
}
} }
} }
@ -855,24 +882,23 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa
} }
} else { } else {
if let peer = peerViewMainPeer(view), peer.isScam { if let peer = peerViewMainPeer(view), peer.isScam {
entries.append(.aboutHeader(presentationData.theme, presentationData.strings.Channel_About_Title.uppercased()))
entries.append(.about(presentationData.theme, presentationData.strings.GroupInfo_ScamGroupWarning)) entries.append(.about(presentationData.theme, presentationData.strings.GroupInfo_ScamGroupWarning))
} }
else if let cachedChannelData = view.cachedData as? CachedChannelData { else if let cachedChannelData = view.cachedData as? CachedChannelData {
if let about = cachedChannelData.about, !about.isEmpty { if let about = cachedChannelData.about, !about.isEmpty {
entries.append(.aboutHeader(presentationData.theme, presentationData.strings.Channel_About_Title.uppercased()))
entries.append(.about(presentationData.theme, about)) entries.append(.about(presentationData.theme, about))
} }
if let peer = view.peers[view.peerId] as? TelegramChannel, let username = peer.username, !username.isEmpty { if let peer = view.peers[view.peerId] as? TelegramChannel {
entries.append(.link(presentationData.theme, "t.me/" + username))
if let location = cachedChannelData.peerGeoLocation { if let location = cachedChannelData.peerGeoLocation {
entries.append(.locationHeader(presentationData.theme, presentationData.strings.GroupInfo_Location.uppercased()))
entries.append(.location(presentationData.theme, location)) entries.append(.location(presentationData.theme, location))
} }
if let username = peer.username, !username.isEmpty {
entries.append(.link(presentationData.theme, "t.me/" + username))
}
} }
} else if let cachedGroupData = view.cachedData as? CachedGroupData { } else if let cachedGroupData = view.cachedData as? CachedGroupData {
if let about = cachedGroupData.about, !about.isEmpty { if let about = cachedGroupData.about, !about.isEmpty {
entries.append(.aboutHeader(presentationData.theme, presentationData.strings.Channel_About_Title.uppercased()))
entries.append(.about(presentationData.theme, about)) entries.append(.about(presentationData.theme, about))
} }
} }
@ -1239,6 +1265,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
var endEditingImpl: (() -> Void)? var endEditingImpl: (() -> Void)?
var removePeerChatImpl: ((Peer, Bool) -> Void)? var removePeerChatImpl: ((Peer, Bool) -> Void)?
var errorImpl: (() -> Void)? var errorImpl: (() -> Void)?
var clearHighlightImpl: (() -> Void)?
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
@ -1967,6 +1994,40 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
let controller = legacyLocationController(message: nil, mapMedia: mapMedia, context: context, isModal: false, openPeer: { _ in }, sendLiveLocation: { _, _ in }, stopLiveLocation: {}, openUrl: { _ in }) let controller = legacyLocationController(message: nil, mapMedia: mapMedia, context: context, isModal: false, openPeer: { _ in }, sendLiveLocation: { _, _ in }, stopLiveLocation: {}, openUrl: { _ in })
pushControllerImpl?(controller) pushControllerImpl?(controller)
}) })
}, changeLocation: {
let _ = (peerView.get()
|> take(1)
|> deliverOnMainQueue).start(next: { peerView in
guard let peer = peerView.peers[peerView.peerId] else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = legacyLocationPickerController(context: context, selfPeer: peer, peer: peer, sendLocation: { coordinate, _, address in
let addressSignal: Signal<String, NoError>
if let address = address {
addressSignal = .single(address)
} else {
addressSignal = reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|> map { placemark in
if let placemark = placemark {
return placemark.fullAddress
} else {
return "\(coordinate.latitude), \(coordinate.longitude)"
}
}
}
let _ = (addressSignal
|> mapToSignal { address -> Signal<Bool, NoError> in
return updateChannelGeoLocation(postbox: context.account.postbox, network: context.account.network, channelId: peer.id, coordinate: (coordinate.latitude, coordinate.longitude), address: address)
}
|> deliverOnMainQueue).start()
}, sendLiveLocation: { _, _ in }, theme: presentationData.theme, customLocationPicker: true, presentationCompleted: {
clearHighlightImpl?()
})
presentControllerImpl?(controller, nil)
})
}, displayLocationContextMenu: { text in }, displayLocationContextMenu: { text in
displayCopyContextMenuImpl?(text, .location) displayCopyContextMenuImpl?(text, .location)
}) })
@ -2320,6 +2381,9 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
[weak controller] in [weak controller] in
controller?.view.endEditing(true) controller?.view.endEditing(true)
} }
clearHighlightImpl = { [weak controller] in
controller?.clearItemNodesHighlight(animated: true)
}
let hapticFeedback = HapticFeedback() let hapticFeedback = HapticFeedback()
errorImpl = { [weak controller] in errorImpl = { [weak controller] in

View File

@ -11,7 +11,7 @@ private func generateClearIcon(color: UIColor) -> UIImage? {
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color)
} }
func legacyLocationPickerController(context: AccountContext, selfPeer: Peer, peer: Peer, sendLocation: @escaping (CLLocationCoordinate2D, MapVenue?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D, Int32) -> Void, theme: PresentationTheme, customLocationPicker: Bool = false, presentationCompleted: @escaping () -> Void = {}) -> ViewController { func legacyLocationPickerController(context: AccountContext, selfPeer: Peer, peer: Peer, sendLocation: @escaping (CLLocationCoordinate2D, MapVenue?, String?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D, Int32) -> Void, theme: PresentationTheme, customLocationPicker: Bool = false, presentationCompleted: @escaping () -> Void = {}) -> ViewController {
let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: theme) let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: theme)
legacyController.presentationCompleted = { legacyController.presentationCompleted = {
presentationCompleted() presentationCompleted()
@ -34,10 +34,10 @@ func legacyLocationPickerController(context: AccountContext, selfPeer: Peer, pee
}, rootController: nil) }, rootController: nil)
legacyController.bind(controller: navigationController) legacyController.bind(controller: navigationController)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
controller.locationPicked = { [weak legacyController] coordinate, venue in controller.locationPicked = { [weak legacyController] coordinate, venue, address in
sendLocation(coordinate, venue.flatMap { venue in sendLocation(coordinate, venue.flatMap { venue in
return MapVenue(title: venue.title, address: venue.address, provider: venue.provider, id: venue.venueId, type: venue.type) return MapVenue(title: venue.title, address: venue.address, provider: venue.provider, id: venue.venueId, type: venue.type)
}) }, address)
legacyController?.dismiss() legacyController?.dismiss()
} }
controller.liveLocationStarted = { [weak legacyController] coordinate, period in controller.liveLocationStarted = { [weak legacyController] coordinate, period in

View File

@ -37,9 +37,9 @@ private func arePeerNearbyArraysEqual(_ lhs: [PeerNearbyEntry], _ rhs: [PeerNear
private final class PeersNearbyControllerArguments { private final class PeersNearbyControllerArguments {
let context: AccountContext let context: AccountContext
let openChat: (Peer) -> Void let openChat: (Peer) -> Void
let openCreateGroup: (Double, Double) -> Void let openCreateGroup: (Double, Double, String?) -> Void
init(context: AccountContext, openChat: @escaping (Peer) -> Void, openCreateGroup: @escaping (Double, Double) -> Void) { init(context: AccountContext, openChat: @escaping (Peer) -> Void, openCreateGroup: @escaping (Double, Double, String?) -> Void) {
self.context = context self.context = context
self.openChat = openChat self.openChat = openChat
self.openCreateGroup = openCreateGroup self.openCreateGroup = openCreateGroup
@ -61,7 +61,7 @@ private enum PeersNearbyEntry: ItemListNodeEntry {
case user(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry) case user(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry)
case groupsHeader(PresentationTheme, String) case groupsHeader(PresentationTheme, String)
case createGroup(PresentationTheme, String, Double?, Double?) case createGroup(PresentationTheme, String, Double?, Double?, String?)
case group(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry) case group(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry)
case channelsHeader(PresentationTheme, String) case channelsHeader(PresentationTheme, String)
@ -135,8 +135,8 @@ private enum PeersNearbyEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .createGroup(lhsTheme, lhsText, lhsLatitude, lhsLongitude): case let .createGroup(lhsTheme, lhsText, lhsLatitude, lhsLongitude, lhsAddress):
if case let .createGroup(rhsTheme, rhsText, rhsLatitude, rhsLongitude) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLatitude == rhsLatitude && lhsLongitude == rhsLongitude { if case let .createGroup(rhsTheme, rhsText, rhsLatitude, rhsLongitude, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLatitude == rhsLatitude && lhsLongitude == rhsLongitude && lhsAddress == rhsAddress {
return true return true
} else { } else {
return false return false
@ -183,7 +183,7 @@ private enum PeersNearbyEntry: ItemListNodeEntry {
return PeersNearbyHeaderItem(theme: theme, text: text, sectionId: self.section) return PeersNearbyHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .usersHeader(theme, text): case let .usersHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .empty(theme, text, loading): case let .empty(theme, text, _):
return ItemListPlaceholderItem(theme: theme, text: text, sectionId: self.section, style: .blocks) return ItemListPlaceholderItem(theme: theme, text: text, sectionId: self.section, style: .blocks)
case let .user(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer): case let .user(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer.peer.0, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: .text(strings.Map_DistanceAway(stringForDistance(peer.distance)).0), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer.peer.0, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: .text(strings.Map_DistanceAway(stringForDistance(peer.distance)).0), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
@ -191,10 +191,10 @@ private enum PeersNearbyEntry: ItemListNodeEntry {
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopGroupInset: false, tag: nil) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopGroupInset: false, tag: nil)
case let .groupsHeader(theme, text): case let .groupsHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .createGroup(theme, title, latitude, longitude): case let .createGroup(theme, title, latitude, longitude, address):
return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.createGroupIcon(theme), title: title, alwaysPlain: false, sectionId: self.section, editing: false, action: { return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.createGroupIcon(theme), title: title, alwaysPlain: false, sectionId: self.section, editing: false, action: {
if let latitude = latitude, let longitude = longitude { if let latitude = latitude, let longitude = longitude {
arguments.openCreateGroup(latitude, longitude) arguments.openCreateGroup(latitude, longitude, address)
} }
}) })
case let .group(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer): case let .group(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer):
@ -226,20 +226,22 @@ private enum PeersNearbyEntry: ItemListNodeEntry {
private struct PeersNearbyData: Equatable { private struct PeersNearbyData: Equatable {
let latitude: Double let latitude: Double
let longitude: Double let longitude: Double
let address: String?
let users: [PeerNearbyEntry] let users: [PeerNearbyEntry]
let groups: [PeerNearbyEntry] let groups: [PeerNearbyEntry]
let channels: [PeerNearbyEntry] let channels: [PeerNearbyEntry]
init(latitude: Double, longitude: Double, users: [PeerNearbyEntry], groups: [PeerNearbyEntry], channels: [PeerNearbyEntry]) { init(latitude: Double, longitude: Double, address: String?, users: [PeerNearbyEntry], groups: [PeerNearbyEntry], channels: [PeerNearbyEntry]) {
self.latitude = latitude self.latitude = latitude
self.longitude = longitude self.longitude = longitude
self.address = address
self.users = users self.users = users
self.groups = groups self.groups = groups
self.channels = channels self.channels = channels
} }
static func ==(lhs: PeersNearbyData, rhs: PeersNearbyData) -> Bool { static func ==(lhs: PeersNearbyData, rhs: PeersNearbyData) -> Bool {
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude && arePeerNearbyArraysEqual(lhs.users, rhs.users) && arePeerNearbyArraysEqual(lhs.groups, rhs.groups) && arePeerNearbyArraysEqual(lhs.channels, rhs.channels) return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude && lhs.address == rhs.address && arePeerNearbyArraysEqual(lhs.users, rhs.users) && arePeerNearbyArraysEqual(lhs.groups, rhs.groups) && arePeerNearbyArraysEqual(lhs.channels, rhs.channels)
} }
} }
@ -259,7 +261,7 @@ private func peersNearbyControllerEntries(data: PeersNearbyData?, presentationDa
} }
entries.append(.groupsHeader(presentationData.theme, presentationData.strings.PeopleNearby_Groups.uppercased())) entries.append(.groupsHeader(presentationData.theme, presentationData.strings.PeopleNearby_Groups.uppercased()))
entries.append(.createGroup(presentationData.theme, presentationData.strings.PeopleNearby_CreateGroup, data?.latitude, data?.longitude)) entries.append(.createGroup(presentationData.theme, presentationData.strings.PeopleNearby_CreateGroup, data?.latitude, data?.longitude, data?.address))
if let data = data, !data.groups.isEmpty { if let data = data, !data.groups.isEmpty {
var i: Int32 = 0 var i: Int32 = 0
for group in data.groups { for group in data.groups {
@ -282,18 +284,25 @@ private func peersNearbyControllerEntries(data: PeersNearbyData?, presentationDa
public func peersNearbyController(context: AccountContext) -> ViewController { public func peersNearbyController(context: AccountContext) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
var replaceTopControllerImpl: ((ViewController, Bool) -> Void)? var replaceAllButRootControllerImpl: ((ViewController, Bool) -> Void)?
var replaceTopControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var navigateToChatImpl: ((Peer) -> Void)? var navigateToChatImpl: ((Peer) -> Void)?
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
let dataPromise = Promise<PeersNearbyData?>(nil) let dataPromise = Promise<PeersNearbyData?>(nil)
let addressPromise = Promise<String?>(nil)
let arguments = PeersNearbyControllerArguments(context: context, openChat: { peer in let arguments = PeersNearbyControllerArguments(context: context, openChat: { peer in
navigateToChatImpl?(peer) navigateToChatImpl?(peer)
}, openCreateGroup: { latitude, longitude in }, openCreateGroup: { latitude, longitude, address in
let controller = createGroupController(context: context, peerIds: [], type: .locatedGroup(latitude: latitude, longitude: longitude)) let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = PermissionController(context: context, splashScreen: true)
controller.setState(.custom(icon: PermissionControllerCustomIcon(light: UIImage(bundleImageName: "Location/LocalGroupLightIcon"), dark: UIImage(bundleImageName: "Location/LocalGroupDarkIcon")), title: presentationData.strings.LocalGroup_Title, subtitle: address, text: presentationData.strings.LocalGroup_Text, buttonTitle: presentationData.strings.LocalGroup_ButtonTitle, footerText: presentationData.strings.LocalGroup_IrrelevantWarning), animated: false)
controller.proceed = { result in
replaceTopControllerImpl?(createGroupController(context: context, peerIds: [], mode: .locatedGroup(latitude: latitude, longitude: longitude, address: address)))
}
pushControllerImpl?(controller) pushControllerImpl?(controller)
}) })
@ -307,9 +316,23 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
return Signal { subscriber in return Signal { subscriber in
let peersNearbyContext = PeersNearbyContext(network: context.account.network, accountStateManager: context.account.stateManager, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude)) let peersNearbyContext = PeersNearbyContext(network: context.account.network, accountStateManager: context.account.stateManager, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude))
let peersNearby: Signal<PeersNearbyData?, Void> = peersNearbyContext.get() let peersNearby: Signal<PeersNearbyData?, Void> = combineLatest(peersNearbyContext.get(), addressPromise.get())
|> introduceError(Void.self) |> introduceError(Void.self)
|> mapToSignal { peersNearby -> Signal<PeersNearbyData?, Void> in |> mapToSignal { peersNearby, address -> Signal<([PeerNearby]?, String?), Void> in
if let address = address {
return .single((peersNearby, address))
} else {
return reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|> introduceError(Void.self)
|> map { placemark in
return (peersNearby, placemark?.fullAddress)
}
}
}
|> mapToSignal { peersNearby, address -> Signal<PeersNearbyData?, Void> in
guard let peersNearby = peersNearby else {
return .single(nil)
}
return context.account.postbox.transaction { transaction -> PeersNearbyData? in return context.account.postbox.transaction { transaction -> PeersNearbyData? in
var users: [PeerNearbyEntry] = [] var users: [PeerNearbyEntry] = []
var groups: [PeerNearbyEntry] = [] var groups: [PeerNearbyEntry] = []
@ -323,7 +346,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
} }
} }
} }
return PeersNearbyData(latitude: coordinate.latitude, longitude: coordinate.longitude, users: users, groups: groups, channels: []) return PeersNearbyData(latitude: coordinate.latitude, longitude: coordinate.longitude, address: address, users: users, groups: groups, channels: [])
} }
|> introduceError(Void.self) |> introduceError(Void.self)
} }
@ -346,8 +369,10 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
|> restartIfError |> restartIfError
|> `catch` { _ -> Signal<PeersNearbyData?, NoError> in |> `catch` { _ -> Signal<PeersNearbyData?, NoError> in
return .single(nil) return .single(nil)
} |> filter { value in
return value != nil
} }
dataPromise.set(combinedSignal) dataPromise.set(.single(nil) |> then(combinedSignal))
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get()) let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get())
|> deliverOnMainQueue |> deliverOnMainQueue
@ -362,11 +387,14 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
} }
let controller = ItemListController(context: context, state: signal) let controller = ItemListController(context: context, state: signal)
controller.didDisappear = { [weak controller] _ in
controller?.clearItemNodesHighlight(animated: true)
}
navigateToChatImpl = { [weak controller] peer in navigateToChatImpl = { [weak controller] peer in
if let navigationController = controller?.navigationController as? NavigationController { if let navigationController = controller?.navigationController as? NavigationController {
navigateToChatController(navigationController: navigationController, context: context, chatLocation: .peer(peer.id), keepStack: .always, purposefulAction: { [weak navigationController] in navigateToChatController(navigationController: navigationController, context: context, chatLocation: .peer(peer.id), keepStack: .always, purposefulAction: { [weak navigationController] in
if let navigationController = navigationController, let chatController = navigationController.viewControllers.last as? ChatController { if let navigationController = navigationController, let chatController = navigationController.viewControllers.last as? ChatController {
replaceTopControllerImpl?(chatController, false) replaceAllButRootControllerImpl?(chatController, false)
} }
}) })
} }
@ -376,11 +404,16 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
(controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true)
} }
} }
replaceTopControllerImpl = { [weak controller] c, a in replaceAllButRootControllerImpl = { [weak controller] c, a in
if let controller = controller { if let controller = controller {
(controller.navigationController as? NavigationController)?.replaceAllButRootController(c, animated: a) (controller.navigationController as? NavigationController)?.replaceAllButRootController(c, animated: a)
} }
} }
replaceTopControllerImpl = { [weak controller] c in
if let controller = controller {
(controller.navigationController as? NavigationController)?.replaceTopController(c, animated: true)
}
}
presentControllerImpl = { [weak controller] c, p in presentControllerImpl = { [weak controller] c, p in
if let controller = controller { if let controller = controller {
controller.present(c, in: .window(.root), with: p) controller.present(c, in: .window(.root), with: p)

View File

@ -76,7 +76,7 @@ class PeersNearbyHeaderItemNode: ListViewItemNode {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, params, neighbors in return { item, params, neighbors in
let leftInset: CGFloat = 54.0 + params.leftInset let leftInset: CGFloat = 48.0 + params.leftInset
let topInset: CGFloat = 92.0 let topInset: CGFloat = 92.0
let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor) let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor)

View File

@ -6,13 +6,15 @@ import TelegramPresentationData
final class PermissionContentNode: ASDisplayNode { final class PermissionContentNode: ASDisplayNode {
private var theme: PresentationTheme private var theme: PresentationTheme
let kind: PermissionKind let kind: Int32
private let iconNode: ASImageNode private let iconNode: ASImageNode
private let nearbyIconNode: PeersNearbyIconNode? private let nearbyIconNode: PeersNearbyIconNode?
private let titleNode: ImmediateTextNode private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private let textNode: ImmediateTextNode private let textNode: ImmediateTextNode
private let actionButton: SolidRoundedButtonNode private let actionButton: SolidRoundedButtonNode
private let footerNode: ImmediateTextNode
private let privacyPolicyButton: HighlightableButtonNode private let privacyPolicyButton: HighlightableButtonNode
private var title: String private var title: String
@ -20,7 +22,7 @@ final class PermissionContentNode: ASDisplayNode {
var buttonAction: (() -> Void)? var buttonAction: (() -> Void)?
var openPrivacyPolicy: (() -> Void)? var openPrivacyPolicy: (() -> Void)?
init(theme: PresentationTheme, strings: PresentationStrings, kind: PermissionKind, icon: UIImage?, title: String, text: String, buttonTitle: String, buttonAction: @escaping () -> Void, openPrivacyPolicy: (() -> Void)?) { init(theme: PresentationTheme, strings: PresentationStrings, kind: Int32, icon: UIImage?, title: String, subtitle: String? = nil, text: String, buttonTitle: String, footerText: String? = nil, buttonAction: @escaping () -> Void, openPrivacyPolicy: (() -> Void)?) {
self.theme = theme self.theme = theme
self.kind = kind self.kind = kind
@ -34,7 +36,7 @@ final class PermissionContentNode: ASDisplayNode {
self.iconNode.displayWithoutProcessing = true self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false self.iconNode.displaysAsynchronously = false
if kind == .nearbyLocation { if kind == PermissionKind.nearbyLocation.rawValue {
self.nearbyIconNode = PeersNearbyIconNode(theme: theme) self.nearbyIconNode = PeersNearbyIconNode(theme: theme)
} else { } else {
self.nearbyIconNode = nil self.nearbyIconNode = nil
@ -46,6 +48,12 @@ final class PermissionContentNode: ASDisplayNode {
self.titleNode.isUserInteractionEnabled = false self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false self.titleNode.displaysAsynchronously = false
self.subtitleNode = ImmediateTextNode()
self.subtitleNode.maximumNumberOfLines = 1
self.subtitleNode.textAlignment = .center
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.displaysAsynchronously = false
self.textNode = ImmediateTextNode() self.textNode = ImmediateTextNode()
self.textNode.textAlignment = .center self.textNode.textAlignment = .center
self.textNode.maximumNumberOfLines = 0 self.textNode.maximumNumberOfLines = 0
@ -53,6 +61,11 @@ final class PermissionContentNode: ASDisplayNode {
self.actionButton = SolidRoundedButtonNode(theme: theme, height: 48.0, cornerRadius: 9.0) self.actionButton = SolidRoundedButtonNode(theme: theme, height: 48.0, cornerRadius: 9.0)
self.footerNode = ImmediateTextNode()
self.footerNode.textAlignment = .center
self.footerNode.maximumNumberOfLines = 0
self.footerNode.displaysAsynchronously = false
self.privacyPolicyButton = HighlightableButtonNode() self.privacyPolicyButton = HighlightableButtonNode()
self.privacyPolicyButton.setTitle(strings.Permissions_PrivacyPolicy, with: Font.regular(16.0), with: theme.list.itemAccentColor, for: .normal) self.privacyPolicyButton.setTitle(strings.Permissions_PrivacyPolicy, with: Font.regular(16.0), with: theme.list.itemAccentColor, for: .normal)
@ -68,13 +81,23 @@ final class PermissionContentNode: ASDisplayNode {
self.actionButton.title = buttonTitle self.actionButton.title = buttonTitle
self.privacyPolicyButton.isHidden = openPrivacyPolicy == nil self.privacyPolicyButton.isHidden = openPrivacyPolicy == nil
if let subtitle = subtitle {
self.subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
}
if let footerText = footerText {
self.footerNode.attributedText = NSAttributedString(string: footerText, font: Font.regular(13.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
}
self.addSubnode(self.iconNode) self.addSubnode(self.iconNode)
if let nearbyIconNode = self.nearbyIconNode { if let nearbyIconNode = self.nearbyIconNode {
self.addSubnode(nearbyIconNode) self.addSubnode(nearbyIconNode)
} }
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.textNode) self.addSubnode(self.textNode)
self.addSubnode(self.actionButton) self.addSubnode(self.actionButton)
self.addSubnode(self.footerNode)
self.addSubnode(self.privacyPolicyButton) self.addSubnode(self.privacyPolicyButton)
self.actionButton.pressed = { [weak self] in self.actionButton.pressed = { [weak self] in
@ -93,25 +116,33 @@ final class PermissionContentNode: ASDisplayNode {
let fontSize: CGFloat let fontSize: CGFloat
if min(size.width, size.height) > 330.0 { if min(size.width, size.height) > 330.0 {
fontSize = 24.0 fontSize = 24.0
sidePadding = 38.0 sidePadding = 36.0
} else { } else {
fontSize = 20.0 fontSize = 20.0
sidePadding = 20.0 sidePadding = 20.0
} }
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(fontSize), textColor: self.theme.list.itemPrimaryTextColor) let smallerSidePadding: CGFloat = 20.0
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(fontSize), textColor: self.theme.list.itemPrimaryTextColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude)) let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude))
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: size.width - smallerSidePadding * 2.0, height: .greatestFiniteMagnitude))
let textSize = self.textNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude)) let textSize = self.textNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude))
let buttonWidth = min(size.width, size.height) let buttonWidth = min(size.width, size.height)
let buttonHeight = self.actionButton.updateLayout(width: buttonWidth, transition: transition) let buttonHeight = self.actionButton.updateLayout(width: buttonWidth, transition: transition)
let footerSize = self.footerNode.updateLayout(CGSize(width: size.width - smallerSidePadding * 2.0, height: .greatestFiniteMagnitude))
let privacyButtonSize = self.privacyPolicyButton.measure(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude)) let privacyButtonSize = self.privacyPolicyButton.measure(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude))
let availableHeight = floor(size.height - insets.top - insets.bottom - titleSize.height - textSize.height - buttonHeight) let availableHeight = floor(size.height - insets.top - insets.bottom - titleSize.height - subtitleSize.height - textSize.height - buttonHeight)
let titleSubtitleSpacing: CGFloat = max(15.0, floor(availableHeight * 0.055)) let titleTextSpacing: CGFloat = max(15.0, floor(availableHeight * 0.045))
let titleSubtitleSpacing: CGFloat = 6.0
let buttonSpacing: CGFloat = max(19.0, floor(availableHeight * 0.075)) let buttonSpacing: CGFloat = max(19.0, floor(availableHeight * 0.075))
var contentHeight = titleSize.height + titleSubtitleSpacing + textSize.height + buttonHeight + buttonSpacing var contentHeight = titleSize.height + titleTextSpacing + textSize.height + buttonHeight + buttonSpacing
if subtitleSize.height > 0.0 {
contentHeight += titleSubtitleSpacing + subtitleSize.height
}
var imageSize = CGSize() var imageSize = CGSize()
var imageSpacing: CGFloat = 0.0 var imageSpacing: CGFloat = 0.0
@ -126,24 +157,36 @@ final class PermissionContentNode: ASDisplayNode {
contentHeight += imageSize.height + imageSpacing contentHeight += imageSize.height + imageSpacing
} }
let privacySpacing: CGFloat = max(30.0 + privacyButtonSize.height, (availableHeight - titleSubtitleSpacing - buttonSpacing - imageSize.height - imageSpacing) / 2.0) let privacySpacing: CGFloat = max(30.0 + privacyButtonSize.height, (availableHeight - titleTextSpacing - buttonSpacing - imageSize.height - imageSpacing) / 2.0)
let contentOrigin = insets.top + floor((size.height - insets.top - insets.bottom - contentHeight) / 2.0) let contentOrigin = insets.top + floor((size.height - insets.top - insets.bottom - contentHeight) / 2.0) - availableHeight * 0.05
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize) let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize)
let nearbyIconFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize) let nearbyIconFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize)
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: iconFrame.maxY + imageSpacing), size: titleSize) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: iconFrame.maxY + imageSpacing), size: titleSize)
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSubtitleSpacing), size: textSize)
let buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonWidth) / 2.0), y: textFrame.maxY + buttonSpacing), size: CGSize(width: buttonWidth, height: buttonHeight))
let privacyButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - privacyButtonSize.width) / 2.0), y: buttonFrame.maxY + floor((privacySpacing - privacyButtonSize.height) / 2.0)), size: privacyButtonSize)
let subtitleFrame: CGRect
if subtitleSize.height > 0.0 {
subtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + titleSubtitleSpacing), size: subtitleSize)
} else {
subtitleFrame = titleFrame
}
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: subtitleFrame.maxY + titleTextSpacing), size: textSize)
let buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonWidth) / 2.0), y: textFrame.maxY + buttonSpacing), size: CGSize(width: buttonWidth, height: buttonHeight))
let footerFrame = CGRect(origin: CGPoint(x: floor((size.width - footerSize.width) / 2.0), y: size.height - footerSize.height - insets.bottom - 8.0), size: footerSize)
let privacyButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - privacyButtonSize.width) / 2.0), y: buttonFrame.maxY + floor((privacySpacing - privacyButtonSize.height) / 2.0)), size: privacyButtonSize)
transition.updateFrame(node: self.iconNode, frame: iconFrame) transition.updateFrame(node: self.iconNode, frame: iconFrame)
if let nearbyIconNode = self.nearbyIconNode { if let nearbyIconNode = self.nearbyIconNode {
transition.updateFrame(node: nearbyIconNode, frame: nearbyIconFrame) transition.updateFrame(node: nearbyIconNode, frame: nearbyIconFrame)
} }
transition.updateFrame(node: self.titleNode, frame: titleFrame) transition.updateFrame(node: self.titleNode, frame: titleFrame)
transition.updateFrame(node: self.subtitleNode, frame: subtitleFrame)
transition.updateFrame(node: self.textNode, frame: textFrame) transition.updateFrame(node: self.textNode, frame: textFrame)
transition.updateFrame(node: self.actionButton, frame: buttonFrame) transition.updateFrame(node: self.actionButton, frame: buttonFrame)
transition.updateFrame(node: self.footerNode, frame: footerFrame)
transition.updateFrame(node: self.privacyPolicyButton, frame: privacyButtonFrame) transition.updateFrame(node: self.privacyPolicyButton, frame: privacyButtonFrame)
} }
} }

View File

@ -10,7 +10,7 @@ import DeviceAccess
public final class PermissionController : ViewController { public final class PermissionController : ViewController {
private let context: AccountContext private let context: AccountContext
private let splitTest: PermissionUISplitTest? private let splitTest: PermissionUISplitTest?
private var state: PermissionState? private var state: PermissionControllerContent?
private var splashScreen = false private var splashScreen = false
private var controllerNode: PermissionControllerNode { private var controllerNode: PermissionControllerNode {
@ -101,95 +101,103 @@ public final class PermissionController : ViewController {
self.context.sharedContext.applicationBindings.openSettings() self.context.sharedContext.applicationBindings.openSettings()
} }
public func setState(_ state: PermissionState, animated: Bool) { public func setState(_ state: PermissionControllerContent, animated: Bool) {
guard state != self.state else { guard state != self.state else {
return return
} }
self.state = state self.state = state
switch state { if case let .permission(permission) = state, let state = permission {
case let .contacts(status): switch state {
self.splitTest?.addEvent(.ContactsModalRequest) case let .contacts(status):
self.splitTest?.addEvent(.ContactsModalRequest)
self.allow = { [weak self] in
if let strongSelf = self { self.allow = { [weak self] in
switch status { if let strongSelf = self {
case .requestable: switch status {
strongSelf.splitTest?.addEvent(.ContactsRequest) case .requestable:
DeviceAccess.authorizeAccess(to: .contacts, { [weak self] result in strongSelf.splitTest?.addEvent(.ContactsRequest)
if let strongSelf = self { DeviceAccess.authorizeAccess(to: .contacts, { [weak self] result in
if result { if let strongSelf = self {
strongSelf.splitTest?.addEvent(.ContactsAllowed) if result {
} else { strongSelf.splitTest?.addEvent(.ContactsAllowed)
strongSelf.splitTest?.addEvent(.ContactsDenied) } else {
strongSelf.splitTest?.addEvent(.ContactsDenied)
}
strongSelf.proceed?(true)
} }
strongSelf.proceed?(true) })
} case .denied:
}) strongSelf.openAppSettings()
case .denied: strongSelf.proceed?(true)
strongSelf.openAppSettings() default:
strongSelf.proceed?(true) break
default: }
break
} }
} }
} case let .notifications(status):
case let .notifications(status): self.splitTest?.addEvent(.NotificationsModalRequest)
self.splitTest?.addEvent(.NotificationsModalRequest)
self.allow = { [weak self] in
self.allow = { [weak self] in if let strongSelf = self {
if let strongSelf = self { switch status {
switch status { case .requestable:
case .requestable: strongSelf.splitTest?.addEvent(.NotificationsRequest)
strongSelf.splitTest?.addEvent(.NotificationsRequest) let context = strongSelf.context
let context = strongSelf.context DeviceAccess.authorizeAccess(to: .notifications, registerForNotifications: { [weak context] result in
DeviceAccess.authorizeAccess(to: .notifications, registerForNotifications: { [weak context] result in context?.sharedContext.applicationBindings.registerForNotifications(result)
context?.sharedContext.applicationBindings.registerForNotifications(result) }, { [weak self] result in
}, { [weak self] result in if let strongSelf = self {
if let strongSelf = self { if result {
if result { strongSelf.splitTest?.addEvent(.NotificationsAllowed)
strongSelf.splitTest?.addEvent(.NotificationsAllowed) } else {
} else { strongSelf.splitTest?.addEvent(.NotificationsDenied)
strongSelf.splitTest?.addEvent(.NotificationsDenied) }
strongSelf.proceed?(true)
} }
strongSelf.proceed?(true) })
} case .denied, .unreachable:
}) strongSelf.openAppSettings()
strongSelf.proceed?(true)
default:
break
}
}
}
case .siri:
self.allow = { [weak self] in
self?.proceed?(true)
}
case .cellularData:
self.allow = { [weak self] in
self?.proceed?(true)
}
case let .nearbyLocation(status):
self.title = self.presentationData.strings.Permissions_PeopleNearbyTitle_v0
self.navigationItem.rightBarButtonItem = nil
self.allow = { [weak self] in
if let strongSelf = self {
switch status {
case .requestable:
DeviceAccess.authorizeAccess(to: .location(.tracking), presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, { [weak self] result in
self?.proceed?(result)
})
case .denied, .unreachable: case .denied, .unreachable:
strongSelf.openAppSettings() strongSelf.openAppSettings()
strongSelf.proceed?(true) strongSelf.proceed?(false)
default: default:
break break
}
} }
} }
}
} else {
self.allow = { [weak self] in
if let strongSelf = self {
strongSelf.proceed?(true)
} }
case .siri: }
self.allow = { [weak self] in
self?.proceed?(true)
}
case .cellularData:
self.allow = { [weak self] in
self?.proceed?(true)
}
case let .nearbyLocation(status):
self.title = self.presentationData.strings.Permissions_PeopleNearbyTitle_v0
self.navigationItem.rightBarButtonItem = nil
self.allow = { [weak self] in
if let strongSelf = self {
switch status {
case .requestable:
DeviceAccess.authorizeAccess(to: .location(.tracking), presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, { [weak self] result in
self?.proceed?(result)
})
case .denied, .unreachable:
strongSelf.openAppSettings()
strongSelf.proceed?(false)
default:
break
}
}
}
} }
self.skip = { [weak self] in self.skip = { [weak self] in

View File

@ -6,8 +6,23 @@ import SwiftSignalKit
import TelegramCore import TelegramCore
import TelegramPresentationData import TelegramPresentationData
public struct PermissionControllerCustomIcon: Equatable {
let light: UIImage?
let dark: UIImage?
init(light: UIImage?, dark: UIImage?) {
self.light = light
self.dark = dark
}
}
public enum PermissionControllerContent: Equatable {
case permission(PermissionState?)
case custom(icon: PermissionControllerCustomIcon, title: String, subtitle: String?, text: String, buttonTitle: String, footerText: String?)
}
private struct PermissionControllerDataState: Equatable { private struct PermissionControllerDataState: Equatable {
var state: PermissionState? var state: PermissionControllerContent?
} }
private struct PermissionControllerLayoutState: Equatable { private struct PermissionControllerLayoutState: Equatable {
@ -92,7 +107,7 @@ final class PermissionControllerNode: ASDisplayNode {
}) })
} }
public func setState(_ state: PermissionState, transition: ContainedViewLayoutTransition) { public func setState(_ state: PermissionControllerContent, transition: ContainedViewLayoutTransition) {
self.updateState({ currentState -> PermissionControllerInnerState in self.updateState({ currentState -> PermissionControllerInnerState in
return PermissionControllerInnerState(layout: currentState.layout, data: PermissionControllerDataState(state: state)) return PermissionControllerInnerState(layout: currentState.layout, data: PermissionControllerDataState(state: state))
}, transition: transition) }, transition: transition)
@ -112,107 +127,132 @@ final class PermissionControllerNode: ASDisplayNode {
let insets = state.layout.layout.insets(options: [.statusBar]) let insets = state.layout.layout.insets(options: [.statusBar])
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: state.layout.layout.size.width, height: state.layout.layout.size.height)) let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: state.layout.layout.size.width, height: state.layout.layout.size.height))
if state.data.state?.kind != self.contentNode?.kind { if let state = state.data.state {
if let dataState = state.data.state { switch state {
let icon: UIImage? case let .permission(permission):
let title: String if permission?.kind.rawValue != self.contentNode?.kind {
let text: String if let dataState = permission {
let buttonTitle: String let icon: UIImage?
let hasPrivacyPolicy: Bool let title: String
let text: String
switch dataState { let buttonTitle: String
case let .contacts(status): let hasPrivacyPolicy: Bool
icon = UIImage(bundleImageName: "Settings/Permissions/Contacts")
if let splitTest = self.splitTest, case let .modal(titleKey, textKey, allowTitleKey, allowInSettingsTitleKey) = splitTest.configuration.contacts { switch dataState {
title = localizedString(for: titleKey, strings: self.presentationData.strings) case let .contacts(status):
text = localizedString(for: textKey, strings: self.presentationData.strings) icon = UIImage(bundleImageName: "Settings/Permissions/Contacts")
if status == .denied { if let splitTest = self.splitTest, case let .modal(titleKey, textKey, allowTitleKey, allowInSettingsTitleKey) = splitTest.configuration.contacts {
buttonTitle = localizedString(for: allowInSettingsTitleKey, strings: self.presentationData.strings) title = localizedString(for: titleKey, strings: self.presentationData.strings)
} else { text = localizedString(for: textKey, strings: self.presentationData.strings)
buttonTitle = localizedString(for: allowTitleKey, strings: self.presentationData.strings) if status == .denied {
buttonTitle = localizedString(for: allowInSettingsTitleKey, strings: self.presentationData.strings)
} else {
buttonTitle = localizedString(for: allowTitleKey, strings: self.presentationData.strings)
}
} else {
title = self.presentationData.strings.Permissions_ContactsTitle_v0
text = self.presentationData.strings.Permissions_ContactsText_v0
if status == .denied {
buttonTitle = self.presentationData.strings.Permissions_ContactsAllowInSettings_v0
} else {
buttonTitle = self.presentationData.strings.Permissions_ContactsAllow_v0
}
}
hasPrivacyPolicy = true
case let .notifications(status):
icon = UIImage(bundleImageName: "Settings/Permissions/Notifications")
if let splitTest = self.splitTest, case let .modal(titleKey, textKey, allowTitleKey, allowInSettingsTitleKey) = splitTest.configuration.notifications {
title = localizedString(for: titleKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsTitle_v0)
text = localizedString(for: textKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsText_v0)
if status == .denied {
buttonTitle = localizedString(for: allowInSettingsTitleKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsAllowInSettings_v0)
} else {
buttonTitle = localizedString(for: allowTitleKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsAllow_v0)
}
} else {
title = self.presentationData.strings.Permissions_NotificationsTitle_v0
text = self.presentationData.strings.Permissions_NotificationsText_v0
if status == .denied {
buttonTitle = self.presentationData.strings.Permissions_NotificationsAllowInSettings_v0
} else {
buttonTitle = self.presentationData.strings.Permissions_NotificationsAllow_v0
}
}
hasPrivacyPolicy = false
case let .siri(status):
icon = UIImage(bundleImageName: "Settings/Permissions/Siri")
title = self.presentationData.strings.Permissions_SiriTitle_v0
text = self.presentationData.strings.Permissions_SiriText_v0
if status == .denied {
buttonTitle = self.presentationData.strings.Permissions_SiriAllowInSettings_v0
} else {
buttonTitle = self.presentationData.strings.Permissions_SiriAllow_v0
}
hasPrivacyPolicy = false
case .cellularData:
icon = UIImage(bundleImageName: "Settings/Permissions/CellularData")
title = self.presentationData.strings.Permissions_CellularDataTitle_v0
text = self.presentationData.strings.Permissions_CellularDataText_v0
buttonTitle = self.presentationData.strings.Permissions_CellularDataAllowInSettings_v0
hasPrivacyPolicy = false
case let .nearbyLocation(status):
icon = nil
title = self.presentationData.strings.Permissions_PeopleNearbyTitle_v0
text = self.presentationData.strings.Permissions_PeopleNearbyText_v0
if status == .denied {
buttonTitle = self.presentationData.strings.Permissions_PeopleNearbyAllowInSettings_v0
} else {
buttonTitle = self.presentationData.strings.Permissions_PeopleNearbyAllow_v0
}
hasPrivacyPolicy = false
} }
} else {
title = self.presentationData.strings.Permissions_ContactsTitle_v0 let contentNode = PermissionContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, kind: dataState.kind.rawValue, icon: icon, title: title, text: text, buttonTitle: buttonTitle, buttonAction: { [weak self] in
text = self.presentationData.strings.Permissions_ContactsText_v0 self?.allow?()
if status == .denied { }, openPrivacyPolicy: hasPrivacyPolicy ? self.openPrivacyPolicy : nil)
buttonTitle = self.presentationData.strings.Permissions_ContactsAllowInSettings_v0 self.insertSubnode(contentNode, at: 0)
} else { contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: .immediate)
buttonTitle = self.presentationData.strings.Permissions_ContactsAllow_v0 contentNode.frame = contentFrame
if let currentContentNode = self.contentNode {
transition.updatePosition(node: currentContentNode, position: CGPoint(x: -contentFrame.size.width / 2.0, y: contentFrame.midY), completion: { [weak currentContentNode] _ in
currentContentNode?.removeFromSupernode()
})
transition.animateHorizontalOffsetAdditive(node: contentNode, offset: -contentFrame.width)
} else if transition.isAnimated {
contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
} }
self.contentNode = contentNode
} else if let currentContentNode = self.contentNode {
transition.updateAlpha(node: currentContentNode, alpha: 0.0, completion: { [weak currentContentNode] _ in
currentContentNode?.removeFromSupernode()
})
self.contentNode = nil
} }
hasPrivacyPolicy = true } else if let contentNode = self.contentNode {
case let .notifications(status): transition.updateFrame(node: contentNode, frame: contentFrame)
icon = UIImage(bundleImageName: "Settings/Permissions/Notifications") contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: transition)
if let splitTest = self.splitTest, case let .modal(titleKey, textKey, allowTitleKey, allowInSettingsTitleKey) = splitTest.configuration.notifications { }
title = localizedString(for: titleKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsTitle_v0) case let .custom(icon, title, subtitle, text, buttonTitle, footerText):
text = localizedString(for: textKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsText_v0) if let contentNode = self.contentNode {
if status == .denied { transition.updateFrame(node: contentNode, frame: contentFrame)
buttonTitle = localizedString(for: allowInSettingsTitleKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsAllowInSettings_v0) contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: transition)
} else { } else {
buttonTitle = localizedString(for: allowTitleKey, strings: self.presentationData.strings, fallback: self.presentationData.strings.Permissions_NotificationsAllow_v0) let iconImage: UIImage?
} if self.presentationData.theme.overallDarkAppearance {
iconImage = icon.dark ?? icon.light
} else { } else {
title = self.presentationData.strings.Permissions_NotificationsTitle_v0 iconImage = icon.light
text = self.presentationData.strings.Permissions_NotificationsText_v0
if status == .denied {
buttonTitle = self.presentationData.strings.Permissions_NotificationsAllowInSettings_v0
} else {
buttonTitle = self.presentationData.strings.Permissions_NotificationsAllow_v0
}
} }
hasPrivacyPolicy = false
case let .siri(status): let contentNode = PermissionContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, kind: 0, icon: iconImage, title: title, subtitle: subtitle, text: text, buttonTitle: buttonTitle, footerText: footerText, buttonAction: { [weak self] in
icon = UIImage(bundleImageName: "Settings/Permissions/Siri") self?.allow?()
title = self.presentationData.strings.Permissions_SiriTitle_v0 }, openPrivacyPolicy: nil)
text = self.presentationData.strings.Permissions_SiriText_v0 self.insertSubnode(contentNode, at: 0)
if status == .denied { contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: .immediate)
buttonTitle = self.presentationData.strings.Permissions_SiriAllowInSettings_v0 contentNode.frame = contentFrame
} else { self.contentNode = contentNode
buttonTitle = self.presentationData.strings.Permissions_SiriAllow_v0 }
}
hasPrivacyPolicy = false
case .cellularData:
icon = UIImage(bundleImageName: "Settings/Permissions/CellularData")
title = self.presentationData.strings.Permissions_CellularDataTitle_v0
text = self.presentationData.strings.Permissions_CellularDataText_v0
buttonTitle = self.presentationData.strings.Permissions_CellularDataAllowInSettings_v0
hasPrivacyPolicy = false
case let .nearbyLocation(status):
icon = nil
title = self.presentationData.strings.Permissions_PeopleNearbyTitle_v0
text = self.presentationData.strings.Permissions_PeopleNearbyText_v0
if status == .denied {
buttonTitle = self.presentationData.strings.Permissions_PeopleNearbyAllowInSettings_v0
} else {
buttonTitle = self.presentationData.strings.Permissions_PeopleNearbyAllow_v0
}
hasPrivacyPolicy = false
}
let contentNode = PermissionContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, kind: dataState.kind, icon: icon, title: title, text: text, buttonTitle: buttonTitle, buttonAction: { [weak self] in
self?.allow?()
}, openPrivacyPolicy: hasPrivacyPolicy ? self.openPrivacyPolicy : nil)
self.insertSubnode(contentNode, at: 0)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: .immediate)
contentNode.frame = contentFrame
if let currentContentNode = self.contentNode {
transition.updatePosition(node: currentContentNode, position: CGPoint(x: -contentFrame.size.width / 2.0, y: contentFrame.midY), completion: { [weak currentContentNode] _ in
currentContentNode?.removeFromSupernode()
})
transition.animateHorizontalOffsetAdditive(node: contentNode, offset: -contentFrame.width)
} else if transition.isAnimated {
contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
self.contentNode = contentNode
} else if let currentContentNode = self.contentNode {
transition.updateAlpha(node: currentContentNode, alpha: 0.0, completion: { [weak currentContentNode] _ in
currentContentNode?.removeFromSupernode()
})
self.contentNode = nil
} }
} else if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: contentFrame)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: transition)
} }
} }

View File

@ -110,7 +110,7 @@ struct PresentationResourcesItemList {
static func createGroupIcon(_ theme: PresentationTheme) -> UIImage? { static func createGroupIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListCreateGroupIcon.rawValue, { theme in return theme.image(PresentationResourceKey.itemListCreateGroupIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Contact List/CreateGroupActionIcon"), color: theme.list.itemAccentColor) return generateTintedImage(image: UIImage(bundleImageName: "Location/CreateGroupIcon"), color: theme.list.itemAccentColor)
}) })
} }

View File

@ -1,7 +0,0 @@
#import <Foundation/Foundation.h>
@interface TGEmojiSuggestions : NSObject
+ (NSArray *)suggestionsForQuery:(NSString *)query;
@end

View File

@ -1,49 +0,0 @@
#import "TGEmojiSuggestions.h"
#import "emoji_suggestions.h"
#import <LegacyComponents/TGAlphacode.h>
std::vector<Ui::Emoji::utf16char> convertToUtf16(NSString *string) {
auto cf = (__bridge CFStringRef)string;
auto range = CFRangeMake(0, CFStringGetLength(cf));
auto bufferLength = CFIndex(0);
CFStringGetBytes(cf, range, kCFStringEncodingUTF16LE, 0, FALSE, nullptr, 0, &bufferLength);
if (!bufferLength) {
return std::vector<Ui::Emoji::utf16char>();
}
auto result = std::vector<Ui::Emoji::utf16char>(bufferLength / 2 + 1, 0);
CFStringGetBytes(cf, range, kCFStringEncodingUTF16LE, 0, FALSE, reinterpret_cast<UInt8*>(result.data()), result.size() * 2, &bufferLength);
result.resize(bufferLength / 2);
return result;
}
NSString *convertFromUtf16(Ui::Emoji::utf16string string) {
auto result = CFStringCreateWithBytes(nullptr, reinterpret_cast<const UInt8*>(string.data()), string.size() * 2, kCFStringEncodingUTF16LE, false);
return (__bridge NSString*)result;
}
void test() {
}
@implementation TGEmojiSuggestions
+ (NSArray *)suggestionsForQuery:(NSString *)queryText {
auto query = convertToUtf16(queryText);
auto values = Ui::Emoji::GetSuggestions(Ui::Emoji::utf16string(query.data(), query.size()));
NSMutableArray *array = [[NSMutableArray alloc] init];
for (auto &item : values) {
NSString *emoji = convertFromUtf16(item.emoji());
NSString *label = convertFromUtf16(item.label());
NSString *replacement = convertFromUtf16(item.replacement());
[array addObject:[[TGAlphacodeEntry alloc] initWithEmoji:emoji code:replacement]];
}
return array;
}
@end

View File

@ -12,7 +12,6 @@ module TelegramUIPrivateModule {
header "../DeviceProximityManager.h" header "../DeviceProximityManager.h"
header "../RaiseToListenActivator.h" header "../RaiseToListenActivator.h"
header "../TGMimeTypeMap.h" header "../TGMimeTypeMap.h"
header "../TGEmojiSuggestions.h"
header "../TGChannelIntroController.h" header "../TGChannelIntroController.h"
header "../Bridge Audio/TGBridgeAudioDecoder.h" header "../Bridge Audio/TGBridgeAudioDecoder.h"
header "../Bridge Audio/TGBridgeAudioEncoder.h" header "../Bridge Audio/TGBridgeAudioEncoder.h"

View File

@ -0,0 +1,325 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor(theme.list.itemBlocksBackgroundColor.cgColor)
context.fill(bounds)
context.setBlendMode(.clear)
context.fillEllipse(in: bounds)
context.setBlendMode(.normal)
let lineWidth: CGFloat
if selected {
var accentColor = theme.list.itemAccentColor
if accentColor.rgb == UIColor.white.rgb {
accentColor = UIColor(rgb: 0x999999)
}
context.setStrokeColor(accentColor.cgColor)
lineWidth = 2.0
} else {
context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor)
lineWidth = 1.0
}
if bordered || selected {
context.setLineWidth(lineWidth)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
}
})?.stretchableImage(withLeftCapWidth: 15, topCapHeight: 15)
}
class ThemeSettingsAccentColorItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId
let theme: PresentationTheme
let strings: PresentationStrings
let colors: [UIColor]
let currentColor: UIColor
let updated: (UIColor) -> Void
let tag: ItemListItemTag?
init(theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, colors: [UIColor], currentColor: UIColor, updated: @escaping (UIColor) -> Void, tag: ItemListItemTag? = nil) {
self.theme = theme
self.strings = strings
self.colors = colors
self.currentColor = currentColor
self.updated = updated
self.tag = tag
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ThemeSettingsAccentColorItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ThemeSettingsAccentColorItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private final class ThemeSettingsAccentColorNode : ASDisplayNode {
private let iconNode: ASImageNode
private let overlayNode: ASImageNode
private let textNode: ASTextNode
private var action: (() -> Void)?
override init() {
self.iconNode = ASImageNode()
self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0))
self.iconNode.isLayerBacked = true
self.overlayNode = ASImageNode()
self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0))
self.overlayNode.isLayerBacked = true
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = true
super.init()
self.addSubnode(self.iconNode)
self.addSubnode(self.overlayNode)
self.addSubnode(self.textNode)
}
func setup(theme: PresentationTheme, icon: UIImage, title: NSAttributedString, bordered: Bool, selected: Bool, action: @escaping () -> Void) {
self.iconNode.image = icon
self.textNode.attributedText = title
self.overlayNode.image = generateBorderImage(theme: theme, bordered: bordered, selected: selected)
self.action = {
action()
}
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.action?()
}
}
override func layout() {
super.layout()
let bounds = self.bounds
self.iconNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 14.0), size: CGSize(width: 62.0, height: 62.0))
self.overlayNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 14.0), size: CGSize(width: 62.0, height: 62.0))
self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 14.0 + 60.0 + 4.0 + 9.0), size: CGSize(width: bounds.size.width, height: 16.0))
}
}
private let textFont = Font.regular(11.0)
private let itemSize = Font.regular(11.0)
class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let scrollNode: ASScrollNode
private var nodes: [ThemeSettingsAccentColorNode] = []
private var item: ThemeSettingsAccentColorItem?
private var layoutParams: ListViewItemLayoutParams?
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.scrollNode = ASScrollNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.scrollNode)
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.showsHorizontalScrollIndicator = false
}
func asyncLayout() -> (_ item: ThemeSettingsAccentColorItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
contentSize = CGSize(width: params.width, height: 116.0)
insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.scrollNode.view.contentInset = UIEdgeInsetsMake(0.0, params.leftInset, 0.0, params.rightInset)
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
strongSelf.topStripeNode.isHidden = false
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: CGSize(width: layoutSize.width, height: layoutSize.height))
let nodeInset: CGFloat = 4.0
let nodeSize = CGSize(width: 80.0, height: 112.0)
var nodeOffset = nodeInset
var i = 0
for icon in item.colors {
let imageNode: ThemeSettingsAccentColorNode
if strongSelf.nodes.count > i {
imageNode = strongSelf.nodes[i]
} else {
imageNode = ThemeSettingsAccentColorNode()
strongSelf.nodes.append(imageNode)
strongSelf.scrollNode.addSubnode(imageNode)
}
// if let image = UIImage(named: icon.imageName, in: Bundle.main, compatibleWith: nil) {
// let selected = icon.name == item.currentIconName
//
// var name = "Icon"
// var bordered = true
// switch icon.name {
// case "Blue":
// name = item.strings.Appearance_AppIconDefault
// case "Black":
// name = item.strings.Appearance_AppIconDefaultX
// case "BlueClassic":
// name = item.strings.Appearance_AppIconClassic
// case "BlackClassic":
// name = item.strings.Appearance_AppIconClassicX
// case "BlueFilled":
// name = item.strings.Appearance_AppIconFilled
// bordered = false
// case "BlackFilled":
// name = item.strings.Appearance_AppIconFilledX
// bordered = false
// case "WhiteFilled":
// name = " White"
// default:
// break
// }
//
// imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), bordered: bordered, selected: selected, action: {
// item.updated(icon.name)
// })
// }
imageNode.frame = CGRect(origin: CGPoint(x: nodeOffset, y: 0.0), size: nodeSize)
nodeOffset += nodeSize.width + 15.0
i += 1
}
if let lastNode = strongSelf.nodes.last {
let contentSize = CGSize(width: lastNode.frame.maxX + nodeInset, height: strongSelf.scrollNode.frame.height)
if strongSelf.scrollNode.view.contentSize != contentSize {
strongSelf.scrollNode.view.contentSize = contentSize
}
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -294,6 +294,8 @@ private func themeSettingsControllerEntries(presentationData: PresentationData,
return entries return entries
} }
private let themeColors = [UIColor(rgb: 0x007aff), UIColor(rgb: 0x70bb23), UIColor(rgb: 0xeb6ca4), UIColor(rgb: 0xf08200), UIColor(rgb: 0x9472ee), UIColor(rgb: 0xd33213), UIColor(rgb: 0xedb400), UIColor(rgb: 0x6d839e), UIColor(rgb: 0x000000)]
public func themeSettingsController(context: AccountContext, focusOnItemTag: ThemeSettingsEntryTag? = nil) -> ViewController { public func themeSettingsController(context: AccountContext, focusOnItemTag: ThemeSettingsEntryTag? = nil) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController) -> Void)?

View File

@ -1,432 +0,0 @@
/*
This file is part of Telegram Desktop,
the official desktop version of Telegram messaging app, see https://telegram.org
Telegram Desktop is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
It is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
In addition, as a special exception, the copyright holders give permission
to link the code of portions of this program with the OpenSSL library.
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
*/
#include "emoji_suggestions.h"
#include <algorithm>
#include "emoji_suggestions_data.h"
#ifndef Expects
#include <cassert>
#define Expects(condition) assert(condition)
#endif // Expects
namespace Ui {
namespace Emoji {
namespace internal {
namespace {
checksum Crc32Table[256];
class Crc32Initializer {
public:
Crc32Initializer() {
checksum poly = 0x04C11DB7U;
for (auto i = 0; i != 256; ++i) {
Crc32Table[i] = reflect(i, 8) << 24;
for (auto j = 0; j != 8; ++j) {
Crc32Table[i] = (Crc32Table[i] << 1) ^ (Crc32Table[i] & (1 << 31) ? poly : 0);
}
Crc32Table[i] = reflect(Crc32Table[i], 32);
}
}
private:
checksum reflect(checksum val, char ch) {
checksum result = 0;
for (int i = 1; i < (ch + 1); ++i) {
if (val & 1) {
result |= 1 << (ch - i);
}
val >>= 1;
}
return result;
}
};
} // namespace
checksum countChecksum(const void *data, std::size_t size) {
static Crc32Initializer InitTable;
auto buffer = static_cast<const unsigned char*>(data);
auto result = checksum(0xFFFFFFFFU);
for (auto i = std::size_t(0); i != size; ++i) {
result = (result >> 8) ^ Crc32Table[(result & 0xFFU) ^ buffer[i]];
}
return (result ^ 0xFFFFFFFFU);
}
} // namespace internal
namespace {
class string_span {
public:
string_span() = default;
string_span(const utf16string *data, std::size_t size) : begin_(data), size_(size) {
}
string_span(const std::vector<utf16string> &data) : begin_(data.data()), size_(data.size()) {
}
string_span(const string_span &other) = default;
string_span &operator=(const string_span &other) = default;
const utf16string *begin() const {
return begin_;
}
const utf16string *end() const {
return begin_ + size_;
}
std::size_t size() const {
return size_;
}
string_span subspan(std::size_t offset, std::size_t size) {
return string_span(begin_ + offset, size);
}
private:
const utf16string *begin_ = nullptr;
std::size_t size_ = 0;
};
bool IsNumber(utf16char ch) {
return (ch >= '0' && ch <= '9');
}
bool IsLetterOrNumber(utf16char ch) {
return (ch >= 'a' && ch <= 'z') || IsNumber(ch);
}
using Replacement = internal::Replacement;
class Completer {
public:
Completer(utf16string query);
std::vector<Suggestion> resolve();
private:
struct Result {
const Replacement *replacement;
int wordsUsed;
};
static std::vector<utf16char> NormalizeQuery(utf16string query);
void addResult(const Replacement *replacement);
bool isDuplicateOfLastResult(const Replacement *replacement) const;
bool isBetterThanLastResult(const Replacement *replacement) const;
void processInitialList();
void filterInitialList();
void initWordsTracking();
bool matchQueryForCurrentItem();
bool matchQueryTailStartingFrom(int position);
string_span findWordsStartingWith(utf16char ch);
int findEqualCharsCount(int position, const utf16string *word);
std::vector<Suggestion> prepareResult();
bool startsWithQuery(utf16string word);
bool isExactMatch(utf16string replacement);
std::vector<Result> _result;
utf16string _initialQuery;
const std::vector<utf16char> _query;
const utf16char *_queryBegin = nullptr;
int _querySize = 0;
const std::vector<const Replacement*> *_initialList = nullptr;
string_span _currentItemWords;
int _currentItemWordsUsedCount = 0;
class UsedWordGuard {
public:
UsedWordGuard(std::vector<small> &map, int index);
UsedWordGuard(const UsedWordGuard &other) = delete;
UsedWordGuard(UsedWordGuard &&other);
UsedWordGuard &operator=(const UsedWordGuard &other) = delete;
UsedWordGuard &operator=(UsedWordGuard &&other) = delete;
explicit operator bool() const;
~UsedWordGuard();
private:
std::vector<small> &_map;
int _index = 0;
bool _guarded = false;
};
std::vector<small> _currentItemWordsUsedMap;
};
Completer::UsedWordGuard::UsedWordGuard(std::vector<small> &map, int index) : _map(map), _index(index) {
Expects(_map.size() > _index);
if (!_map[_index]) {
_guarded = _map[_index] = 1;
}
}
Completer::UsedWordGuard::UsedWordGuard(UsedWordGuard &&other) : _map(other._map), _index(other._index), _guarded(other._guarded) {
other._guarded = 0;
}
Completer::UsedWordGuard::operator bool() const {
return _guarded;
}
Completer::UsedWordGuard::~UsedWordGuard() {
if (_guarded) {
_map[_index] = 0;
}
}
Completer::Completer(utf16string query) : _initialQuery(query), _query(NormalizeQuery(query)) {
}
// Remove all non-letters-or-numbers.
// Leave '-' and '+' only if they're followed by a number or
// at the end of the query (so it is possibly followed by a number).
std::vector<utf16char> Completer::NormalizeQuery(utf16string query) {
auto result = std::vector<utf16char>();
result.reserve(query.size());
auto copyFrom = query.data();
auto e = copyFrom + query.size();
auto copyTo = result.data();
for (auto i = query.data(); i != e; ++i) {
if (IsLetterOrNumber(*i)) {
continue;
} else if (*i == '-' || *i == '+') {
if (i + 1 == e || IsNumber(*(i + 1))) {
continue;
}
}
if (i > copyFrom) {
result.resize(result.size() + (i - copyFrom));
memcpy(copyTo, copyFrom, (i - copyFrom) * sizeof(utf16char));
copyTo += (i - copyFrom);
}
copyFrom = i + 1;
}
if (e > copyFrom) {
result.resize(result.size() + (e - copyFrom));
memcpy(copyTo, copyFrom, (e - copyFrom) * sizeof(utf16char));
copyTo += (e - copyFrom);
}
return result;
}
std::vector<Suggestion> Completer::resolve() {
_queryBegin = _query.data();
_querySize = _query.size();
if (!_querySize) {
return std::vector<Suggestion>();
}
_initialList = Ui::Emoji::internal::GetReplacements(*_queryBegin);
if (!_initialList) {
return std::vector<Suggestion>();
}
_result.reserve(_initialList->size());
processInitialList();
return prepareResult();
}
bool Completer::isDuplicateOfLastResult(const Replacement *item) const {
if (_result.empty()) {
return false;
}
return (_result.back().replacement->emoji == item->emoji);
}
bool Completer::isBetterThanLastResult(const Replacement *item) const {
Expects(!_result.empty());
auto &last = _result.back();
if (_currentItemWordsUsedCount < last.wordsUsed) {
return true;
}
auto firstCharOfQuery = _query[0];
auto firstCharAfterColonLast = last.replacement->replacement[1];
auto firstCharAfterColonCurrent = item->replacement[1];
auto goodLast = (firstCharAfterColonLast == firstCharOfQuery);
auto goodCurrent = (firstCharAfterColonCurrent == firstCharOfQuery);
return !goodLast && goodCurrent;
}
void Completer::addResult(const Replacement *item) {
if (!isDuplicateOfLastResult(item)) {
_result.push_back({ item, _currentItemWordsUsedCount });
} else if (isBetterThanLastResult(item)) {
_result.back() = { item, _currentItemWordsUsedCount };
}
}
void Completer::processInitialList() {
if (_querySize > 1) {
filterInitialList();
} else {
_currentItemWordsUsedCount = 1;
for (auto item : *_initialList) {
addResult(item);
}
}
}
void Completer::initWordsTracking() {
auto maxWordsCount = 0;
for (auto item : *_initialList) {
auto wordsCount = item->words.size();
if (maxWordsCount < wordsCount) {
maxWordsCount = wordsCount;
}
}
_currentItemWordsUsedMap = std::vector<small>(maxWordsCount, 0);
}
void Completer::filterInitialList() {
initWordsTracking();
for (auto item : *_initialList) {
_currentItemWords = string_span(item->words);
_currentItemWordsUsedCount = 1;
if (matchQueryForCurrentItem()) {
addResult(item);
}
_currentItemWordsUsedCount = 0;
}
}
bool Completer::matchQueryForCurrentItem() {
Expects(_currentItemWords.size() != 0);
if (_currentItemWords.size() < 2) {
return startsWithQuery(*_currentItemWords.begin());
}
return matchQueryTailStartingFrom(0);
}
bool Completer::startsWithQuery(utf16string word) {
if (word.size() < _query.size()) {
return false;
}
for (auto i = std::size_t(0), size = _query.size(); i != size; ++i) {
if (word[i] != _query[i]) {
return false;
}
}
return true;
}
bool Completer::isExactMatch(utf16string replacement) {
if (replacement.size() != _initialQuery.size() + 1) {
return false;
}
for (auto i = std::size_t(0), size = _initialQuery.size(); i != size; ++i) {
if (replacement[i] != _initialQuery[i]) {
return false;
}
}
return true;
}
bool Completer::matchQueryTailStartingFrom(int position) {
auto charsLeftToMatch = (_querySize - position);
if (!charsLeftToMatch) {
return true;
}
auto firstCharToMatch = *(_queryBegin + position);
auto foundWords = findWordsStartingWith(firstCharToMatch);
for (auto word = foundWords.begin(), foundWordsEnd = word + foundWords.size(); word != foundWordsEnd; ++word) {
auto wordIndex = word - _currentItemWords.begin();
if (auto guard = UsedWordGuard(_currentItemWordsUsedMap, wordIndex)) {
++_currentItemWordsUsedCount;
auto equalCharsCount = findEqualCharsCount(position, word);
for (auto check = equalCharsCount; check != 0; --check) {
if (matchQueryTailStartingFrom(position + check)) {
return true;
}
}
--_currentItemWordsUsedCount;
}
}
return false;
}
int Completer::findEqualCharsCount(int position, const utf16string *word) {
auto charsLeft = (_querySize - position);
auto wordBegin = word->data();
auto wordSize = word->size();
auto possibleEqualCharsCount = (charsLeft > wordSize ? wordSize : charsLeft);
for (auto equalTill = 1; equalTill != possibleEqualCharsCount; ++equalTill) {
auto wordCh = *(wordBegin + equalTill);
auto queryCh = *(_queryBegin + position + equalTill);
if (wordCh != queryCh) {
return equalTill;
}
}
return possibleEqualCharsCount;
}
std::vector<Suggestion> Completer::prepareResult() {
auto firstCharOfQuery = _query[0];
std::stable_partition(_result.begin(), _result.end(), [firstCharOfQuery](Result &result) {
auto firstCharAfterColon = result.replacement->replacement[1];
return (firstCharAfterColon == firstCharOfQuery);
});
std::stable_partition(_result.begin(), _result.end(), [](Result &result) {
return (result.wordsUsed < 2);
});
std::stable_partition(_result.begin(), _result.end(), [](Result &result) {
return (result.wordsUsed < 3);
});
std::stable_partition(_result.begin(), _result.end(), [this](Result &result) {
return isExactMatch(result.replacement->replacement);
});
auto result = std::vector<Suggestion>();
result.reserve(_result.size());
for (auto &item : _result) {
result.emplace_back(item.replacement->emoji, item.replacement->replacement, item.replacement->replacement);
}
return result;
}
string_span Completer::findWordsStartingWith(utf16char ch) {
auto begin = std::lower_bound(_currentItemWords.begin(), _currentItemWords.end(), ch, [](utf16string word, utf16char ch) {
return word[0] < ch;
});
auto end = std::upper_bound(_currentItemWords.begin(), _currentItemWords.end(), ch, [](utf16char ch, utf16string word) {
return ch < word[0];
});
return _currentItemWords.subspan(begin - _currentItemWords.begin(), end - begin);
}
} // namespace
std::vector<Suggestion> GetSuggestions(utf16string query) {
return Completer(query).resolve();
}
int GetSuggestionMaxLength() {
return internal::kReplacementMaxLength;
}
} // namespace Emoji
} // namespace Ui

View File

@ -1,107 +0,0 @@
/*
This file is part of Telegram Desktop,
the official desktop version of Telegram messaging app, see https://telegram.org
Telegram Desktop is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
It is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
In addition, as a special exception, the copyright holders give permission
to link the code of portions of this program with the OpenSSL library.
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
*/
#pragma once
#include <vector>
namespace Ui {
namespace Emoji {
using small = unsigned char;
using medium = unsigned short;
using utf16char = unsigned short;
static_assert(sizeof(utf16char) == 2, "Bad UTF-16 character size.");
class utf16string {
public:
utf16string() = default;
utf16string(const utf16char *data, std::size_t size) : data_(data), size_(size) {
}
utf16string(const utf16string &other) = default;
utf16string &operator=(const utf16string &other) = default;
const utf16char *data() const {
return data_;
}
std::size_t size() const {
return size_;
}
utf16char operator[](int index) const {
return data_[index];
}
private:
const utf16char *data_ = nullptr;
std::size_t size_ = 0;
};
inline bool operator==(utf16string a, utf16string b) {
return (a.size() == b.size()) && (!a.size() || !memcmp(a.data(), b.data(), a.size() * sizeof(utf16char)));
}
namespace internal {
using checksum = unsigned int;
checksum countChecksum(const void *data, std::size_t size);
utf16string GetReplacementEmoji(utf16string replacement);
} // namespace internal
class Suggestion {
public:
Suggestion() = default;
Suggestion(utf16string emoji, utf16string label, utf16string replacement) : emoji_(emoji), label_(label), replacement_(replacement) {
}
Suggestion(const Suggestion &other) = default;
Suggestion &operator=(const Suggestion &other) = default;
utf16string emoji() const {
return emoji_;
}
utf16string label() const {
return label_;
}
utf16string replacement() const {
return replacement_;
}
private:
utf16string emoji_;
utf16string label_;
utf16string replacement_;
};
std::vector<Suggestion> GetSuggestions(utf16string query);
inline utf16string GetSuggestionEmoji(utf16string replacement) {
return internal::GetReplacementEmoji(replacement);
}
int GetSuggestionMaxLength();
} // namespace Emoji
} // namespace Ui

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +0,0 @@
/*
WARNING! All changes made in this file will be lost!
Created from 'empty' by 'codegen_emoji'
This file is part of Telegram Desktop,
the official desktop version of Telegram messaging app, see https://telegram.org
Telegram Desktop is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
It is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
In addition, as a special exception, the copyright holders give permission
to link the code of portions of this program with the OpenSSL library.
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
*/
#pragma once
#include "emoji_suggestions.h"
namespace Ui {
namespace Emoji {
namespace internal {
struct Replacement {
utf16string emoji;
utf16string replacement;
std::vector<utf16string> words;
};
constexpr auto kReplacementMaxLength = 55;
void InitReplacements();
const std::vector<const Replacement*> *GetReplacements(utf16char first);
utf16string GetReplacementEmoji(utf16string replacement);
} // namespace internal
} // namespace Emoji
} // namespace Ui

View File

@ -93,6 +93,7 @@
09874E5721078FA100E190B8 /* YoutubeUserScript.js in Resources */ = {isa = PBXBuildFile; fileRef = 0979788121065F8B0077D77F /* YoutubeUserScript.js */; }; 09874E5721078FA100E190B8 /* YoutubeUserScript.js in Resources */ = {isa = PBXBuildFile; fileRef = 0979788121065F8B0077D77F /* YoutubeUserScript.js */; };
09874E582107A4C300E190B8 /* VimeoEmbedImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09874E3A21075BF400E190B8 /* VimeoEmbedImplementation.swift */; }; 09874E582107A4C300E190B8 /* VimeoEmbedImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09874E3A21075BF400E190B8 /* VimeoEmbedImplementation.swift */; };
09874E592107BD4100E190B8 /* GenericEmbedImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09874E4021075C1700E190B8 /* GenericEmbedImplementation.swift */; }; 09874E592107BD4100E190B8 /* GenericEmbedImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09874E4021075C1700E190B8 /* GenericEmbedImplementation.swift */; };
098CF79222B924E200AF6134 /* ThemeSettingsAccentColorItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 098CF79122B924E200AF6134 /* ThemeSettingsAccentColorItem.swift */; };
099529AA21CDB27900805E13 /* ShareProxyServerActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099529A921CDB27900805E13 /* ShareProxyServerActionSheetController.swift */; }; 099529AA21CDB27900805E13 /* ShareProxyServerActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099529A921CDB27900805E13 /* ShareProxyServerActionSheetController.swift */; };
099529AC21CDBBB200805E13 /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099529AB21CDBBB200805E13 /* QRCode.swift */; }; 099529AC21CDBBB200805E13 /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099529AB21CDBBB200805E13 /* QRCode.swift */; };
099529B021D2123E00805E13 /* ChatMessageUnsupportedBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099529AF21D2123E00805E13 /* ChatMessageUnsupportedBubbleContentNode.swift */; }; 099529B021D2123E00805E13 /* ChatMessageUnsupportedBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099529AF21D2123E00805E13 /* ChatMessageUnsupportedBubbleContentNode.swift */; };
@ -324,12 +325,6 @@
D0380DAD204ED434000414AB /* LegacyLiveUploadInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */; }; D0380DAD204ED434000414AB /* LegacyLiveUploadInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */; };
D0380DB8204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DB7204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift */; }; D0380DB8204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DB7204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift */; };
D0383ED4207CFBB900C45548 /* GalleryThumbnailContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383ED3207CFBB900C45548 /* GalleryThumbnailContainerNode.swift */; }; D0383ED4207CFBB900C45548 /* GalleryThumbnailContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383ED3207CFBB900C45548 /* GalleryThumbnailContainerNode.swift */; };
D0383EDC207D1A1600C45548 /* emoji_suggestions_data.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383ED6207D1A1500C45548 /* emoji_suggestions_data.h */; };
D0383EDD207D1A1600C45548 /* TGEmojiSuggestions.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383ED7207D1A1500C45548 /* TGEmojiSuggestions.h */; };
D0383EDE207D1A1600C45548 /* emoji_suggestions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0383ED8207D1A1600C45548 /* emoji_suggestions.cpp */; };
D0383EDF207D1A1600C45548 /* emoji_suggestions_data.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0383ED9207D1A1600C45548 /* emoji_suggestions_data.cpp */; };
D0383EE0207D1A1600C45548 /* TGEmojiSuggestions.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0383EDA207D1A1600C45548 /* TGEmojiSuggestions.mm */; };
D0383EE1207D1A1600C45548 /* emoji_suggestions.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383EDB207D1A1600C45548 /* emoji_suggestions.h */; };
D0383EE4207D292800C45548 /* EmojisChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */; }; D0383EE4207D292800C45548 /* EmojisChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */; };
D0383EE6207D299600C45548 /* EmojisChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.swift */; }; D0383EE6207D299600C45548 /* EmojisChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.swift */; };
D039FB152170D99D00BD1BAD /* RadialCloudProgressContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */; }; D039FB152170D99D00BD1BAD /* RadialCloudProgressContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039FB142170D99D00BD1BAD /* RadialCloudProgressContentNode.swift */; };
@ -1265,6 +1260,7 @@
0979788821065F8C0077D77F /* GenericUserScript.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = GenericUserScript.js; sourceTree = "<group>"; }; 0979788821065F8C0077D77F /* GenericUserScript.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = GenericUserScript.js; sourceTree = "<group>"; };
09874E3A21075BF400E190B8 /* VimeoEmbedImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VimeoEmbedImplementation.swift; sourceTree = "<group>"; }; 09874E3A21075BF400E190B8 /* VimeoEmbedImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VimeoEmbedImplementation.swift; sourceTree = "<group>"; };
09874E4021075C1700E190B8 /* GenericEmbedImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericEmbedImplementation.swift; sourceTree = "<group>"; }; 09874E4021075C1700E190B8 /* GenericEmbedImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericEmbedImplementation.swift; sourceTree = "<group>"; };
098CF79122B924E200AF6134 /* ThemeSettingsAccentColorItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingsAccentColorItem.swift; sourceTree = "<group>"; };
099529A921CDB27900805E13 /* ShareProxyServerActionSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareProxyServerActionSheetController.swift; sourceTree = "<group>"; }; 099529A921CDB27900805E13 /* ShareProxyServerActionSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareProxyServerActionSheetController.swift; sourceTree = "<group>"; };
099529AB21CDBBB200805E13 /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = "<group>"; }; 099529AB21CDBBB200805E13 /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = "<group>"; };
099529AF21D2123E00805E13 /* ChatMessageUnsupportedBubbleContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageUnsupportedBubbleContentNode.swift; sourceTree = "<group>"; }; 099529AF21D2123E00805E13 /* ChatMessageUnsupportedBubbleContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageUnsupportedBubbleContentNode.swift; sourceTree = "<group>"; };
@ -1588,12 +1584,6 @@
D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyLiveUploadInterface.swift; sourceTree = "<group>"; }; D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyLiveUploadInterface.swift; sourceTree = "<group>"; };
D0380DB7204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInstantVideoMessageDurationNode.swift; sourceTree = "<group>"; }; D0380DB7204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInstantVideoMessageDurationNode.swift; sourceTree = "<group>"; };
D0383ED3207CFBB900C45548 /* GalleryThumbnailContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryThumbnailContainerNode.swift; sourceTree = "<group>"; }; D0383ED3207CFBB900C45548 /* GalleryThumbnailContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryThumbnailContainerNode.swift; sourceTree = "<group>"; };
D0383ED6207D1A1500C45548 /* emoji_suggestions_data.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = emoji_suggestions_data.h; sourceTree = "<group>"; };
D0383ED7207D1A1500C45548 /* TGEmojiSuggestions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmojiSuggestions.h; sourceTree = "<group>"; };
D0383ED8207D1A1600C45548 /* emoji_suggestions.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = emoji_suggestions.cpp; sourceTree = "<group>"; };
D0383ED9207D1A1600C45548 /* emoji_suggestions_data.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = emoji_suggestions_data.cpp; sourceTree = "<group>"; };
D0383EDA207D1A1600C45548 /* TGEmojiSuggestions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TGEmojiSuggestions.mm; sourceTree = "<group>"; };
D0383EDB207D1A1600C45548 /* emoji_suggestions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = emoji_suggestions.h; sourceTree = "<group>"; };
D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojisChatInputContextPanelNode.swift; sourceTree = "<group>"; }; D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojisChatInputContextPanelNode.swift; sourceTree = "<group>"; };
D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojisChatInputPanelItem.swift; sourceTree = "<group>"; }; D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojisChatInputPanelItem.swift; sourceTree = "<group>"; };
D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = "<group>"; }; D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = "<group>"; };
@ -3007,19 +2997,6 @@
name = "Plaintext Fields"; name = "Plaintext Fields";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D0383ED5207D19BC00C45548 /* Emoji */ = {
isa = PBXGroup;
children = (
D0383ED9207D1A1600C45548 /* emoji_suggestions_data.cpp */,
D0383ED6207D1A1500C45548 /* emoji_suggestions_data.h */,
D0383ED8207D1A1600C45548 /* emoji_suggestions.cpp */,
D0383EDB207D1A1600C45548 /* emoji_suggestions.h */,
D0383ED7207D1A1500C45548 /* TGEmojiSuggestions.h */,
D0383EDA207D1A1600C45548 /* TGEmojiSuggestions.mm */,
);
name = Emoji;
sourceTree = "<group>";
};
D0383EE2207D291100C45548 /* Emojis */ = { D0383EE2207D291100C45548 /* Emojis */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -3228,6 +3205,7 @@
D0B37C5F1F8D286E004252DF /* ThemeSettingsFontSizeItem.swift */, D0B37C5F1F8D286E004252DF /* ThemeSettingsFontSizeItem.swift */,
D06F31E52135A41C001A0F12 /* ThemeSettingsBrightnessItem.swift */, D06F31E52135A41C001A0F12 /* ThemeSettingsBrightnessItem.swift */,
09A218F422A15F1400DE6898 /* ThemeSettingsAppIconItem.swift */, 09A218F422A15F1400DE6898 /* ThemeSettingsAppIconItem.swift */,
098CF79122B924E200AF6134 /* ThemeSettingsAccentColorItem.swift */,
090E777822A6A32E00CD99F5 /* ThemeSettingsThemeItem.swift */, 090E777822A6A32E00CD99F5 /* ThemeSettingsThemeItem.swift */,
D06E4C322134A59700088087 /* ThemeAccentColorActionSheet.swift */, D06E4C322134A59700088087 /* ThemeAccentColorActionSheet.swift */,
D06E4C342134AE3C00088087 /* ThemeAutoNightSettingsController.swift */, D06E4C342134AE3C00088087 /* ThemeAutoNightSettingsController.swift */,
@ -4728,7 +4706,6 @@
09E4A7FE223ADFD80038140F /* Data */, 09E4A7FE223ADFD80038140F /* Data */,
D0B69C3A20EBD8B3003632C7 /* Device */, D0B69C3A20EBD8B3003632C7 /* Device */,
D01C7EFE1EF9D434008305F1 /* Device Contacts */, D01C7EFE1EF9D434008305F1 /* Device Contacts */,
D0383ED5207D19BC00C45548 /* Emoji */,
D025A4241F79428300563950 /* Fetch Manager */, D025A4241F79428300563950 /* Fetch Manager */,
09E4A7FC223ADF300038140F /* Images */, 09E4A7FC223ADF300038140F /* Images */,
D04614352005093B00EC0EF2 /* Location */, D04614352005093B00EC0EF2 /* Location */,
@ -4882,7 +4859,6 @@
D0208AD51FA33D14001F0D5F /* RaiseToListenActivator.h in Headers */, D0208AD51FA33D14001F0D5F /* RaiseToListenActivator.h in Headers */,
D00817DA22B47A14008A895F /* TGPresentationAutoNightPreferences.h in Headers */, D00817DA22B47A14008A895F /* TGPresentationAutoNightPreferences.h in Headers */,
D0E9BAE31F0574D800F079A4 /* STPBankAccountParams.h in Headers */, D0E9BAE31F0574D800F079A4 /* STPBankAccountParams.h in Headers */,
D0383EE1207D1A1600C45548 /* emoji_suggestions.h in Headers */,
D0E9BA361F05585000F079A4 /* STPPhoneNumberValidator.h in Headers */, D0E9BA361F05585000F079A4 /* STPPhoneNumberValidator.h in Headers */,
D0E9BA511F0559DA00F079A4 /* STPImageLibrary.h in Headers */, D0E9BA511F0559DA00F079A4 /* STPImageLibrary.h in Headers */,
D0E9BA4C1F0559C700F079A4 /* NSString+Stripe_CardBrands.h in Headers */, D0E9BA4C1F0559C700F079A4 /* NSString+Stripe_CardBrands.h in Headers */,
@ -4892,7 +4868,6 @@
D0E9BA2A1F0557A600F079A4 /* STPFormEncoder.h in Headers */, D0E9BA2A1F0557A600F079A4 /* STPFormEncoder.h in Headers */,
D0E9BA321F05583A00F079A4 /* STPPostalCodeValidator.h in Headers */, D0E9BA321F05583A00F079A4 /* STPPostalCodeValidator.h in Headers */,
D0E9BADC1F0574D800F079A4 /* PKPayment+Stripe.h in Headers */, D0E9BADC1F0574D800F079A4 /* PKPayment+Stripe.h in Headers */,
D0383EDC207D1A1600C45548 /* emoji_suggestions_data.h in Headers */,
D0E9BA491F0559B600F079A4 /* STPPaymentMethod.h in Headers */, D0E9BA491F0559B600F079A4 /* STPPaymentMethod.h in Headers */,
D08803C51F6064CF00DD7951 /* TelegramUI.h in Headers */, D08803C51F6064CF00DD7951 /* TelegramUI.h in Headers */,
D0E9BA171F05574500F079A4 /* STPPaymentCardTextFieldViewModel.h in Headers */, D0E9BA171F05574500F079A4 /* STPPaymentCardTextFieldViewModel.h in Headers */,
@ -4924,7 +4899,6 @@
D00817CE22B47A14008A895F /* TGProxyItem.h in Headers */, D00817CE22B47A14008A895F /* TGProxyItem.h in Headers */,
D0E9BA401F0558FE00F079A4 /* StripeError.h in Headers */, D0E9BA401F0558FE00F079A4 /* StripeError.h in Headers */,
D0E9BA191F05574500F079A4 /* STPPaymentCardTextField.h in Headers */, D0E9BA191F05574500F079A4 /* STPPaymentCardTextField.h in Headers */,
D0383EDD207D1A1600C45548 /* TGEmojiSuggestions.h in Headers */,
D0E9BA3F1F0558FE00F079A4 /* STPSource.h in Headers */, D0E9BA3F1F0558FE00F079A4 /* STPSource.h in Headers */,
D008177A22B46B7E008A895F /* TGShareLocationSignals.h in Headers */, D008177A22B46B7E008A895F /* TGShareLocationSignals.h in Headers */,
D0E9BABC1F05735F00F079A4 /* STPPaymentConfiguration.h in Headers */, D0E9BABC1F05735F00F079A4 /* STPPaymentConfiguration.h in Headers */,
@ -5655,7 +5629,6 @@
D056CD761FF2A30900880D28 /* ChatSwipeToReplyRecognizer.swift in Sources */, D056CD761FF2A30900880D28 /* ChatSwipeToReplyRecognizer.swift in Sources */,
D00580B321E4B51600CB7CD3 /* DeleteChatPeerActionSheetItem.swift in Sources */, D00580B321E4B51600CB7CD3 /* DeleteChatPeerActionSheetItem.swift in Sources */,
D091C7A41F8EBB1E00D7DE13 /* ChatPresentationData.swift in Sources */, D091C7A41F8EBB1E00D7DE13 /* ChatPresentationData.swift in Sources */,
D0383EE0207D1A1600C45548 /* TGEmojiSuggestions.mm in Sources */,
09A218F522A15F1400DE6898 /* ThemeSettingsAppIconItem.swift in Sources */, 09A218F522A15F1400DE6898 /* ThemeSettingsAppIconItem.swift in Sources */,
D013630C208FA62400EB3653 /* SecureIdDocumentGalleryFooterContentNode.swift in Sources */, D013630C208FA62400EB3653 /* SecureIdDocumentGalleryFooterContentNode.swift in Sources */,
D0EB41F31F2FEAB800838FE6 /* LegacyComponentsStickers.swift in Sources */, D0EB41F31F2FEAB800838FE6 /* LegacyComponentsStickers.swift in Sources */,
@ -6014,6 +5987,7 @@
D081E106217F5834003CD921 /* LanguageLinkPreviewControllerNode.swift in Sources */, D081E106217F5834003CD921 /* LanguageLinkPreviewControllerNode.swift in Sources */,
D093D7E72063E57F00BC3599 /* BotPaymentActionItemNode.swift in Sources */, D093D7E72063E57F00BC3599 /* BotPaymentActionItemNode.swift in Sources */,
D01C06BA1FBBB076001561AB /* ItemListSelectableControlNode.swift in Sources */, D01C06BA1FBBB076001561AB /* ItemListSelectableControlNode.swift in Sources */,
098CF79222B924E200AF6134 /* ThemeSettingsAccentColorItem.swift in Sources */,
D0EC6E541EB9F58900EBF1C3 /* ConvertToSupergroupController.swift in Sources */, D0EC6E541EB9F58900EBF1C3 /* ConvertToSupergroupController.swift in Sources */,
D0EC6E561EB9F58900EBF1C3 /* UserInfoController.swift in Sources */, D0EC6E561EB9F58900EBF1C3 /* UserInfoController.swift in Sources */,
D0EC6E571EB9F58900EBF1C3 /* GroupsInCommonController.swift in Sources */, D0EC6E571EB9F58900EBF1C3 /* GroupsInCommonController.swift in Sources */,
@ -6044,14 +6018,12 @@
D0EC6E651EB9F58900EBF1C3 /* TwoStepVerificationResetController.swift in Sources */, D0EC6E651EB9F58900EBF1C3 /* TwoStepVerificationResetController.swift in Sources */,
D0EC6E661EB9F58900EBF1C3 /* PasscodeOptionsController.swift in Sources */, D0EC6E661EB9F58900EBF1C3 /* PasscodeOptionsController.swift in Sources */,
09CE95082237A53900A7D2C3 /* SettingsSearchableItems.swift in Sources */, 09CE95082237A53900A7D2C3 /* SettingsSearchableItems.swift in Sources */,
D0383EDE207D1A1600C45548 /* emoji_suggestions.cpp in Sources */,
D0EC6E671EB9F58900EBF1C3 /* DataAndStorageSettingsController.swift in Sources */, D0EC6E671EB9F58900EBF1C3 /* DataAndStorageSettingsController.swift in Sources */,
D0EC6E681EB9F58900EBF1C3 /* VoiceCallDataSavingController.swift in Sources */, D0EC6E681EB9F58900EBF1C3 /* VoiceCallDataSavingController.swift in Sources */,
D0EC6E691EB9F58900EBF1C3 /* NetworkUsageStatsController.swift in Sources */, D0EC6E691EB9F58900EBF1C3 /* NetworkUsageStatsController.swift in Sources */,
D0EC6E6A1EB9F58900EBF1C3 /* StorageUsageController.swift in Sources */, D0EC6E6A1EB9F58900EBF1C3 /* StorageUsageController.swift in Sources */,
D079FCDF1F05C9280038FADE /* BotReceiptController.swift in Sources */, D079FCDF1F05C9280038FADE /* BotReceiptController.swift in Sources */,
D0EC6E6B1EB9F58900EBF1C3 /* InstalledStickerPacksController.swift in Sources */, D0EC6E6B1EB9F58900EBF1C3 /* InstalledStickerPacksController.swift in Sources */,
D0383EDF207D1A1600C45548 /* emoji_suggestions_data.cpp in Sources */,
D0EC6E6C1EB9F58900EBF1C3 /* FeaturedStickerPacksController.swift in Sources */, D0EC6E6C1EB9F58900EBF1C3 /* FeaturedStickerPacksController.swift in Sources */,
D0B85C231FF70BF400E795B4 /* AuthorizationSequenceAwaitingAccountResetController.swift in Sources */, D0B85C231FF70BF400E795B4 /* AuthorizationSequenceAwaitingAccountResetController.swift in Sources */,
D0EC6E6D1EB9F58900EBF1C3 /* ItemListStickerPackItem.swift in Sources */, D0EC6E6D1EB9F58900EBF1C3 /* ItemListStickerPackItem.swift in Sources */,