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";
"PeopleNearby.Title" = "People Nearby";
"PeopleNearby.Description" = "Use this section to quickly find people and groups near you.";
"PeopleNearby.Users" = "People Around You";
"PeopleNearby.UsersEmpty" = "No one else is viewing \"People Nearby\" around you now";
"PeopleNearby.Groups" = "Groups Around You";
"PeopleNearby.CreateGroup" = "Start a Group Chat Here";
"PeopleNearby.Channels" = "Channels Around You";
"PeopleNearby.Description" = "Ask your friend nearby to open this page to exchange phone numbers.";
"PeopleNearby.Users" = "People Nearby";
"PeopleNearby.UsersEmpty" = "Looking for users around you...";
"PeopleNearby.Groups" = "Groups Nearby";
"PeopleNearby.CreateGroup" = "Create a Group Here";
"PeopleNearby.Channels" = "Channels Nearby";
"Channel.Management.LabelOwner" = "Owner";
"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.";
"Group.Location.Title" = "Location";
"Group.Location.SetLocation" = "Set Location";
"Group.Location.ChangeLocation" = "Change Location";
"Group.Location.RemoveLocation" = "Remove Location";
"Group.Location.Info" = "People will be able to find your group in the Groups Nearby section (Contacts > Add People Nearby).";
"Group.Username.Title" = "Username";
"Group.Location.Info" = "People can find your group using People Nearby section.";
"Channel.AdminLog.MessageTransferedName" = "transferred ownership to %1$@";
"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.PeopleNearbyAllowInSettings.v0" = "Allow in Settings";
"Conversation.ReportGroupLocation" = "Group unrelated to tocation?";
"Conversation.ReportGroupLocation" = "Group unrelated to location?";
"ReportGroupLocation.Title" = "Report Unrelated Group";
"ReportGroupLocation.Text" = "Please tell us if this group is not related to this location.";
"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
@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);

View File

@ -345,7 +345,7 @@ const CGPoint TGLocationPickerPinOffset = { 0.0f, 33.0f };
coordinate = [self mapCenterCoordinateForPickerPin];
if (self.locationPicked != nil)
self.locationPicked(coordinate, nil);
self.locationPicked(coordinate, nil, _customAddress);
}
- (void)searchButtonPressed
@ -520,7 +520,7 @@ const CGPoint TGLocationPickerPinOffset = { 0.0f, 33.0f };
NSString *address = @"";
if (result != nil)
address = result.displayAddress;
address = result.fullAddress;
strongSelf->_customAddress = address;
[strongSelf updateCurrentLocationCell];
@ -662,9 +662,9 @@ const CGPoint TGLocationPickerPinOffset = { 0.0f, 33.0f };
_ownLocationView.hidden = true;
_pickerPinWrapper.hidden = false;
if (_intent != TGLocationPickerControllerCustomLocationIntent) {
//if (_intent != TGLocationPickerControllerCustomLocationIntent) {
[_pickerPinView setCustomPin:true animated:true];
}
//}
_mapView.tapEnabled = false;
_mapView.longPressAsTapEnabled = false;
@ -1128,7 +1128,7 @@ const CGPoint TGLocationPickerPinOffset = { 0.0f, 33.0f };
}
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
{
_pinRaised = raised;
avatar = false;
[_shadowView.layer removeAllAnimations];
if (iosMajorVersion() < 7)
@ -491,6 +492,9 @@ NSString *const TGLocationPinAnnotationKind = @"TGLocationPinAnnotation";
_iconView.image = image;
[_backgroundView addSubview:_avatarView];
_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(), ^
{
@ -504,8 +508,11 @@ NSString *const TGLocationPinAnnotationKind = @"TGLocationPinAnnotation";
if (!customPin)
[self addSubview:_avatarView];
_animating = false;
[self setNeedsLayout];
}];
});
[self setNeedsLayout];
}
else
{

View File

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

View File

@ -69,5 +69,17 @@
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

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>) {
let buffer = Buffer()
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>) {
let buffer = Buffer()
buffer.appendInt32(870184064)
@ -3503,6 +3473,38 @@ public extension Api {
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 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
}
public func adminedPublicChannels(account: Account) -> Signal<[Peer], NoError> {
return account.network.request(Api.functions.channels.getAdminedPublicChannels())
public func adminedPublicChannels(account: Account, location: Bool = false) -> Signal<[Peer], NoError> {
var flags: Int32 = 0
if location {
flags |= (1 << 0)
}
return account.network.request(Api.functions.channels.getAdminedPublicChannels(flags: flags))
|> retryRequest
|> mapToSignal { result -> Signal<[Peer], NoError> in
var peers: [Peer] = []

View File

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

View File

@ -14,9 +14,29 @@ import Foundation
#endif
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.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
if error.errorDescription == "USER_RESTRICTED" {
return .restricted
@ -46,17 +66,12 @@ private func createChannel(account: Account, title: String, description: String?
|> switchToLatest
}
public enum CreateChannelError {
case generic
case restricted
}
public func createChannel(account: Account, title: String, description: String?) -> Signal<PeerId, CreateChannelError> {
return createChannel(account: account, title: title, description: description, isSupergroup: false)
}
public func createSupergroup(account: Account, title: String, description: String?) -> Signal<PeerId, CreateChannelError> {
return createChannel(account: account, title: title, description: description, isSupergroup: true)
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, location: location)
}
public enum DeleteChannelError {

View File

@ -90,7 +90,13 @@ public func updateChannelOwnership(postbox: Postbox, network: Network, accountSt
}
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
if let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData {
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 canAddContact = PeerStatusSettings(rawValue: 1 << 4)
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 {

View File

@ -16,11 +16,11 @@ public struct PeerNearby {
public final class PeersNearbyContext {
private let queue: Queue = Queue.mainQueue()
private var subscribers = Bag<([PeerNearby]) -> Void>()
private var subscribers = Bag<([PeerNearby]?) -> Void>()
private let disposable = MetaDisposable()
private var timer: SwiftSignalKit.Timer?
private var entries: [PeerNearby] = []
private var entries: [PeerNearby]?
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)))
@ -53,7 +53,7 @@ public final class PeersNearbyContext {
}
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 }
var existingPeerIds: [PeerId: Int] = [:]
@ -82,7 +82,7 @@ public final class PeersNearbyContext {
}
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)
self.timer?.start()
}
@ -92,7 +92,7 @@ public final class PeersNearbyContext {
self.timer?.invalidate()
}
public func get() -> Signal<[PeerNearby], NoError> {
public func get() -> Signal<[PeerNearby]?, NoError> {
let queue = self.queue
return Signal { [weak self] subscriber in
if let strongSelf = self {

View File

@ -296,6 +296,9 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI
if (flags & (1 << 7)) != 0 {
channelFlags.insert(.canSetStickerSet)
}
if (flags & (1 << 16)) != 0 {
channelFlags.insert(.canChangePeerGeoLocation)
}
let linkedDiscussionPeerId: PeerId?
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
}
controller.setState(state, animated: didAppear)
controller.setState(.permission(state), animated: didAppear)
controller.proceed = { resolved in
permissionsPosition.set(position + 1)
switch state {

View File

@ -235,7 +235,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI
guard let peer = peer else {
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 cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in

View File

@ -19,10 +19,8 @@ private final class ChannelVisibilityControllerArguments {
let copyPrivateLink: () -> Void
let revokePrivateLink: () -> 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.updateCurrentType = updateCurrentType
self.updatePublicLinkText = updatePublicLinkText
@ -33,8 +31,6 @@ private final class ChannelVisibilityControllerArguments {
self.copyPrivateLink = copyPrivateLink
self.revokePrivateLink = revokePrivateLink
self.sharePrivateLink = sharePrivateLink
self.setLocation = setLocation
self.removeLocation = removeLocation
}
}
@ -78,12 +74,6 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
case existingLinksInfo(PresentationTheme, String)
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 {
switch self {
case .typeHeader, .typePublic, .typePrivate, .typeInfo:
@ -94,8 +84,6 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
return ChannelVisibilitySection.linkActions.rawValue
case .existingLinksInfo, .existingLinkPeerItem:
return ChannelVisibilitySection.link.rawValue
case .locationHeader, .location, .locationSetup, .locationRemove, .locationInfo:
return ChannelVisibilitySection.location.rawValue
}
}
@ -133,16 +121,6 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
return 14
case let .existingLinkPeerItem(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 {
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
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 {
if let addressName = peer.addressName, !addressName.isEmpty {
selectedType = .publicChannel
} else if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil {
selectedType = .publicChannel
} else {
selectedType = .privateChannel
}
@ -543,30 +478,33 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
}
}
switch mode {
case .privateLink:
break
case .initialSetup, .generic:
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))
entries.append(.typePrivate(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivate, selectedType == .privateChannel))
switch selectedType {
case .publicChannel:
if isGroup {
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePublicWithLocationHelp))
} else {
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePublicHelp))
}
case .privateChannel:
if isGroup {
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePrivateHelp))
} else {
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivateHelp))
}
}
if let _ = (view.cachedData as? CachedChannelData)?.peerGeoLocation {
} else {
switch mode {
case .privateLink:
break
case .initialSetup, .generic:
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))
entries.append(.typePrivate(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivate, selectedType == .privateChannel))
switch selectedType {
case .publicChannel:
if isGroup {
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePublicHelp))
} else {
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePublicHelp))
}
case .privateChannel:
if isGroup {
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePrivateHelp))
} else {
entries.append(.typeInfo(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivateHelp))
}
}
}
}
switch selectedType {
case .publicChannel:
var displayAvailability = false
@ -596,7 +534,6 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp, true))
}
} else {
entries.append(.publicLinkHeader(presentationData.theme, presentationData.strings.Group_Username_Title.uppercased()))
entries.append(.editablePublicLink(presentationData.theme, currentAddressName))
if let status = state.addressNameValidationStatus {
let text: String
@ -639,26 +576,6 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
}
if isGroup {
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 {
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(.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 {
case .publicChannel:
@ -755,7 +672,6 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp, true))
}
} else {
entries.append(.publicLinkHeader(presentationData.theme, presentationData.strings.Group_Username_Title.uppercased()))
entries.append(.editablePublicLink(presentationData.theme, currentAddressName))
if let status = state.addressNameValidationStatus {
let text: String
@ -789,19 +705,6 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
entries.append(.publicLinkStatus(presentationData.theme, text, status))
}
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:
let link = (view.cachedData as? CachedGroupData)?.exportedInvitation?.link
@ -902,8 +805,8 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
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
if case .addressNameLimitReached = result {
return adminedPublicChannels(account: context.account)
|> map(Optional.init)
return adminedPublicChannels(account: context.account, location: false)
|> map(Optional.init)
} else {
return .single([])
}
@ -1059,60 +962,6 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
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)
@ -1134,14 +983,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
break
case .publicChannel:
var hasLocation = false
if let editingLocation = state.editingLocation {
switch editingLocation {
case .location:
hasLocation = true
case .removed:
hasLocation = false
}
} else if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil {
if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil {
hasLocation = true
}
@ -1247,16 +1089,6 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
case .privateChannel:
break
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 {
switch addressNameValidationStatus {
case .availability(.available):
@ -1265,7 +1097,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
doneEnabled = false
}
} 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.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 {
return
}

View File

@ -169,7 +169,15 @@ public class ComposeController: ViewController {
self.contactsNode.openCreateNewChannel = { [weak self] in
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 {
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
}

View File

@ -855,7 +855,7 @@ final class ContactListNode: ASDisplayNode {
var authorizeImpl: (() -> 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?()
}, openPrivacyPolicy: {
openPrivacyPolicyImpl?()
@ -1256,7 +1256,7 @@ final class ContactListNode: ASDisplayNode {
let authorizationPreviousHidden = strongSelf.authorizationNode.isHidden
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?()
}, openPrivacyPolicy: {
openPrivacyPolicyImpl?()

View File

@ -277,7 +277,7 @@ public class ContactsController: ViewController {
presentPeersNearby()
default:
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
if result {
presentPeersNearby()

View File

@ -8,12 +8,19 @@ import TelegramPresentationData
import TelegramUIPreferences
import LegacyComponents
public enum CreateGroupMode {
case generic
case supergroup
case locatedGroup(latitude: Double, longitude: Double, address: String?)
}
private struct CreateGroupArguments {
let account: Account
let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void
let done: () -> Void
let changeProfilePhoto: () -> Void
let changeLocation: () -> Void
}
private enum CreateGroupSection: Int32 {
@ -47,6 +54,8 @@ private enum CreateGroupEntry: ItemListNodeEntry {
case member(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, PeerPresence?)
case locationHeader(PresentationTheme, String)
case location(PresentationTheme, PeerGeoLocation)
case changeLocation(PresentationTheme, String)
case locationInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
@ -54,7 +63,7 @@ private enum CreateGroupEntry: ItemListNodeEntry {
return CreateGroupSection.info.rawValue
case .member:
return CreateGroupSection.members.rawValue
case .locationHeader, .location:
case .locationHeader, .location, .changeLocation, .locationInfo:
return CreateGroupSection.location.rawValue
}
}
@ -71,6 +80,10 @@ private enum CreateGroupEntry: ItemListNodeEntry {
return 10000
case .location:
return 10001
case .changeLocation:
return 10002
case .locationInfo:
return 10003
}
}
@ -153,6 +166,18 @@ private enum CreateGroupEntry: ItemListNodeEntry {
} else {
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):
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 .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 editingName: ItemListAvatarAndNameInfoItemName
var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?
var location: PeerGeoLocation?
static func ==(lhs: CreateGroupState, rhs: CreateGroupState) -> Bool {
if lhs.creating != rhs.creating {
@ -197,12 +229,14 @@ private struct CreateGroupState: Equatable {
if lhs.avatar != rhs.avatar {
return false
}
if lhs.location != rhs.location {
return false
}
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] = []
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]))
}
if let geoLocation = geoLocation {
if let location = state.location {
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
}
public enum CreateGroupType {
case generic
case supergroup
case locatedGroup(latitude: Double, longitude: Double)
}
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)
public func createGroupController(context: AccountContext, peerIds: [PeerId], initialTitle: String? = nil, mode: CreateGroupMode = .generic, completion: ((PeerId, @escaping () -> Void) -> Void)? = nil) -> ViewController {
var location: PeerGeoLocation?
if case let .locatedGroup(latitude, longitude, address) = mode {
location = PeerGeoLocation(latitude: latitude, longitude: longitude, address: address ?? "")
}
let initialState = CreateGroupState(creating: false, editingName: .title(title: initialTitle ?? "", type: .group), avatar: nil, location: location)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in
@ -269,6 +304,7 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var endEditingImpl: (() -> Void)?
var clearHighlightImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
@ -276,9 +312,16 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
let uploadedAvatar = Promise<UploadedPeerPhotoData>()
let placemarkPromise = Promise<ReverseGeocodedPlacemark?>()
if case let .locatedGroup(latitude, longitude) = type {
placemarkPromise.set(reverseGeocodeLocation(latitude: latitude, longitude: longitude))
let addressPromise = Promise<String?>(nil)
if case let .locatedGroup(latitude, longitude, address) = mode {
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
@ -288,8 +331,8 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
return current
}
}, done: {
let (creating, title) = stateValue.with { state -> (Bool, String) in
return (state.creating, state.editingName.composedTitle)
let (creating, title, location) = stateValue.with { state -> (Bool, String, PeerGeoLocation?) in
return (state.creating, state.editingName.composedTitle, state.location)
}
if !creating && !title.isEmpty {
@ -301,7 +344,7 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
endEditingImpl?()
let createSignal: Signal<PeerId?, CreateGroupError>
switch type {
switch mode {
case .generic:
createSignal = createGroup(account: context.account, title: title, peerIds: peerIds)
case .supergroup:
@ -315,28 +358,25 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
return .restricted
}
}
case let .locatedGroup(latitude, longitude):
createSignal = createSupergroup(account: context.account, title: title, description: nil)
|> map(Optional.init)
|> mapError { error -> CreateGroupError in
switch error {
case .generic:
return .generic
case .restricted:
return .restricted
}
case .locatedGroup:
guard let location = location else {
return
}
|> mapToSignal { peerId in
guard let peerId = peerId else {
return .single(nil)
createSignal = addressPromise.get()
|> introduceError(CreateGroupError.self)
|> mapToSignal { address -> Signal<PeerId?, CreateGroupError> in
guard let address = address else {
return .complete()
}
return placemarkPromise.get()
|> introduceError(CreateGroupError.self)
|> mapToSignal { placemark in
return updateChannelGeoLocation(postbox: context.account.postbox, network: context.account.network, channelId: peerId, coordinate: (latitude, longitude), address: placemark?.fullAddress ?? "\(latitude), \(longitude)")
|> introduceError(CreateGroupError.self)
|> map { _ in
return peerId
return createSupergroup(account: context.account, title: title, description: nil, location: (location.latitude, location.longitude, address))
|> map(Optional.init)
|> mapError { error -> CreateGroupError in
switch error {
case .generic:
return .generic
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()))
|> map { presentationData, state, view, placemark -> (ItemListControllerState, (ItemListNodeState<CreateGroupEntry>, CreateGroupEntry.ItemGenerationArguments)) in
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), context.account.postbox.multiplePeersView(peerIds), .single(nil) |> then(addressPromise.get()))
|> map { presentationData, state, view, address -> (ItemListControllerState, (ItemListNodeState<CreateGroupEntry>, CreateGroupEntry.ItemGenerationArguments)) in
let rightNavigationButton: ItemListNavigationButton
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 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))
}
@ -526,5 +590,8 @@ public func createGroupController(context: AccountContext, peerIds: [PeerId], in
[weak controller] in
controller?.view.endEditing(true)
}
clearHighlightImpl = { [weak controller] in
controller?.clearItemNodesHighlight(animated: true)
}
return controller
}

View File

@ -39,9 +39,10 @@ private final class GroupInfoArguments {
let openGroupTypeSetup: () -> Void
let openLinkedChannelSetup: () -> Void
let openLocation: (PeerGeoLocation) -> Void
let changeLocation: () -> 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.avatarAndNameInfoContext = avatarAndNameInfoContext
self.tapAvatarAction = tapAvatarAction
@ -69,6 +70,7 @@ private final class GroupInfoArguments {
self.openGroupTypeSetup = openGroupTypeSetup
self.openLinkedChannelSetup = openLinkedChannelSetup
self.openLocation = openLocation
self.changeLocation = changeLocation
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 setGroupPhoto(PresentationTheme, String)
case groupDescriptionSetup(PresentationTheme, String, String)
case aboutHeader(PresentationTheme, String)
case about(PresentationTheme, String)
case locationHeader(PresentationTheme, String)
case location(PresentationTheme, PeerGeoLocation)
case changeLocation(PresentationTheme, String)
case link(PresentationTheme, String)
case sharedMedia(PresentationTheme, String)
case notifications(PresentationTheme, String, String)
@ -159,9 +162,9 @@ private enum GroupInfoEntry: ItemListNodeEntry {
var section: ItemListSectionId {
switch self {
case .info, .setGroupPhoto, .groupDescriptionSetup:
case .info, .setGroupPhoto, .groupDescriptionSetup, .about:
return GroupInfoSection.info.rawValue
case .aboutHeader, .about, .link, .location:
case .locationHeader, .location, .changeLocation, .link:
return GroupInfoSection.about.rawValue
case .groupTypeSetup, .linkedChannelSetup, .preHistory, .stickerPack:
return GroupInfoSection.infoManagement.rawValue
@ -237,20 +240,14 @@ private enum GroupInfoEntry: ItemListNodeEntry {
} else {
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):
if case let .about(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 {
case let .locationHeader(lhsTheme, lhsText):
if case let .locationHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
@ -261,6 +258,18 @@ private enum GroupInfoEntry: ItemListNodeEntry {
} else {
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):
if case let .notifications(rhsTheme, rhsTitle, rhsText) = rhs {
if lhsTheme !== rhsTheme {
@ -399,13 +408,15 @@ private enum GroupInfoEntry: ItemListNodeEntry {
return 1
case .groupDescriptionSetup:
return 2
case .aboutHeader:
return 4
case .about:
return 5
case .link:
return 6
return 3
case .locationHeader:
return 4
case .location:
return 5
case .changeLocation:
return 6
case .link:
return 7
case .groupTypeSetup:
return 8
@ -424,11 +435,11 @@ private enum GroupInfoEntry: ItemListNodeEntry {
case .administrators:
return 15
case .addMember:
return 17
return 16
case let .member(_, _, _, _, index, _, _, _, _, _, _, _, _, _):
return 20 + index
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: {
arguments.changeProfilePhoto()
})
case let .aboutHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .about(theme, text):
return ItemListMultilineTextItem(theme: theme, text: foldMultipleLineBreaks(text), enabledEntitiyTypes: [.url, .mention, .hashtag], sectionId: self.section, style: .blocks, longTapAction: {
arguments.displayAboutContextMenu(text)
}, linkItemAction: { action, itemLink in
arguments.aboutLinkAction(action, itemLink)
}, tag: GroupInfoEntryTag.about)
case let .link(theme, url):
return ItemListActionItem(theme: theme, title: url, kind: .neutral, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.displayUsernameShareMenu(url)
}, longTapAction: {
arguments.displayUsernameContextMenu(url)
}, tag: GroupInfoEntryTag.link)
case let .locationHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .location(theme, location):
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)
}, longTapAction: {
arguments.displayLocationContextMenu(location.address.replacingOccurrences(of: "\n", with: ", "))
}, 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):
return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: {
arguments.changeNotificationMuteSettings()
@ -806,21 +821,33 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa
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 {
if isCreator || (channel.adminRights != nil && channel.hasPermission(.pinMessages)) {
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 isCreator, let location = cachedChannelData.peerGeoLocation {
entries.append(.locationHeader(presentationData.theme, presentationData.strings.GroupInfo_Location.uppercased()))
entries.append(.location(presentationData.theme, location))
if cachedChannelData.flags.contains(.canChangePeerGeoLocation) {
entries.append(.changeLocation(presentationData.theme, presentationData.strings.Group_Location_ChangeLocation))
}
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 {
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))
}
else if let cachedChannelData = view.cachedData as? CachedChannelData {
if let about = cachedChannelData.about, !about.isEmpty {
entries.append(.aboutHeader(presentationData.theme, presentationData.strings.Channel_About_Title.uppercased()))
entries.append(.about(presentationData.theme, about))
}
if let peer = view.peers[view.peerId] as? TelegramChannel, let username = peer.username, !username.isEmpty {
entries.append(.link(presentationData.theme, "t.me/" + username))
if let peer = view.peers[view.peerId] as? TelegramChannel {
if let location = cachedChannelData.peerGeoLocation {
entries.append(.locationHeader(presentationData.theme, presentationData.strings.GroupInfo_Location.uppercased()))
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 {
if let about = cachedGroupData.about, !about.isEmpty {
entries.append(.aboutHeader(presentationData.theme, presentationData.strings.Channel_About_Title.uppercased()))
entries.append(.about(presentationData.theme, about))
}
}
@ -1239,6 +1265,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
var endEditingImpl: (() -> Void)?
var removePeerChatImpl: ((Peer, Bool) -> Void)?
var errorImpl: (() -> Void)?
var clearHighlightImpl: (() -> Void)?
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 })
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
displayCopyContextMenuImpl?(text, .location)
})
@ -2320,6 +2381,9 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
[weak controller] in
controller?.view.endEditing(true)
}
clearHighlightImpl = { [weak controller] in
controller?.clearItemNodesHighlight(animated: true)
}
let hapticFeedback = HapticFeedback()
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)
}
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)
legacyController.presentationCompleted = {
presentationCompleted()
@ -34,10 +34,10 @@ func legacyLocationPickerController(context: AccountContext, selfPeer: Peer, pee
}, rootController: nil)
legacyController.bind(controller: navigationController)
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
return MapVenue(title: venue.title, address: venue.address, provider: venue.provider, id: venue.venueId, type: venue.type)
})
}, address)
legacyController?.dismiss()
}
controller.liveLocationStarted = { [weak legacyController] coordinate, period in

View File

@ -37,9 +37,9 @@ private func arePeerNearbyArraysEqual(_ lhs: [PeerNearbyEntry], _ rhs: [PeerNear
private final class PeersNearbyControllerArguments {
let context: AccountContext
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.openChat = openChat
self.openCreateGroup = openCreateGroup
@ -61,7 +61,7 @@ private enum PeersNearbyEntry: ItemListNodeEntry {
case user(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry)
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 channelsHeader(PresentationTheme, String)
@ -135,8 +135,8 @@ private enum PeersNearbyEntry: ItemListNodeEntry {
} else {
return false
}
case let .createGroup(lhsTheme, lhsText, lhsLatitude, lhsLongitude):
if case let .createGroup(rhsTheme, rhsText, rhsLatitude, rhsLongitude) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLatitude == rhsLatitude && lhsLongitude == rhsLongitude {
case let .createGroup(lhsTheme, lhsText, lhsLatitude, lhsLongitude, lhsAddress):
if case let .createGroup(rhsTheme, rhsText, rhsLatitude, rhsLongitude, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLatitude == rhsLatitude && lhsLongitude == rhsLongitude && lhsAddress == rhsAddress {
return true
} else {
return false
@ -183,7 +183,7 @@ private enum PeersNearbyEntry: ItemListNodeEntry {
return PeersNearbyHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .usersHeader(theme, text):
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)
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: {
@ -191,10 +191,10 @@ private enum PeersNearbyEntry: ItemListNodeEntry {
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopGroupInset: false, tag: nil)
case let .groupsHeader(theme, text):
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: {
if let latitude = latitude, let longitude = longitude {
arguments.openCreateGroup(latitude, longitude)
arguments.openCreateGroup(latitude, longitude, address)
}
})
case let .group(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer):
@ -226,20 +226,22 @@ private enum PeersNearbyEntry: ItemListNodeEntry {
private struct PeersNearbyData: Equatable {
let latitude: Double
let longitude: Double
let address: String?
let users: [PeerNearbyEntry]
let groups: [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.longitude = longitude
self.address = address
self.users = users
self.groups = groups
self.channels = channels
}
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(.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 {
var i: Int32 = 0
for group in data.groups {
@ -282,18 +284,25 @@ private func peersNearbyControllerEntries(data: PeersNearbyData?, presentationDa
public func peersNearbyController(context: AccountContext) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
var replaceTopControllerImpl: ((ViewController, Bool) -> Void)?
var replaceAllButRootControllerImpl: ((ViewController, Bool) -> Void)?
var replaceTopControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var navigateToChatImpl: ((Peer) -> Void)?
let actionsDisposable = DisposableSet()
let dataPromise = Promise<PeersNearbyData?>(nil)
let addressPromise = Promise<String?>(nil)
let arguments = PeersNearbyControllerArguments(context: context, openChat: { peer in
navigateToChatImpl?(peer)
}, openCreateGroup: { latitude, longitude in
let controller = createGroupController(context: context, peerIds: [], type: .locatedGroup(latitude: latitude, longitude: longitude))
}, openCreateGroup: { latitude, longitude, address in
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)
})
@ -307,9 +316,23 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
return Signal { subscriber in
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)
|> 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
var users: [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)
}
@ -346,8 +369,10 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
|> restartIfError
|> `catch` { _ -> Signal<PeersNearbyData?, NoError> in
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())
|> deliverOnMainQueue
@ -362,11 +387,14 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
}
let controller = ItemListController(context: context, state: signal)
controller.didDisappear = { [weak controller] _ in
controller?.clearItemNodesHighlight(animated: true)
}
navigateToChatImpl = { [weak controller] peer in
if let navigationController = controller?.navigationController as? NavigationController {
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 {
replaceTopControllerImpl?(chatController, false)
replaceAllButRootControllerImpl?(chatController, false)
}
})
}
@ -376,11 +404,16 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
(controller.navigationController as? NavigationController)?.pushViewController(c, animated: true)
}
}
replaceTopControllerImpl = { [weak controller] c, a in
replaceAllButRootControllerImpl = { [weak controller] c, a in
if let controller = controller {
(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
if let controller = controller {
controller.present(c, in: .window(.root), with: p)

View File

@ -76,7 +76,7 @@ class PeersNearbyHeaderItemNode: ListViewItemNode {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
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 attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor)

View File

@ -6,13 +6,15 @@ import TelegramPresentationData
final class PermissionContentNode: ASDisplayNode {
private var theme: PresentationTheme
let kind: PermissionKind
let kind: Int32
private let iconNode: ASImageNode
private let nearbyIconNode: PeersNearbyIconNode?
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let actionButton: SolidRoundedButtonNode
private let footerNode: ImmediateTextNode
private let privacyPolicyButton: HighlightableButtonNode
private var title: String
@ -20,7 +22,7 @@ final class PermissionContentNode: ASDisplayNode {
var buttonAction: (() -> 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.kind = kind
@ -34,7 +36,7 @@ final class PermissionContentNode: ASDisplayNode {
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
if kind == .nearbyLocation {
if kind == PermissionKind.nearbyLocation.rawValue {
self.nearbyIconNode = PeersNearbyIconNode(theme: theme)
} else {
self.nearbyIconNode = nil
@ -46,6 +48,12 @@ final class PermissionContentNode: ASDisplayNode {
self.titleNode.isUserInteractionEnabled = 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.textAlignment = .center
self.textNode.maximumNumberOfLines = 0
@ -53,6 +61,11 @@ final class PermissionContentNode: ASDisplayNode {
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.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.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)
if let nearbyIconNode = self.nearbyIconNode {
self.addSubnode(nearbyIconNode)
}
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.actionButton)
self.addSubnode(self.footerNode)
self.addSubnode(self.privacyPolicyButton)
self.actionButton.pressed = { [weak self] in
@ -93,25 +116,33 @@ final class PermissionContentNode: ASDisplayNode {
let fontSize: CGFloat
if min(size.width, size.height) > 330.0 {
fontSize = 24.0
sidePadding = 38.0
sidePadding = 36.0
} else {
fontSize = 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 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 buttonWidth = min(size.width, size.height)
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 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))
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 imageSpacing: CGFloat = 0.0
@ -126,24 +157,36 @@ final class PermissionContentNode: ASDisplayNode {
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 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 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)
if let nearbyIconNode = self.nearbyIconNode {
transition.updateFrame(node: nearbyIconNode, frame: nearbyIconFrame)
}
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.actionButton, frame: buttonFrame)
transition.updateFrame(node: self.footerNode, frame: footerFrame)
transition.updateFrame(node: self.privacyPolicyButton, frame: privacyButtonFrame)
}
}

View File

@ -10,7 +10,7 @@ import DeviceAccess
public final class PermissionController : ViewController {
private let context: AccountContext
private let splitTest: PermissionUISplitTest?
private var state: PermissionState?
private var state: PermissionControllerContent?
private var splashScreen = false
private var controllerNode: PermissionControllerNode {
@ -101,95 +101,103 @@ public final class PermissionController : ViewController {
self.context.sharedContext.applicationBindings.openSettings()
}
public func setState(_ state: PermissionState, animated: Bool) {
public func setState(_ state: PermissionControllerContent, animated: Bool) {
guard state != self.state else {
return
}
self.state = state
switch state {
case let .contacts(status):
self.splitTest?.addEvent(.ContactsModalRequest)
self.allow = { [weak self] in
if let strongSelf = self {
switch status {
case .requestable:
strongSelf.splitTest?.addEvent(.ContactsRequest)
DeviceAccess.authorizeAccess(to: .contacts, { [weak self] result in
if let strongSelf = self {
if result {
strongSelf.splitTest?.addEvent(.ContactsAllowed)
} else {
strongSelf.splitTest?.addEvent(.ContactsDenied)
if case let .permission(permission) = state, let state = permission {
switch state {
case let .contacts(status):
self.splitTest?.addEvent(.ContactsModalRequest)
self.allow = { [weak self] in
if let strongSelf = self {
switch status {
case .requestable:
strongSelf.splitTest?.addEvent(.ContactsRequest)
DeviceAccess.authorizeAccess(to: .contacts, { [weak self] result in
if let strongSelf = self {
if result {
strongSelf.splitTest?.addEvent(.ContactsAllowed)
} else {
strongSelf.splitTest?.addEvent(.ContactsDenied)
}
strongSelf.proceed?(true)
}
strongSelf.proceed?(true)
}
})
case .denied:
strongSelf.openAppSettings()
strongSelf.proceed?(true)
default:
break
})
case .denied:
strongSelf.openAppSettings()
strongSelf.proceed?(true)
default:
break
}
}
}
}
case let .notifications(status):
self.splitTest?.addEvent(.NotificationsModalRequest)
self.allow = { [weak self] in
if let strongSelf = self {
switch status {
case .requestable:
strongSelf.splitTest?.addEvent(.NotificationsRequest)
let context = strongSelf.context
DeviceAccess.authorizeAccess(to: .notifications, registerForNotifications: { [weak context] result in
context?.sharedContext.applicationBindings.registerForNotifications(result)
}, { [weak self] result in
if let strongSelf = self {
if result {
strongSelf.splitTest?.addEvent(.NotificationsAllowed)
} else {
strongSelf.splitTest?.addEvent(.NotificationsDenied)
case let .notifications(status):
self.splitTest?.addEvent(.NotificationsModalRequest)
self.allow = { [weak self] in
if let strongSelf = self {
switch status {
case .requestable:
strongSelf.splitTest?.addEvent(.NotificationsRequest)
let context = strongSelf.context
DeviceAccess.authorizeAccess(to: .notifications, registerForNotifications: { [weak context] result in
context?.sharedContext.applicationBindings.registerForNotifications(result)
}, { [weak self] result in
if let strongSelf = self {
if result {
strongSelf.splitTest?.addEvent(.NotificationsAllowed)
} else {
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:
strongSelf.openAppSettings()
strongSelf.proceed?(true)
strongSelf.proceed?(false)
default:
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

View File

@ -6,8 +6,23 @@ import SwiftSignalKit
import TelegramCore
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 {
var state: PermissionState?
var state: PermissionControllerContent?
}
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
return PermissionControllerInnerState(layout: currentState.layout, data: PermissionControllerDataState(state: state))
}, transition: transition)
@ -112,107 +127,132 @@ final class PermissionControllerNode: ASDisplayNode {
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))
if state.data.state?.kind != self.contentNode?.kind {
if let dataState = state.data.state {
let icon: UIImage?
let title: String
let text: String
let buttonTitle: String
let hasPrivacyPolicy: Bool
switch dataState {
case let .contacts(status):
icon = UIImage(bundleImageName: "Settings/Permissions/Contacts")
if let splitTest = self.splitTest, case let .modal(titleKey, textKey, allowTitleKey, allowInSettingsTitleKey) = splitTest.configuration.contacts {
title = localizedString(for: titleKey, strings: self.presentationData.strings)
text = localizedString(for: textKey, strings: self.presentationData.strings)
if status == .denied {
buttonTitle = localizedString(for: allowInSettingsTitleKey, strings: self.presentationData.strings)
} else {
buttonTitle = localizedString(for: allowTitleKey, strings: self.presentationData.strings)
if let state = state.data.state {
switch state {
case let .permission(permission):
if permission?.kind.rawValue != self.contentNode?.kind {
if let dataState = permission {
let icon: UIImage?
let title: String
let text: String
let buttonTitle: String
let hasPrivacyPolicy: Bool
switch dataState {
case let .contacts(status):
icon = UIImage(bundleImageName: "Settings/Permissions/Contacts")
if let splitTest = self.splitTest, case let .modal(titleKey, textKey, allowTitleKey, allowInSettingsTitleKey) = splitTest.configuration.contacts {
title = localizedString(for: titleKey, strings: self.presentationData.strings)
text = localizedString(for: textKey, 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
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
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
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
}
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 if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: contentFrame)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: transition)
}
case let .custom(icon, title, subtitle, text, buttonTitle, footerText):
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: contentFrame)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: transition)
} else {
let iconImage: UIImage?
if self.presentationData.theme.overallDarkAppearance {
iconImage = icon.dark ?? icon.light
} 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
}
iconImage = icon.light
}
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
}
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
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
self?.allow?()
}, openPrivacyPolicy: nil)
self.insertSubnode(contentNode, at: 0)
contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: .immediate)
contentNode.frame = contentFrame
self.contentNode = contentNode
}
}
} 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? {
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 "../RaiseToListenActivator.h"
header "../TGMimeTypeMap.h"
header "../TGEmojiSuggestions.h"
header "../TGChannelIntroController.h"
header "../Bridge Audio/TGBridgeAudioDecoder.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
}
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 {
var pushControllerImpl: ((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 */; };
09874E582107A4C300E190B8 /* VimeoEmbedImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09874E3A21075BF400E190B8 /* VimeoEmbedImplementation.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 */; };
099529AC21CDBBB200805E13 /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099529AB21CDBBB200805E13 /* QRCode.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 */; };
D0380DB8204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DB7204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.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 */; };
D0383EE6207D299600C45548 /* EmojisChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.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>"; };
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>"; };
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>"; };
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>"; };
@ -1588,12 +1584,6 @@
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>"; };
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>"; };
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>"; };
@ -3007,19 +2997,6 @@
name = "Plaintext Fields";
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 */ = {
isa = PBXGroup;
children = (
@ -3228,6 +3205,7 @@
D0B37C5F1F8D286E004252DF /* ThemeSettingsFontSizeItem.swift */,
D06F31E52135A41C001A0F12 /* ThemeSettingsBrightnessItem.swift */,
09A218F422A15F1400DE6898 /* ThemeSettingsAppIconItem.swift */,
098CF79122B924E200AF6134 /* ThemeSettingsAccentColorItem.swift */,
090E777822A6A32E00CD99F5 /* ThemeSettingsThemeItem.swift */,
D06E4C322134A59700088087 /* ThemeAccentColorActionSheet.swift */,
D06E4C342134AE3C00088087 /* ThemeAutoNightSettingsController.swift */,
@ -4728,7 +4706,6 @@
09E4A7FE223ADFD80038140F /* Data */,
D0B69C3A20EBD8B3003632C7 /* Device */,
D01C7EFE1EF9D434008305F1 /* Device Contacts */,
D0383ED5207D19BC00C45548 /* Emoji */,
D025A4241F79428300563950 /* Fetch Manager */,
09E4A7FC223ADF300038140F /* Images */,
D04614352005093B00EC0EF2 /* Location */,
@ -4882,7 +4859,6 @@
D0208AD51FA33D14001F0D5F /* RaiseToListenActivator.h in Headers */,
D00817DA22B47A14008A895F /* TGPresentationAutoNightPreferences.h in Headers */,
D0E9BAE31F0574D800F079A4 /* STPBankAccountParams.h in Headers */,
D0383EE1207D1A1600C45548 /* emoji_suggestions.h in Headers */,
D0E9BA361F05585000F079A4 /* STPPhoneNumberValidator.h in Headers */,
D0E9BA511F0559DA00F079A4 /* STPImageLibrary.h in Headers */,
D0E9BA4C1F0559C700F079A4 /* NSString+Stripe_CardBrands.h in Headers */,
@ -4892,7 +4868,6 @@
D0E9BA2A1F0557A600F079A4 /* STPFormEncoder.h in Headers */,
D0E9BA321F05583A00F079A4 /* STPPostalCodeValidator.h in Headers */,
D0E9BADC1F0574D800F079A4 /* PKPayment+Stripe.h in Headers */,
D0383EDC207D1A1600C45548 /* emoji_suggestions_data.h in Headers */,
D0E9BA491F0559B600F079A4 /* STPPaymentMethod.h in Headers */,
D08803C51F6064CF00DD7951 /* TelegramUI.h in Headers */,
D0E9BA171F05574500F079A4 /* STPPaymentCardTextFieldViewModel.h in Headers */,
@ -4924,7 +4899,6 @@
D00817CE22B47A14008A895F /* TGProxyItem.h in Headers */,
D0E9BA401F0558FE00F079A4 /* StripeError.h in Headers */,
D0E9BA191F05574500F079A4 /* STPPaymentCardTextField.h in Headers */,
D0383EDD207D1A1600C45548 /* TGEmojiSuggestions.h in Headers */,
D0E9BA3F1F0558FE00F079A4 /* STPSource.h in Headers */,
D008177A22B46B7E008A895F /* TGShareLocationSignals.h in Headers */,
D0E9BABC1F05735F00F079A4 /* STPPaymentConfiguration.h in Headers */,
@ -5655,7 +5629,6 @@
D056CD761FF2A30900880D28 /* ChatSwipeToReplyRecognizer.swift in Sources */,
D00580B321E4B51600CB7CD3 /* DeleteChatPeerActionSheetItem.swift in Sources */,
D091C7A41F8EBB1E00D7DE13 /* ChatPresentationData.swift in Sources */,
D0383EE0207D1A1600C45548 /* TGEmojiSuggestions.mm in Sources */,
09A218F522A15F1400DE6898 /* ThemeSettingsAppIconItem.swift in Sources */,
D013630C208FA62400EB3653 /* SecureIdDocumentGalleryFooterContentNode.swift in Sources */,
D0EB41F31F2FEAB800838FE6 /* LegacyComponentsStickers.swift in Sources */,
@ -6014,6 +5987,7 @@
D081E106217F5834003CD921 /* LanguageLinkPreviewControllerNode.swift in Sources */,
D093D7E72063E57F00BC3599 /* BotPaymentActionItemNode.swift in Sources */,
D01C06BA1FBBB076001561AB /* ItemListSelectableControlNode.swift in Sources */,
098CF79222B924E200AF6134 /* ThemeSettingsAccentColorItem.swift in Sources */,
D0EC6E541EB9F58900EBF1C3 /* ConvertToSupergroupController.swift in Sources */,
D0EC6E561EB9F58900EBF1C3 /* UserInfoController.swift in Sources */,
D0EC6E571EB9F58900EBF1C3 /* GroupsInCommonController.swift in Sources */,
@ -6044,14 +6018,12 @@
D0EC6E651EB9F58900EBF1C3 /* TwoStepVerificationResetController.swift in Sources */,
D0EC6E661EB9F58900EBF1C3 /* PasscodeOptionsController.swift in Sources */,
09CE95082237A53900A7D2C3 /* SettingsSearchableItems.swift in Sources */,
D0383EDE207D1A1600C45548 /* emoji_suggestions.cpp in Sources */,
D0EC6E671EB9F58900EBF1C3 /* DataAndStorageSettingsController.swift in Sources */,
D0EC6E681EB9F58900EBF1C3 /* VoiceCallDataSavingController.swift in Sources */,
D0EC6E691EB9F58900EBF1C3 /* NetworkUsageStatsController.swift in Sources */,
D0EC6E6A1EB9F58900EBF1C3 /* StorageUsageController.swift in Sources */,
D079FCDF1F05C9280038FADE /* BotReceiptController.swift in Sources */,
D0EC6E6B1EB9F58900EBF1C3 /* InstalledStickerPacksController.swift in Sources */,
D0383EDF207D1A1600C45548 /* emoji_suggestions_data.cpp in Sources */,
D0EC6E6C1EB9F58900EBF1C3 /* FeaturedStickerPacksController.swift in Sources */,
D0B85C231FF70BF400E795B4 /* AuthorizationSequenceAwaitingAccountResetController.swift in Sources */,
D0EC6E6D1EB9F58900EBF1C3 /* ItemListStickerPackItem.swift in Sources */,