diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index a14b0d5dcb..4d4967d035 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -307,6 +307,7 @@ public enum ResolvedUrl { case boost(peerId: PeerId?, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?) case premiumGiftCode(slug: String) case premiumMultiGift(reference: String?) + case messageLink(link: TelegramResolvedMessageLink) } public enum ResolveUrlResult { @@ -865,6 +866,9 @@ public protocol CollectibleItemInfoScreenInitialData: AnyObject { var collectibleItemInfo: TelegramCollectibleItemInfo { get } } +public protocol BusinessLinksSetupScreenInitialData: AnyObject { +} + public enum CollectibleItemInfoScreenSubject { case phoneNumber(String) case username(String) @@ -968,6 +972,8 @@ public protocol SharedAccountContext: AnyObject { func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal func makeBusinessIntroSetupScreen(context: AccountContext, initialData: BusinessIntroSetupScreenInitialData) -> ViewController func makeBusinessIntroSetupScreenInitialData(context: AccountContext) -> Signal + func makeBusinessLinksSetupScreen(context: AccountContext, initialData: BusinessLinksSetupScreenInitialData) -> ViewController + func makeBusinessLinksSetupScreenInitialData(context: AccountContext) -> Signal func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal func navigateToChatController(_ params: NavigateToChatControllerParams) diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index e58660538d..bde186f915 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -1091,6 +1091,7 @@ public enum ChatQuickReplyShortcutType { public enum ChatCustomContentsKind: Equatable { case quickReplyMessageInput(shortcut: String, shortcutType: ChatQuickReplyShortcutType) + case businessLinkSetup(link: TelegramBusinessChatLinks.Link) } public protocol ChatCustomContentsProtocol: AnyObject { @@ -1103,6 +1104,7 @@ public protocol ChatCustomContentsProtocol: AnyObject { func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) func quickReplyUpdateShortcut(value: String) + func businessLinkUpdate(message: String, entities: [MessageTextEntity], title: String?) } public enum ChatHistoryListDisplayHeaders { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 612184ba43..025d315385 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -96,12 +96,14 @@ public enum ChatListItemContent { public var searchQuery: String? public var messageCount: Int? public var hideSeparator: Bool + public var hideDate: Bool - public init(commandPrefix: String?, searchQuery: String?, messageCount: Int?, hideSeparator: Bool) { + public init(commandPrefix: String?, searchQuery: String?, messageCount: Int?, hideSeparator: Bool, hideDate: Bool) { self.commandPrefix = commandPrefix self.searchQuery = searchQuery self.messageCount = messageCount self.hideSeparator = hideSeparator + self.hideDate = hideDate } } @@ -2741,12 +2743,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { case let .peer(peerData): topIndex = peerData.messages.first?.index } - if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { - if let messageCount = customMessageListData.messageCount, customMessageListData.commandPrefix == nil { - dateText = "\(messageCount)" - } else { - dateText = " " - } + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, let messageCount = customMessageListData.messageCount, customMessageListData.commandPrefix == nil { + dateText = "\(messageCount)" + } else if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.hideDate { + dateText = " " } else if let topIndex { var t = Int(topIndex.timestamp) var timeinfo = tm() diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 3e53306ce0..15849fc8fa 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -1112,6 +1112,10 @@ public final class ChatPresentationInterfaceState: Equatable { return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) } + public func updatedSubject(_ subject: ChatControllerSubject?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + } + public func updatedAutoremoveTimeout(_ autoremoveTimeout: Int32?) -> ChatPresentationInterfaceState { return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) } diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index 32531869ed..7b768293f3 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -1399,6 +1399,9 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta adminedPublicChannels.set(.single(peers)) } else { adminedPublicChannels.set(context.engine.peers.adminedPublicChannels(scope: .all) + |> map { result in + return result.map(\.peer) + } |> map(Optional.init)) } @@ -1406,6 +1409,9 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta peersDisablingAddressNameAssignment.set(.single(nil) |> then(context.engine.peers.channelAddressNameAssignmentAvailability(peerId: peerId.namespace == Namespaces.Peer.CloudChannel ? peerId : nil) |> mapToSignal { result -> Signal<[EnginePeer]?, NoError> in if case .addressNameLimitReached = result { return context.engine.peers.adminedPublicChannels(scope: .all) + |> map { result in + return result.map(\.peer) + } |> map(Optional.init) } else { return .single([]) @@ -1478,7 +1484,11 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta let controller = channelVisibilityController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, mode: .revokeNames(peers), upgradedToSupergroup: { _, _ in }, revokedPeerAddressName: { revokedPeerId in let updatedPublicChannels = peers.filter { $0.id != revokedPeerId } adminedPublicChannels.set(.single(updatedPublicChannels) |> then( - context.engine.peers.adminedPublicChannels(scope: .all) |> map(Optional.init)) + context.engine.peers.adminedPublicChannels(scope: .all) + |> map { result in + return result.map(\.peer) + } + |> map(Optional.init)) ) }) controller.navigationPresentation = .modal diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index c9a5d2b66e..c5c3a54c58 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -857,14 +857,6 @@ struct PremiumIntroConfiguration { businessPerks = PremiumIntroConfiguration.defaultValue.businessPerks } -#if DEBUG - if !businessPerks.contains(.businessLinks) { - businessPerks.append(.businessLinks) - } - if !businessPerks.contains(.businessIntro) { - businessPerks.append(.businessIntro) - } -#endif return PremiumIntroConfiguration(perks: perks, businessPerks: businessPerks) } else { return .defaultValue @@ -2175,7 +2167,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ))) ], alignment: .left, spacing: 2.0)), leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( - backgroundColor: gradientColors[i], + backgroundColor: gradientColors[min(i, gradientColors.count - 1)], foregroundColor: .white, iconName: perk.iconName ))), @@ -2239,6 +2231,15 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData)) }) + case .businessLinks: + let _ = (accountContext.sharedContext.makeBusinessLinksSetupScreenInitialData(context: accountContext) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeBusinessLinksSetupScreen(context: accountContext, initialData: initialData)) + }) case .businessIntro: let _ = (accountContext.sharedContext.makeBusinessIntroSetupScreenInitialData(context: accountContext) |> take(1) @@ -2248,8 +2249,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeBusinessIntroSetupScreen(context: accountContext, initialData: initialData)) }) - case .businessLinks: - fatalError() default: fatalError() } @@ -3652,6 +3651,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { if case .business = mode { context.account.viewTracker.keepQuickRepliesApproximatelyUpdated() + context.account.viewTracker.keepBusinessLinksApproximatelyUpdated() } } diff --git a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift index a423c640a1..194d60b171 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift @@ -282,7 +282,9 @@ func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDat return peers } - preloadedGroupPeers.set(context.engine.peers.adminedPublicChannels(scope: .all)) + preloadedGroupPeers.set(context.engine.peers.adminedPublicChannels(scope: .all) |> map { result in + return result.map(\.peer) + }) case let .groups(preloadedPeers): peers = .single(preloadedPeers.shuffled()) default: diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 5eebf651dc..19d96dca60 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -965,7 +965,6 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[347227392] = { return Api.Update.parse_updateGroupCall($0) } dict[192428418] = { return Api.Update.parse_updateGroupCallConnection($0) } dict[-219423922] = { return Api.Update.parse_updateGroupCallParticipants($0) } - dict[-856651050] = { return Api.Update.parse_updateGroupInvitePrivacyForbidden($0) } dict[1763610706] = { return Api.Update.parse_updateInlineBotCallbackQuery($0) } dict[1442983757] = { return Api.Update.parse_updateLangPack($0) } dict[1180041828] = { return Api.Update.parse_updateLangPackTooLong($0) } @@ -1347,7 +1346,7 @@ public extension Api { return parser(reader) } else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") return nil } } diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index 641b7e9dfd..2c46795cb8 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -117,7 +117,6 @@ public extension Api { case updateGroupCall(chatId: Int64, call: Api.GroupCall) case updateGroupCallConnection(flags: Int32, params: Api.DataJSON) case updateGroupCallParticipants(call: Api.InputGroupCall, participants: [Api.GroupCallParticipant], version: Int32) - case updateGroupInvitePrivacyForbidden(userId: Int64) case updateInlineBotCallbackQuery(flags: Int32, queryId: Int64, userId: Int64, msgId: Api.InputBotInlineMessageID, chatInstance: Int64, data: Buffer?, gameShortName: String?) case updateLangPack(difference: Api.LangPackDifference) case updateLangPackTooLong(langCode: String) @@ -785,12 +784,6 @@ public extension Api { } serializeInt32(version, buffer: buffer, boxed: false) break - case .updateGroupInvitePrivacyForbidden(let userId): - if boxed { - buffer.appendInt32(-856651050) - } - serializeInt64(userId, buffer: buffer, boxed: false) - break case .updateInlineBotCallbackQuery(let flags, let queryId, let userId, let msgId, let chatInstance, let data, let gameShortName): if boxed { buffer.appendInt32(1763610706) @@ -1494,8 +1487,6 @@ public extension Api { return ("updateGroupCallConnection", [("flags", flags as Any), ("params", params as Any)]) case .updateGroupCallParticipants(let call, let participants, let version): return ("updateGroupCallParticipants", [("call", call as Any), ("participants", participants as Any), ("version", version as Any)]) - case .updateGroupInvitePrivacyForbidden(let userId): - return ("updateGroupInvitePrivacyForbidden", [("userId", userId as Any)]) case .updateInlineBotCallbackQuery(let flags, let queryId, let userId, let msgId, let chatInstance, let data, let gameShortName): return ("updateInlineBotCallbackQuery", [("flags", flags as Any), ("queryId", queryId as Any), ("userId", userId as Any), ("msgId", msgId as Any), ("chatInstance", chatInstance as Any), ("data", data as Any), ("gameShortName", gameShortName as Any)]) case .updateLangPack(let difference): @@ -2877,17 +2868,6 @@ public extension Api { return nil } } - public static func parse_updateGroupInvitePrivacyForbidden(_ reader: BufferReader) -> Update? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.Update.updateGroupInvitePrivacyForbidden(userId: _1!) - } - else { - return nil - } - } public static func parse_updateInlineBotCallbackQuery(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index e140ec56eb..169d91f2d4 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -2990,20 +2990,20 @@ public extension Api.functions.channels { } } public extension Api.functions.channels { - static func inviteToChannel(channel: Api.InputChannel, users: [Api.InputUser]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func inviteToChannel(channel: Api.InputChannel, users: [Api.InputUser]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(429865580) + buffer.appendInt32(-907854508) channel.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(users.count)) for item in users { item.serialize(buffer, true) } - return (FunctionDescription(name: "channels.inviteToChannel", parameters: [("channel", String(describing: channel)), ("users", String(describing: users))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "channels.inviteToChannel", parameters: [("channel", String(describing: channel)), ("users", String(describing: users))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.InvitedUsers? in let reader = BufferReader(buffer) - var result: Api.Updates? + var result: Api.messages.InvitedUsers? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Updates + result = Api.parse(reader, signature: signature) as? Api.messages.InvitedUsers } return result }) @@ -4660,17 +4660,17 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func addChatUser(chatId: Int64, userId: Api.InputUser, fwdLimit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func addChatUser(chatId: Int64, userId: Api.InputUser, fwdLimit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-230206493) + buffer.appendInt32(-876162809) serializeInt64(chatId, buffer: buffer, boxed: false) userId.serialize(buffer, true) serializeInt32(fwdLimit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "messages.addChatUser", parameters: [("chatId", String(describing: chatId)), ("userId", String(describing: userId)), ("fwdLimit", String(describing: fwdLimit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "messages.addChatUser", parameters: [("chatId", String(describing: chatId)), ("userId", String(describing: userId)), ("fwdLimit", String(describing: fwdLimit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.InvitedUsers? in let reader = BufferReader(buffer) - var result: Api.Updates? + var result: Api.messages.InvitedUsers? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Updates + result = Api.parse(reader, signature: signature) as? Api.messages.InvitedUsers } return result }) @@ -4782,9 +4782,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func createChat(flags: Int32, users: [Api.InputUser], title: String, ttlPeriod: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func createChat(flags: Int32, users: [Api.InputUser], title: String, ttlPeriod: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(3450904) + buffer.appendInt32(-1831936556) serializeInt32(flags, buffer: buffer, boxed: false) buffer.appendInt32(481674261) buffer.appendInt32(Int32(users.count)) @@ -4793,11 +4793,11 @@ public extension Api.functions.messages { } serializeString(title, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(ttlPeriod!, buffer: buffer, boxed: false)} - return (FunctionDescription(name: "messages.createChat", parameters: [("flags", String(describing: flags)), ("users", String(describing: users)), ("title", String(describing: title)), ("ttlPeriod", String(describing: ttlPeriod))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "messages.createChat", parameters: [("flags", String(describing: flags)), ("users", String(describing: users)), ("title", String(describing: title)), ("ttlPeriod", String(describing: ttlPeriod))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.InvitedUsers? in let reader = BufferReader(buffer) - var result: Api.Updates? + var result: Api.messages.InvitedUsers? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Updates + result = Api.parse(reader, signature: signature) as? Api.messages.InvitedUsers } return result }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 7fe57fd38c..b3e4c2f189 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -227,7 +227,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) @@ -258,8 +258,6 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { if let boostPeer = boostPeer { result.append(boostPeer.peerId) } - case .messageActionRequestedPeerSentMe: - break } return result diff --git a/submodules/TelegramCore/Sources/MacOS/GroupReturnAndLeft.swift b/submodules/TelegramCore/Sources/MacOS/GroupReturnAndLeft.swift index 452366babe..8b13789179 100644 --- a/submodules/TelegramCore/Sources/MacOS/GroupReturnAndLeft.swift +++ b/submodules/TelegramCore/Sources/MacOS/GroupReturnAndLeft.swift @@ -1,40 +1 @@ -import Postbox -import SwiftSignalKit -import TelegramApi -import MtProtoKit - -public func returnGroup(account: Account, peerId: PeerId) -> Signal { - return account.postbox.loadedPeerWithId(account.peerId) - |> take(1) - |> mapToSignal { peer -> Signal in - if let inputUser = apiInputUser(peer) { - return account.network.request(Api.functions.messages.addChatUser(chatId: peerId.id._internalGetInt64Value(), userId: inputUser, fwdLimit: 50)) - |> retryRequest - |> mapToSignal { updates -> Signal in - account.stateManager.addUpdates(updates) - return .complete() - } - } else { - return .complete() - } - } -} - -public func leftGroup(account: Account, peerId: PeerId) -> Signal { - return account.postbox.loadedPeerWithId(account.peerId) - |> take(1) - |> mapToSignal { peer -> Signal in - if let inputUser = apiInputUser(peer) { - return account.network.request(Api.functions.messages.deleteChatUser(flags: 0, chatId: peerId.id._internalGetInt64Value(), userId: inputUser)) - |> retryRequest - |> mapToSignal { updates -> Signal in - account.stateManager.addUpdates(updates) - return .complete() - } - } else { - return .complete() - } - } -} - diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index f75e9026b4..1e9d4f8d64 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -351,6 +351,9 @@ public final class AccountViewTracker { private var quickRepliesUpdateDisposable: Disposable? private var quickRepliesUpdateTimestamp: Double = 0.0 + private var businessLinksUpdateDisposable: Disposable? + private var businessLinksUpdateTimestamp: Double = 0.0 + init(account: Account) { self.account = account self.accountPeerId = account.peerId @@ -377,6 +380,7 @@ public final class AccountViewTracker { self.updatedReactionsDisposables.dispose() self.externallyUpdatedPeerIdDisposable.dispose() self.quickRepliesUpdateDisposable?.dispose() + self.businessLinksUpdateDisposable?.dispose() } func reset() { @@ -1468,7 +1472,7 @@ public final class AccountViewTracker { if i < slice.count { let value = result[i] transaction.updatePeerCachedData(peerIds: Set([slice[i].0]), update: { _, cachedData in - var cachedData = cachedData as? CachedUserData ?? CachedUserData(about: nil, botInfo: nil, editableBotInfo: nil, peerStatusSettings: nil, pinnedMessageId: nil, isBlocked: false, commonGroupCount: 0, voiceCallsAvailable: true, videoCallsAvailable: true, callsPrivate: true, canPinMessages: true, hasScheduledMessages: true, autoremoveTimeout: .unknown, themeEmoticon: nil, photo: .unknown, personalPhoto: .unknown, fallbackPhoto: .unknown, premiumGiftOptions: [], voiceMessagesAvailable: true, wallpaper: nil, flags: [], businessHours: nil, businessLocation: nil, greetingMessage: nil, awayMessage: nil, connectedBot: nil, businessIntro: .unknown, birthday: nil) + var cachedData = cachedData as? CachedUserData ?? CachedUserData(about: nil, botInfo: nil, editableBotInfo: nil, peerStatusSettings: nil, pinnedMessageId: nil, isBlocked: false, commonGroupCount: 0, voiceCallsAvailable: true, videoCallsAvailable: true, callsPrivate: true, canPinMessages: true, hasScheduledMessages: true, autoremoveTimeout: .unknown, themeEmoticon: nil, photo: .unknown, personalPhoto: .unknown, fallbackPhoto: .unknown, premiumGiftOptions: [], voiceMessagesAvailable: true, wallpaper: nil, flags: [], businessHours: nil, businessLocation: nil, greetingMessage: nil, awayMessage: nil, connectedBot: nil, businessIntro: .unknown, birthday: nil, personalChannel: .unknown) var flags = cachedData.flags if case .boolTrue = value { flags.insert(.premiumRequired) @@ -2595,6 +2599,20 @@ public final class AccountViewTracker { } } } + + public func keepBusinessLinksApproximatelyUpdated() { + self.queue.async { + guard let account = self.account else { + return + } + let timestamp = CFAbsoluteTimeGetCurrent() + if self.businessLinksUpdateTimestamp + 16 * 60 * 60 < timestamp { + self.businessLinksUpdateTimestamp = timestamp + self.businessLinksUpdateDisposable?.dispose() + self.businessLinksUpdateDisposable = _internal_refreshBusinessChatLinks(postbox: account.postbox, network: account.network, accountPeerId: account.peerId).startStrict() + } + } + } } public final class ExtractedChatListItemCachedData: Hashable { diff --git a/submodules/TelegramCore/Sources/State/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift index 9585f79863..6ee4a6c822 100644 --- a/submodules/TelegramCore/Sources/State/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -60,7 +60,7 @@ enum CallSessionInternalState { case requesting(a: Data, disposable: Disposable) case requested(id: Int64, accessHash: Int64, a: Data, gA: Data, config: SecretChatEncryptionConfig, remoteConfirmationTimestamp: Int32?) case confirming(id: Int64, accessHash: Int64, key: Data, keyId: Int64, keyVisualHash: Data, disposable: Disposable) - case active(id: Int64, accessHash: Int64, beginTimestamp: Int32, key: Data, keyId: Int64, keyVisualHash: Data, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: [String: Any], allowsP2P: Bool) + case active(id: Int64, accessHash: Int64, beginTimestamp: Int32, key: Data, keyId: Int64, keyVisualHash: Data, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool) case dropping(reason: CallSessionTerminationReason, disposable: Disposable) case terminated(id: Int64?, accessHash: Int64?, reason: CallSessionTerminationReason, reportRating: Bool, sendDebugLogs: Bool) @@ -143,7 +143,7 @@ public enum CallSessionState { case ringing case accepting case requesting(ringing: Bool) - case active(id: CallId, key: Data, keyVisualHash: Data, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: [String: Any], allowsP2P: Bool) + case active(id: CallId, key: Data, keyVisualHash: Data, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool) case dropping(reason: CallSessionTerminationReason) case terminated(id: CallId?, reason: CallSessionTerminationReason, options: CallTerminationOptions) @@ -884,7 +884,7 @@ private final class CallSessionManagerContext { //assertionFailure() } } - case let .phoneCall(flags, id, _, _, _, _, gAOrB, keyFingerprint, callProtocol, connections, startDate, _): + case let .phoneCall(flags, id, _, _, _, _, gAOrB, keyFingerprint, callProtocol, connections, startDate, customParameters): let allowsP2P = (flags & (1 << 5)) != 0 if let internalId = self.contextIdByStableId[id] { if let context = self.contexts[internalId] { @@ -897,12 +897,18 @@ private final class CallSessionManagerContext { switch callProtocol { case let .phoneCallProtocol(_, _, maxLayer, versions): if !versions.isEmpty { - let customParameters: [String: Any] = [:] + var customParametersValue: String? + switch customParameters { + case .none: + break + case let .dataJSON(data): + customParametersValue = data + } let isVideoPossible = self.videoVersions().contains(where: { versions.contains($0) }) context.isVideoPossible = isVideoPossible - context.state = .active(id: id, accessHash: accessHash, beginTimestamp: startDate, key: key, keyId: calculatedKeyId, keyVisualHash: keyVisualHash, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParameters, allowsP2P: allowsP2P) + context.state = .active(id: id, accessHash: accessHash, beginTimestamp: startDate, key: key, keyId: calculatedKeyId, keyVisualHash: keyVisualHash, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: allowsP2P) self.contextUpdated(internalId: internalId) } else { self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) @@ -918,12 +924,18 @@ private final class CallSessionManagerContext { switch callProtocol { case let .phoneCallProtocol(_, _, maxLayer, versions): if !versions.isEmpty { - let customParameters: [String: Any] = [:] + var customParametersValue: String? + switch customParameters { + case .none: + break + case let .dataJSON(data): + customParametersValue = data + } let isVideoPossible = self.videoVersions().contains(where: { versions.contains($0) }) context.isVideoPossible = isVideoPossible - context.state = .active(id: id, accessHash: accessHash, beginTimestamp: startDate, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParameters, allowsP2P: allowsP2P) + context.state = .active(id: id, accessHash: accessHash, beginTimestamp: startDate, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: allowsP2P) self.contextUpdated(internalId: internalId) } else { self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) @@ -1203,7 +1215,7 @@ public final class CallSessionManager { private enum AcceptedCall { case waiting(config: SecretChatEncryptionConfig) - case call(config: SecretChatEncryptionConfig, gA: Data, timestamp: Int32, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: [String: Any], allowsP2P: Bool) + case call(config: SecretChatEncryptionConfig, gA: Data, timestamp: Int32, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool) } private enum AcceptCallResult { @@ -1243,13 +1255,20 @@ private func acceptCallSession(accountPeerId: PeerId, postbox: Postbox, network: return .failed case .phoneCallWaiting: return .success(.waiting(config: config)) - case let .phoneCall(flags, id, _, _, _, _, gAOrB, _, callProtocol, connections, startDate, _): + case let .phoneCall(flags, id, _, _, _, _, gAOrB, _, callProtocol, connections, startDate, customParameters): if id == stableId { switch callProtocol{ case let .phoneCallProtocol(_, _, maxLayer, versions): if !versions.isEmpty { - let customParameters: [String: Any] = [:] - return .success(.call(config: config, gA: gAOrB.makeData(), timestamp: startDate, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParameters, allowsP2P: (flags & (1 << 5)) != 0)) + var customParametersValue: String? + switch customParameters { + case .none: + break + case let .dataJSON(data): + customParametersValue = data + } + + return .success(.call(config: config, gA: gAOrB.makeData(), timestamp: startDate, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: (flags & (1 << 5)) != 0)) } else { return .failed } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 492780c4bf..7d4a3c8207 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import TelegramApi +import SwiftSignalKit public enum CachedPeerAutoremoveTimeout: Equatable, PostboxCoding { public struct Value: Equatable, PostboxCoding { @@ -106,6 +107,69 @@ public enum CachedTelegramBusinessIntro: Equatable, PostboxCoding { } } +public final class TelegramPersonalChannel: Equatable, Codable { + public let peerId: PeerId + public let subscriberCount: Int32? + public let topMessageId: Int32? + + public init(peerId: PeerId, subscriberCount: Int32?, topMessageId: Int32?) { + self.peerId = peerId + self.subscriberCount = subscriberCount + self.topMessageId = topMessageId + } + + public static func ==(lhs: TelegramPersonalChannel, rhs: TelegramPersonalChannel) -> Bool { + if lhs === rhs { + return true + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.subscriberCount != rhs.subscriberCount { + return false + } + if lhs.topMessageId != rhs.topMessageId { + return false + } + return true + } +} + +public enum CachedTelegramPersonalChannel: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case discriminator = "d" + case value = "v" + } + + case unknown + case known(TelegramPersonalChannel?) + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + switch try container.decode(Int32.self, forKey: .discriminator) { + case 0: + self = .unknown + case 1: + self = .known(try container.decodeIfPresent(TelegramPersonalChannel.self, forKey: .value)) + default: + self = .unknown + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .unknown: + try container.encode(0 as Int32, forKey: .discriminator) + case let .known(value): + try container.encode(1 as Int32, forKey: .discriminator) + try container.encodeIfPresent(value, forKey: .value) + } + } +} + public struct CachedPremiumGiftOption: Equatable, PostboxCoding { public let months: Int32 public let currency: String @@ -481,6 +545,84 @@ extension TelegramBusinessLocation { } } +public final class TelegramBusinessChatLinks: Codable, Equatable { + public final class Link: Codable, Equatable { + public let url: String + public let message: String + public let entities: [MessageTextEntity] + public let title: String? + public let viewCount: Int32 + + public init(url: String, message: String, entities: [MessageTextEntity], title: String?, viewCount: Int32) { + self.url = url + self.message = message + self.entities = entities + self.title = title + self.viewCount = viewCount + } + + public static func ==(lhs: Link, rhs: Link) -> Bool { + if lhs === rhs { + return true + } + if lhs.url != rhs.url { + return false + } + if lhs.message != rhs.message { + return false + } + if lhs.entities != rhs.entities { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.viewCount != rhs.viewCount { + return false + } + return true + } + } + + public let links: [Link] + + public init(links: [Link]) { + self.links = links + } + + public static func ==(lhs: TelegramBusinessChatLinks, rhs: TelegramBusinessChatLinks) -> Bool { + if lhs === rhs { + return true + } + if lhs.links != rhs.links { + return false + } + return true + } +} + +extension TelegramBusinessChatLinks.Link { + convenience init(apiLink: Api.BusinessChatLink) { + switch apiLink { + case let .businessChatLink(_, link, message, entities, title, views): + self.init(url: link, message: message, entities: messageTextEntitiesFromApiEntities(entities ?? []), title: title, viewCount: views) + } + } +} + +extension TelegramBusinessChatLinks { + static func fromApiLinks(apiLinks: Api.account.BusinessChatLinks) -> (result: TelegramBusinessChatLinks, users: [Api.User], chats: [Api.Chat]) { + switch apiLinks { + case let .businessChatLinks(links, chats, users): + return ( + TelegramBusinessChatLinks(links: links.map(Link.init(apiLink:))), + users, + chats + ) + } + } +} + public final class CachedUserData: CachedPeerData { public let about: String? public let botInfo: BotInfo? @@ -510,6 +652,7 @@ public final class CachedUserData: CachedPeerData { public let connectedBot: TelegramAccountConnectedBot? public let businessIntro: CachedTelegramBusinessIntro public let birthday: TelegramBirthday? + public let personalChannel: CachedTelegramPersonalChannel public let peerIds: Set public let messageIds: Set @@ -546,9 +689,10 @@ public final class CachedUserData: CachedPeerData { self.connectedBot = nil self.businessIntro = .unknown self.birthday = nil + self.personalChannel = .unknown } - public init(about: String?, botInfo: BotInfo?, editableBotInfo: EditableBotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: CachedPeerProfilePhoto, personalPhoto: CachedPeerProfilePhoto, fallbackPhoto: CachedPeerProfilePhoto, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool, wallpaper: TelegramWallpaper?, flags: CachedUserFlags, businessHours: TelegramBusinessHours?, businessLocation: TelegramBusinessLocation?, greetingMessage: TelegramBusinessGreetingMessage?, awayMessage: TelegramBusinessAwayMessage?, connectedBot: TelegramAccountConnectedBot?, businessIntro: CachedTelegramBusinessIntro, birthday: TelegramBirthday?) { + public init(about: String?, botInfo: BotInfo?, editableBotInfo: EditableBotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: CachedPeerProfilePhoto, personalPhoto: CachedPeerProfilePhoto, fallbackPhoto: CachedPeerProfilePhoto, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool, wallpaper: TelegramWallpaper?, flags: CachedUserFlags, businessHours: TelegramBusinessHours?, businessLocation: TelegramBusinessLocation?, greetingMessage: TelegramBusinessGreetingMessage?, awayMessage: TelegramBusinessAwayMessage?, connectedBot: TelegramAccountConnectedBot?, businessIntro: CachedTelegramBusinessIntro, birthday: TelegramBirthday?, personalChannel: CachedTelegramPersonalChannel) { self.about = about self.botInfo = botInfo self.editableBotInfo = editableBotInfo @@ -577,6 +721,7 @@ public final class CachedUserData: CachedPeerData { self.connectedBot = connectedBot self.businessIntro = businessIntro self.birthday = birthday + self.personalChannel = personalChannel self.peerIds = Set() @@ -639,6 +784,7 @@ public final class CachedUserData: CachedPeerData { self.businessIntro = decoder.decodeObjectForKey("businessIntro", decoder: CachedTelegramBusinessIntro.init(decoder:)) as? CachedTelegramBusinessIntro ?? .unknown self.birthday = decoder.decodeCodable(TelegramBirthday.self, forKey: "bday") + self.personalChannel = decoder.decodeCodable(CachedTelegramPersonalChannel.self, forKey: "pchan") ?? .unknown } public func encode(_ encoder: PostboxEncoder) { @@ -737,6 +883,8 @@ public final class CachedUserData: CachedPeerData { } else { encoder.encodeNil(forKey: "bday") } + + encoder.encodeCodable(personalChannel, forKey: "pchan") } public func isEqual(to: CachedPeerData) -> Bool { @@ -771,119 +919,256 @@ public final class CachedUserData: CachedPeerData { if other.birthday != self.birthday { return false } + if other.personalChannel != self.personalChannel { + return false + } return other.about == self.about && other.botInfo == self.botInfo && other.editableBotInfo == self.editableBotInfo && self.peerStatusSettings == other.peerStatusSettings && self.isBlocked == other.isBlocked && self.commonGroupCount == other.commonGroupCount && self.voiceCallsAvailable == other.voiceCallsAvailable && self.videoCallsAvailable == other.videoCallsAvailable && self.callsPrivate == other.callsPrivate && self.hasScheduledMessages == other.hasScheduledMessages && self.autoremoveTimeout == other.autoremoveTimeout && self.themeEmoticon == other.themeEmoticon && self.photo == other.photo && self.personalPhoto == other.personalPhoto && self.fallbackPhoto == other.fallbackPhoto && self.premiumGiftOptions == other.premiumGiftOptions && self.voiceMessagesAvailable == other.voiceMessagesAvailable && self.flags == other.flags && self.wallpaper == other.wallpaper } public func withUpdatedAbout(_ about: String?) -> CachedUserData { - return CachedUserData(about: about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedBotInfo(_ botInfo: BotInfo?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedEditableBotInfo(_ editableBotInfo: EditableBotInfo?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedIsBlocked(_ isBlocked: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedCommonGroupCount(_ commonGroupCount: Int32) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedVoiceCallsAvailable(_ voiceCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedVideoCallsAvailable(_ videoCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedCallsPrivate(_ callsPrivate: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedCanPinMessages(_ canPinMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedPhoto(_ photo: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedPersonalPhoto(_ personalPhoto: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedFallbackPhoto(_ fallbackPhoto: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedPremiumGiftOptions(_ premiumGiftOptions: [CachedPremiumGiftOption]) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedVoiceMessagesAvailable(_ voiceMessagesAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedWallpaper(_ wallpaper: TelegramWallpaper?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedFlags(_ flags: CachedUserFlags) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedBusinessHours(_ businessHours: TelegramBusinessHours?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedBusinessLocation(_ businessLocation: TelegramBusinessLocation?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedGreetingMessage(_ greetingMessage: TelegramBusinessGreetingMessage?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedAwayMessage(_ awayMessage: TelegramBusinessAwayMessage?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedConnectedBot(_ connectedBot: TelegramAccountConnectedBot?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: connectedBot, businessIntro: self.businessIntro, birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedBusinessIntro(_ businessIntro: TelegramBusinessIntro?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: .known(businessIntro), birthday: self.birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: .known(businessIntro), birthday: self.birthday, personalChannel: self.personalChannel) } public func withUpdatedBirthday(_ birthday: TelegramBirthday?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: birthday) + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: birthday, personalChannel: self.personalChannel) + } + + public func withUpdatedPersonalChannel(_ personalChannel: TelegramPersonalChannel?) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, editableBotInfo: self.editableBotInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable, wallpaper: self.wallpaper, flags: self.flags, businessHours: self.businessHours, businessLocation: self.businessLocation, greetingMessage: self.greetingMessage, awayMessage: self.awayMessage, connectedBot: self.connectedBot, businessIntro: self.businessIntro, birthday: self.birthday, personalChannel: .known(personalChannel)) + } +} + +public enum AddBusinessChatLinkError { + case generic + case tooManyLinks +} + +func _internal_updateBusinessChatLinks(transaction: Transaction, _ f: ([TelegramBusinessChatLinks.Link]) -> [TelegramBusinessChatLinks.Link]) { + let current = transaction.getPreferencesEntry(key: PreferencesKeys.businessLinks())?.get(TelegramBusinessChatLinks.self) + transaction.setPreferencesEntry(key: PreferencesKeys.businessLinks(), value: PreferencesEntry(TelegramBusinessChatLinks(links: f(current?.links ?? [])))) +} + +func _internal_createBusinessChatLink(account: Account, message: String, entities: [MessageTextEntity], title: String?) -> Signal { + var flags: Int32 = 0 + + var apiEntities: [Api.MessageEntity]? + if !entities.isEmpty { + apiEntities = apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()) + flags |= 1 << 0 + } + + if title != nil { + flags |= 1 << 1 + } + + return account.network.request(Api.functions.account.createBusinessChatLink(link: .inputBusinessChatLink(flags: flags, message: message, entities: apiEntities, title: title))) + |> mapError { error -> AddBusinessChatLinkError in + if error.errorDescription == "CHATLINKS_TOO_MUCH" { + return .tooManyLinks + } else { + return .generic + } + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> TelegramBusinessChatLinks.Link in + let link = TelegramBusinessChatLinks.Link(apiLink: result) + + _internal_updateBusinessChatLinks(transaction: transaction, { list in + var list = list + if let index = list.firstIndex(where: { $0.url == link.url }) { + list.remove(at: index) + } + list.append(link) + return list + }) + + return link + } + |> castError(AddBusinessChatLinkError.self) + } +} + +func _internal_editBusinessChatLink(account: Account, url: String, message: String, entities: [MessageTextEntity], title: String?) -> Signal { + var flags: Int32 = 0 + + var apiEntities: [Api.MessageEntity]? + if !entities.isEmpty { + apiEntities = apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()) + flags |= 1 << 0 + } + + if title != nil { + flags |= 1 << 1 + } + + return account.network.request(Api.functions.account.editBusinessChatLink(slug: url, link: .inputBusinessChatLink(flags: flags, message: message, entities: apiEntities, title: title))) + |> mapError { _ -> AddBusinessChatLinkError in + return .generic + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> TelegramBusinessChatLinks.Link in + let link = TelegramBusinessChatLinks.Link(apiLink: result) + + _internal_updateBusinessChatLinks(transaction: transaction, { list in + var list = list + if let index = list.firstIndex(where: { $0.url == link.url }) { + list[index] = link + } else { + list.append(link) + } + return list + }) + + return link + } + |> castError(AddBusinessChatLinkError.self) + } +} + +func _internal_deleteBusinessChatLink(account: Account, url: String) -> Signal { + let remoteApply = account.network.request(Api.functions.account.deleteBusinessChatLink(slug: url)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues + + return account.postbox.transaction { transaction -> Void in + _internal_updateBusinessChatLinks(transaction: transaction, { list in + var list = list + if let index = list.firstIndex(where: { $0.url == url }) { + list.remove(at: index) + } + return list + }) + } + |> ignoreValues + |> then(remoteApply) +} + +func _internal_refreshBusinessChatLinks(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal { + return network.request(Api.functions.account.getBusinessChatLinks()) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + return postbox.transaction { transaction in + let parsedResult = TelegramBusinessChatLinks.fromApiLinks(apiLinks: result) + let peers = AccumulatedPeers(transaction: transaction, chats: parsedResult.chats, users: parsedResult.users) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers) + + _internal_updateBusinessChatLinks(transaction: transaction, { _ in + return parsedResult.result.links + }) + } + |> ignoreValues } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index afe17a9427..7ae701faa7 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -287,6 +287,7 @@ private enum PreferencesKeyValues: Int32 { case shortcutMessages = 37 case timezoneList = 38 case botBiometricsState = 39 + case businessLinks = 40 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -489,6 +490,12 @@ public struct PreferencesKeys { key.setInt64(4, value: peerId.toInt64()) return key } + + public static func businessLinks() -> ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.businessLinks.rawValue) + return key + } } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index 8ae097ea93..94b48fbc84 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -213,5 +213,25 @@ public extension TelegramEngine { public func updateBusinessIntro(intro: TelegramBusinessIntro?) -> Signal { return _internal_updateBusinessIntro(account: self.account, intro: intro) } + + public func createBusinessChatLink(message: String, entities: [MessageTextEntity], title: String?) -> Signal { + return _internal_createBusinessChatLink(account: self.account, message: message, entities: entities, title: title) + } + + public func editBusinessChatLink(url: String, message: String, entities: [MessageTextEntity], title: String?) -> Signal { + return _internal_editBusinessChatLink(account: self.account, url: url, message: message, entities: entities, title: title) + } + + public func deleteBusinessChatLink(url: String) -> Signal { + return _internal_deleteBusinessChatLink(account: self.account, url: url) + } + + public func refreshBusinessChatLinks() -> Signal { + return _internal_refreshBusinessChatLinks(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId) + } + + public func updatePersonalChannel(personalChannel: TelegramPersonalChannel?) -> Signal { + return _internal_updatePersonalChannel(account: self.account, personalChannel: personalChannel) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 3d5841db66..314b2aa080 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1760,5 +1760,29 @@ public extension TelegramEngine.EngineData.Item { } } } + + public struct BusinessChatLinks: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = TelegramBusinessChatLinks? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .preferences(keys: Set([PreferencesKeys.businessLinks()])) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? PreferencesView else { + preconditionFailure() + } + return view.values[PreferencesKeys.businessLinks()]?.get(TelegramBusinessChatLinks.self) + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index cfa4f55a35..0144b0d438 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -1093,3 +1093,35 @@ public func _internal_setAccountConnectedBot(account: Account, bot: TelegramAcco |> ignoreValues |> then(remoteApply) } + +func _internal_updatePersonalChannel(account: Account, personalChannel: TelegramPersonalChannel?) -> Signal { + let remoteApply = account.postbox.transaction { transaction -> Peer? in + guard let personalChannel else { + return nil + } + return ( + transaction.getPeer(personalChannel.peerId) + ) + } + |> mapToSignal { peer in + let inputPeer = peer.flatMap(apiInputChannel) + + return account.network.request(Api.functions.account.updatePersonalChannel(channel: inputPeer ?? .inputChannelEmpty)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + + return account.postbox.transaction { transaction in + transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in + var current = (current as? CachedUserData) ?? CachedUserData() + current = current.withUpdatedPersonalChannel(personalChannel) + return current + }) + } + |> ignoreValues + |> then(remoteApply) +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift index de47654b99..d97aeb1146 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift @@ -33,9 +33,16 @@ func _internal_addGroupMember(account: Account, peerId: PeerId, memberId: PeerId } } |> mapToSignal { result -> Signal in - account.stateManager.addUpdates(result) + let updatesValue: Api.Updates + switch result { + case let .invitedUsers(updates, missingInvitees): + let _ = missingInvitees + updatesValue = updates + } + + account.stateManager.addUpdates(updatesValue) return account.postbox.transaction { transaction -> Void in - if let message = result.messages.first, let timestamp = message.timestamp { + if let message = updatesValue.messages.first, let timestamp = message.timestamp { transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in if let cachedData = cachedData as? CachedGroupData, let participants = cachedData.participants { var updatedParticipants = participants.participants @@ -94,8 +101,7 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: nil, banInfo: nil, rank: nil) } return account.network.request(Api.functions.channels.inviteToChannel(channel: inputChannel, users: [inputUser])) - |> map { [$0] } - |> `catch` { error -> Signal<[Api.Updates], AddChannelMemberError> in + |> `catch` { error -> Signal in switch error.errorDescription { case "USER_CHANNELS_TOO_MUCH": return .fail(.tooMuchJoined) @@ -118,9 +124,14 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer } } |> mapToSignal { result -> Signal<(ChannelParticipant?, RenderedChannelParticipant), AddChannelMemberError> in - for updates in result { - account.stateManager.addUpdates(updates) + let updatesValue: Api.Updates + switch result { + case let .invitedUsers(updates, missingInvitees): + let _ = missingInvitees + updatesValue = updates } + + account.stateManager.addUpdates(updatesValue) return account.postbox.transaction { transaction -> (ChannelParticipant?, RenderedChannelParticipant) in transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in if let cachedData = cachedData as? CachedChannelData, let memberCount = cachedData.participantsSummary.memberCount, let kickedCount = cachedData.participantsSummary.kickedCount { @@ -214,7 +225,14 @@ func _internal_addChannelMembers(account: Account, peerId: PeerId, memberIds: [P } } |> map { result in - account.stateManager.addUpdates(result) + let updatesValue: Api.Updates + switch result { + case let .invitedUsers(updates, missingInvitees): + let _ = missingInvitees + updatesValue = updates + } + + account.stateManager.addUpdates(updatesValue) account.viewTracker.forceUpdateCachedPeerData(peerId: peerId) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift index 50b317f946..3f7f163421 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift @@ -506,9 +506,33 @@ public enum AdminedPublicChannelsScope { case all case forLocation case forVoiceChat + case forPersonalProfile } -func _internal_adminedPublicChannels(account: Account, scope: AdminedPublicChannelsScope = .all) -> Signal<[Peer], NoError> { +public final class TelegramAdminedPublicChannel: Equatable { + public let peer: EnginePeer + public let subscriberCount: Int? + + public init(peer: EnginePeer, subscriberCount: Int?) { + self.peer = peer + self.subscriberCount = subscriberCount + } + + public static func ==(lhs: TelegramAdminedPublicChannel, rhs: TelegramAdminedPublicChannel) -> Bool { + if lhs === rhs { + return true + } + if lhs.peer != rhs.peer { + return false + } + if lhs.subscriberCount != rhs.subscriberCount { + return false + } + return true + } +} + +func _internal_adminedPublicChannels(account: Account, scope: AdminedPublicChannelsScope = .all) -> Signal<[TelegramAdminedPublicChannel], NoError> { var flags: Int32 = 0 switch scope { case .all: @@ -517,28 +541,39 @@ func _internal_adminedPublicChannels(account: Account, scope: AdminedPublicChann flags |= (1 << 0) case .forVoiceChat: flags |= (1 << 2) + case .forPersonalProfile: + flags |= (1 << 2) } let accountPeerId = account.peerId return account.network.request(Api.functions.channels.getAdminedPublicChannels(flags: flags)) |> retryRequest - |> mapToSignal { result -> Signal<[Peer], NoError> in - return account.postbox.transaction { transaction -> [Peer] in + |> mapToSignal { result -> Signal<[TelegramAdminedPublicChannel], NoError> in + return account.postbox.transaction { transaction -> [TelegramAdminedPublicChannel] in let chats: [Api.Chat] + var subscriberCounts: [PeerId: Int] = [:] let parsedPeers: AccumulatedPeers switch result { case let .chats(apiChats): chats = apiChats + for chat in apiChats { + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _) = chat { + subscriberCounts[chat.peerId] = participantsCount.flatMap(Int.init) + } + } case let .chatsSlice(_, apiChats): chats = apiChats } parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: []) updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) - var peers: [Peer] = [] + var peers: [TelegramAdminedPublicChannel] = [] for chat in chats { if let peer = transaction.getPeer(chat.peerId) { - peers.append(peer) + peers.append(TelegramAdminedPublicChannel( + peer: EnginePeer(peer), + subscriberCount: subscriberCounts[peer.id] + )) } } return peers diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateGroup.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateGroup.swift index da961ee37c..88fbe49ed5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateGroup.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateGroup.swift @@ -49,22 +49,19 @@ func _internal_createGroup(account: Account, title: String, peerIds: [PeerId], t } return .generic } - |> mapToSignal { updates -> Signal in + |> mapToSignal { result -> Signal in var failedToInvitePeerIds: [EnginePeer.Id] = [] failedToInvitePeerIds = [] - switch updates { - case let .updates(updates, _, _, _, _): - for update in updates { - if case let .updateGroupInvitePrivacyForbidden(userId) = update { - failedToInvitePeerIds.append(EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(userId))) - } - } - default: - break + + let updatesValue: Api.Updates + switch result { + case let .invitedUsers(updates, missingInvitees): + let _ = missingInvitees + updatesValue = updates } - account.stateManager.addUpdates(updates) - if let message = updates.messages.first, let peerId = apiMessagePeerId(message) { + account.stateManager.addUpdates(updatesValue) + if let message = updatesValue.messages.first, let peerId = apiMessagePeerId(message) { return account.postbox.multiplePeersView([peerId]) |> filter { view in return view.peers[peerId] != nil diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 6b35401d3f..c2e186eef8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -83,6 +83,18 @@ public final class TelegramCollectibleItemInfo: Equatable { } } +public final class TelegramResolvedMessageLink { + public let peer: EnginePeer + public let message: String + public let entities: [MessageTextEntity] + + public init(peer: EnginePeer, message: String, entities: [MessageTextEntity]) { + self.peer = peer + self.message = message + self.entities = entities + } +} + public extension TelegramEngine { enum NextUnreadChannelLocation: Equatable { case same @@ -122,11 +134,8 @@ public extension TelegramEngine { return _internal_checkPublicChannelCreationAvailability(account: self.account, location: location) } - public func adminedPublicChannels(scope: AdminedPublicChannelsScope = .all) -> Signal<[EnginePeer], NoError> { + public func adminedPublicChannels(scope: AdminedPublicChannelsScope = .all) -> Signal<[TelegramAdminedPublicChannel], NoError> { return _internal_adminedPublicChannels(account: self.account, scope: scope) - |> map { peers -> [EnginePeer] in - return peers.map(EnginePeer.init) - } } public func channelsForStories() -> Signal<[EnginePeer], NoError> { @@ -1502,6 +1511,35 @@ public extension TelegramEngine { public func removeChatManagingBot(chatId: EnginePeer.Id) { let _ = _internal_removeChatManagingBot(account: self.account, chatId: chatId).startStandalone() } + + public func resolveMessageLink(slug: String) -> Signal { + return self.account.network.request(Api.functions.account.resolveBusinessChatLink(slug: slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .single(nil) + } + return self.account.postbox.transaction { transaction -> TelegramResolvedMessageLink? in + switch result { + case let .resolvedBusinessChatLinks(_, peer, message, entities, chats, users): + updatePeers(transaction: transaction, accountPeerId: self.account.peerId, peers: AccumulatedPeers(transaction: transaction, chats: chats, users: users)) + + guard let peer = transaction.getPeer(peer.peerId) else { + return nil + } + + return TelegramResolvedMessageLink( + peer: EnginePeer(peer), + message: message, + entities: messageTextEntitiesFromApiEntities(entities ?? []) + ) + } + } + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 820c9e3dcf..1787f971d7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -258,7 +258,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee previous = CachedUserData() } switch fullUser { - case let .userFull(userFullFlags, _, _, userFullAbout, userFullSettings, personalPhoto, profilePhoto, fallbackPhoto, _, userFullBotInfo, userFullPinnedMsgId, userFullCommonChatsCount, _, userFullTtlPeriod, userFullThemeEmoticon, _, _, _, userPremiumGiftOptions, userWallpaper, stories, businessWorkHours, businessLocation, greetingMessage, awayMessage, businessIntro, birthday, _, _): + case let .userFull(userFullFlags, _, _, userFullAbout, userFullSettings, personalPhoto, profilePhoto, fallbackPhoto, _, userFullBotInfo, userFullPinnedMsgId, userFullCommonChatsCount, _, userFullTtlPeriod, userFullThemeEmoticon, _, _, _, userPremiumGiftOptions, userWallpaper, stories, businessWorkHours, businessLocation, greetingMessage, awayMessage, businessIntro, birthday, personalChannelId, personalChannelMessage): let _ = stories let botInfo = userFullBotInfo.flatMap(BotInfo.init(apiBotInfo:)) let isBlocked = (userFullFlags & (1 << 0)) != 0 @@ -345,6 +345,27 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee if let birthday { mappedBirthday = TelegramBirthday(apiBirthday: birthday) } + + var personalChannel: TelegramPersonalChannel? + if let personalChannelId { + let channelPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(personalChannelId)) + + var subscriberCount: Int32? + for chat in chats { + if chat.peerId == channelPeerId { + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _) = chat { + subscriberCount = participantsCount + } + } + } + + personalChannel = TelegramPersonalChannel( + peerId: channelPeerId, + subscriberCount: subscriberCount, + topMessageId: personalChannelMessage + ) + } + return previous.withUpdatedAbout(userFullAbout) .withUpdatedBotInfo(botInfo) .withUpdatedEditableBotInfo(editableBotInfo) @@ -373,6 +394,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedConnectedBot(mappedConnectedBot) .withUpdatedBusinessIntro(mappedBusinessIntro) .withUpdatedBirthday(mappedBirthday) + .withUpdatedPersonalChannel(personalChannel) } }) } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 7ff2f87a99..b0ceaec934 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -312,6 +312,8 @@ public enum PresentationResourceKey: Int32 { case avatarPremiumLockBadge case shareAvatarPremiumLockBadgeBackground case shareAvatarPremiumLockBadge + + case sharedLinkIcon } public enum ChatExpiredStoryIndicatorType: Hashable { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 1550da9838..8b3385903b 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -383,4 +383,23 @@ public struct PresentationResourcesItemList { return generateTintedImage(image: UIImage(bundleImageName: "Chart/Forwards"), color: theme.list.itemSecondaryTextColor) }) } + + public static func sharedLinkIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.sharedLinkIcon.rawValue, { theme in + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.list.itemCheckColors.fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.list.itemCheckColors.foregroundColor) { + image.draw(at: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5))) + } + }) + }) + } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 7722c07dca..5c3a579dab 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -442,6 +442,7 @@ swift_library( "//submodules/TelegramUI/Components/StickerPickerScreen", "//submodules/TelegramUI/Components/Chat/ChatEmptyNode", "//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem", + "//submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController", "//submodules/TelegramUI/Components/Ads/AdsInfoScreen", "//submodules/TelegramUI/Components/Ads/AdsReportScreen", ] + select({ diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index 9ddf95e13c..ef8be4e20f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -20,6 +20,7 @@ import BalancedTextComponent import Markdown import ReactionSelectionNode import ChatMediaInputStickerGridItem +import UndoUI private protocol ChatEmptyNodeContent { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize @@ -714,9 +715,16 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC private let titleNode: ImmediateTextNode private var lineNodes: [ImmediateTextNode] = [] + private var linkTextButton: HighlightTrackingButtonNode? + private var linkTextNode: ImmediateTextNode? + private var linkTextHighlightNode: LinkHighlightingNode? + private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? + private var businessLink: TelegramBusinessChatLinks.Link? + var shareBusinessLink: ((String) -> Void)? + override init() { self.iconNode = ASImageNode() self.iconNode.isLayerBacked = true @@ -736,6 +744,13 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC self.addSubnode(self.titleNode) } + @objc private func linkTextButtonPressed() { + guard let businessLink = self.businessLink else { + return + } + self.shareBusinessLink?(businessLink.url) + } + func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { var maxWidth: CGFloat = size.width var centerText = false @@ -744,6 +759,8 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC var imageSpacing: CGFloat = 12.0 var titleSpacing: CGFloat = 4.0 + let businessLinkTextSpacing: CGFloat = 9.0 + if case let .customChatContents(customChatContents) = interfaceState.subject { maxWidth = min(240.0, maxWidth) @@ -752,6 +769,10 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC insets.top = 10.0 imageSpacing = 5.0 titleSpacing = 5.0 + case .businessLinkSetup: + insets.top = -9.0 + imageSpacing = 4.0 + titleSpacing = 5.0 } } @@ -765,6 +786,9 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC let titleString: String let strings: [String] + var textFontSize: CGFloat = 14.0 + + var businessLink: String? if case let .customChatContents(customChatContents) = interfaceState.subject { switch customChatContents.kind { @@ -793,6 +817,22 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC interfaceState.strings.EmptyState_AwayMessage_Text ] } + case let .businessLinkSetup(link): + //TODO:localize + iconName = "Chat/Empty Chat/BusinessLink" + centerText = true + titleString = "Link to Chat" + textFontSize = 13.0 + strings = [ + "Add a message that will be entered in the message input field for anyone who starts a chat with you using this link:" + ] + if link.url.hasPrefix("https://") { + businessLink = String(link.url[link.url.index(link.url.startIndex, offsetBy: "https://".count)...]) + } else { + businessLink = link.url + } + + self.businessLink = link } } else { titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title @@ -810,9 +850,9 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC let lines: [NSAttributedString] = strings.map { return parseMarkdownIntoAttributedString($0, attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText), - bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: serviceColor.primaryText), - link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText), + body: MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: serviceColor.primaryText), + bold: MarkdownAttributeSet(font: Font.semibold(textFontSize), textColor: serviceColor.primaryText), + link: MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: serviceColor.primaryText), linkAttribute: { url in return ("URL", url) } @@ -832,6 +872,72 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC self.lineNodes[i].attributedText = lines[i] } + + if let businessLink { + let linkTextButton: HighlightTrackingButtonNode + if let current = self.linkTextButton { + linkTextButton = current + } else { + linkTextButton = HighlightTrackingButtonNode() + self.linkTextButton = linkTextButton + self.addSubnode(linkTextButton) + + linkTextButton.addTarget(self, action: #selector(self.linkTextButtonPressed), forControlEvents: .touchUpInside) + linkTextButton.highligthedChanged = { [weak linkTextButton] highlighted in + if let linkTextButton, linkTextButton.bounds.width > 0.0 { + let animateScale = true + + let topScale: CGFloat = (linkTextButton.bounds.width - 8.0) / linkTextButton.bounds.width + let maxScale: CGFloat = (linkTextButton.bounds.width + 2.0) / linkTextButton.bounds.width + + if highlighted { + linkTextButton.layer.removeAnimation(forKey: "transform.scale") + + if animateScale { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + transition.setScale(layer: linkTextButton.layer, scale: topScale) + } + } else { + if animateScale { + let transition = Transition(animation: .none) + transition.setScale(layer: linkTextButton.layer, scale: 1.0) + + linkTextButton.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak linkTextButton] _ in + guard let linkTextButton else { + return + } + + linkTextButton.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + } + } + + let linkTextNode: ImmediateTextNode + if let current = self.linkTextNode { + linkTextNode = current + } else { + linkTextNode = ImmediateTextNode() + linkTextNode.maximumNumberOfLines = 0 + linkTextNode.textAlignment = .center + linkTextNode.lineSpacing = 0.2 + self.linkTextNode = linkTextNode + linkTextButton.addSubnode(linkTextNode) + } + + linkTextNode.attributedText = NSAttributedString(string: businessLink, font: Font.medium(textFontSize), textColor: serviceColor.primaryText) + } else { + if let linkTextButton = self.linkTextButton { + self.linkTextButton = nil + linkTextButton.removeFromSupernode() + } + if let linkTextNode = self.linkTextNode { + self.linkTextNode = nil + linkTextNode.removeFromSupernode() + } + } } var contentWidth: CGFloat = 100.0 @@ -851,6 +957,14 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC lineNodes.append((textSize, textNode)) } + var linkTextLayout: TextNodeLayout? + if let linkTextNode { + //TODO:localize + let linkTextLayoutValue = linkTextNode.updateLayoutFullInfo(CGSize(width: maxWidth - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude)) + linkTextLayout = linkTextLayoutValue + contentHeight += businessLinkTextSpacing + linkTextLayoutValue.size.height + 20.0 + } + let titleSize = self.titleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) contentWidth = max(contentWidth, titleSize.width) @@ -870,10 +984,94 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC transition.updateFrame(node: self.titleNode, frame: titleFrame) var lineOffset = titleFrame.maxY + titleSpacing + var isFirstLine = true for (textSize, textNode) in lineNodes { + if isFirstLine { + isFirstLine = false + } else { + lineOffset += 4.0 + } + let isRTL = textNode.cachedLayout?.hasRTL ?? false transition.updateFrame(node: textNode, frame: CGRect(origin: CGPoint(x: isRTL ? contentRect.maxX - textSize.width : contentRect.minX, y: lineOffset), size: textSize)) - lineOffset += textSize.height + 4.0 + lineOffset += textSize.height + } + + if let linkTextButton = self.linkTextButton, let linkTextNode = self.linkTextNode, let linkTextLayout { + if isFirstLine { + isFirstLine = false + } else { + lineOffset += businessLinkTextSpacing + } + + let linkTextButtonFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - linkTextLayout.size.width) * 0.5), y: lineOffset), size: linkTextLayout.size) + let linkTextFrame = CGRect(origin: CGPoint(), size: linkTextButtonFrame.size) + + transition.updatePosition(node: linkTextButton, position: linkTextButtonFrame.center) + transition.updateBounds(node: linkTextButton, bounds: CGRect(origin: CGPoint(), size: linkTextButtonFrame.size)) + transition.updateFrame(node: linkTextNode, frame: linkTextFrame) + + let linkTextHighlightNode: LinkHighlightingNode + if let current = self.linkTextHighlightNode { + linkTextHighlightNode = current + } else { + linkTextHighlightNode = LinkHighlightingNode(color: .black) + linkTextHighlightNode.inset = 0.0 + linkTextHighlightNode.useModernPathCalculation = true + self.linkTextHighlightNode = linkTextHighlightNode + linkTextNode.supernode?.insertSubnode(linkTextHighlightNode, belowSubnode: linkTextNode) + } + + let textLayout = linkTextLayout + + var labelRects = textLayout.linesRects() + if labelRects.count > 1 { + let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) + for i in 0 ..< sortedIndices.count { + let index = sortedIndices[i] + for j in -1 ... 1 { + if j != 0 && index + j >= 0 && index + j < sortedIndices.count { + if abs(labelRects[index + j].width - labelRects[index].width) < 16.0 { + labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) + labelRects[index].size.width = labelRects[index + j].size.width + } + } + } + } + } + for i in 0 ..< labelRects.count { + labelRects[i] = labelRects[i].insetBy(dx: -4.0, dy: 0.0) + if i == 0 { + labelRects[i].origin.y -= 1.0 + labelRects[i].size.height += 1.0 + } + if i == labelRects.count - 1 { + labelRects[i].size.height += 1.0 + } else { + let deltaY = labelRects[i + 1].minY - labelRects[i].maxY + let topDelta = deltaY * 0.5 - 0.0 + let bottomDelta = deltaY * 0.5 - 0.0 + labelRects[i].size.height += topDelta + labelRects[i + 1].origin.y -= bottomDelta + labelRects[i + 1].size.height += bottomDelta + } + labelRects[i].origin.x = floor((textLayout.size.width - labelRects[i].width) / 2.0) + } + for i in 0 ..< labelRects.count { + labelRects[i].origin.y -= 12.0 + } + + linkTextHighlightNode.innerRadius = 4.0 + linkTextHighlightNode.outerRadius = 4.0 + + linkTextHighlightNode.updateRects(labelRects, color: interfaceState.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1)) + + linkTextHighlightNode.frame = linkTextFrame.offsetBy(dx: 0.0, dy: 0.0) + } else { + if let linkTextHighlightNode = self.linkTextHighlightNode { + self.linkTextHighlightNode = nil + linkTextHighlightNode.removeFromSupernode() + } } return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size @@ -1388,11 +1586,6 @@ private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode { } self.textMaskNode.updateRects(labelRects) - /*if self.textMaskNode.supernode == nil { - self.addSubnode(self.textMaskNode) - self.textMaskNode.alpha = 0.5 - }*/ - let size = CGSize(width: textLayout.size.width + 4.0 * 2.0, height: textLayout.size.height + 4.0 * 2.0) let textFrame = CGRect(origin: CGPoint(x: 4.0, y: 4.0), size: textLayout.size) self.textNode.frame = textFrame @@ -1606,7 +1799,23 @@ public final class ChatEmptyNode: ASDisplayNode { case .group: node = ChatEmptyNodeGroupChatContent() case .cloud: - node = ChatEmptyNodeCloudChatContent() + let cloudNode = ChatEmptyNodeCloudChatContent() + node = cloudNode + cloudNode.shareBusinessLink = { [weak self] url in + guard let self, let interfaceInteraction = self.interaction else { + return + } + + UIPasteboard.general.string = url + + let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }) + + //TODO:localize + let controller = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.GroupInfo_InviteLink_CopyAlert_Success), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { _ in + return false + }) + interfaceInteraction.presentControllerInCurrent(controller, nil) + } case .peerNearby: node = ChatEmptyNodeNearbyChatContent(context: self.context, interaction: self.interaction) case .greeting: @@ -1626,7 +1835,7 @@ public final class ChatEmptyNode: ASDisplayNode { node.layer.animateScale(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) } } - self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired].contains(contentType) + self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired, .cloud].contains(contentType) let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom)) diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 2ac21284ac..15118ffa9a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1096,6 +1096,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .premiumMultiGift: break + case .messageLink: + break } } })) diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index da181e56d9..a3642584ed 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -1765,6 +1765,15 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { if !emojiEnabled && interfaceState.interfaceState.editMessage == nil { emojiContent = nil } + if case let .customChatContents(customChatContents) = interfaceState.subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case .businessLinkSetup: + stickerContent = nil + gifContent = nil + } + } stickerContent?.inputInteractionHolder.inputInteraction = self.stickerInputInteraction self.currentInputData.emoji?.inputInteractionHolder.inputInteraction = self.emojiInputInteraction diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 6642bd4a80..027439654e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -142,6 +142,7 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/TextLoadingEffect", "//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen", + "//submodules/TelegramUI/Components/Settings/PeerSelectionScreen", "//submodules/ConfettiEffect", ], visibility = [ diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift index 2e34525e62..05f39366a2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift @@ -5,10 +5,12 @@ import TelegramPresentationData final class PeerInfoScreenHeaderItem: PeerInfoScreenItem { let id: AnyHashable let text: String + let label: String? - init(id: AnyHashable, text: String) { + init(id: AnyHashable, text: String, label: String? = nil) { self.id = id self.text = text + self.label = label } func node() -> PeerInfoScreenItemNode { @@ -18,6 +20,7 @@ final class PeerInfoScreenHeaderItem: PeerInfoScreenItem { private final class PeerInfoScreenHeaderItemNode: PeerInfoScreenItemNode { private let textNode: ImmediateTextNode + private let labelNode: ImmediateTextNode private let activateArea: AccessibilityAreaNode private var item: PeerInfoScreenHeaderItem? @@ -27,12 +30,17 @@ private final class PeerInfoScreenHeaderItemNode: PeerInfoScreenItemNode { self.textNode.displaysAsynchronously = false self.textNode.isUserInteractionEnabled = false + self.labelNode = ImmediateTextNode() + self.labelNode.displaysAsynchronously = false + self.labelNode.isUserInteractionEnabled = false + self.activateArea = AccessibilityAreaNode() self.activateArea.accessibilityTraits = [.staticText, .header] super.init() self.addSubnode(self.textNode) + self.addSubnode(self.labelNode) self.addSubnode(self.activateArea) } @@ -54,9 +62,17 @@ private final class PeerInfoScreenHeaderItemNode: PeerInfoScreenItemNode { let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: textSize) + self.labelNode.maximumNumberOfLines = 0 + self.labelNode.attributedText = NSAttributedString(string: item.label ?? "", font: Font.regular(13.0), textColor: presentationData.theme.list.freeTextColor) + + let labelSize = self.labelNode.updateLayout(CGSize(width: max(0.0, width - sideInset * 2.0 - textSize.width - 4.0), height: .greatestFiniteMagnitude)) + + let labelFrame = CGRect(origin: CGPoint(x: width - sideInset - labelSize.width, y: verticalInset), size: labelSize) + let height = textSize.height + verticalInset * 2.0 transition.updateFrame(node: self.textNode, frame: textFrame) + transition.updateFrame(node: self.labelNode, frame: labelFrame) self.activateArea.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift new file mode 100644 index 0000000000..4dd76478de --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -0,0 +1,398 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData +import AccountContext +import TextFormat +import UIKit +import AppBundle +import TelegramStringFormatting +import ContextUI +import TelegramCore +import ChatListUI +import Postbox + +final class PeerInfoScreenPersonalChannelItem: PeerInfoScreenItem { + let id: AnyHashable + let context: AccountContext + let data: PeerInfoPersonalChannelData + let requestLayout: (Bool) -> Void + let action: () -> Void + + init( + id: AnyHashable, + context: AccountContext, + data: PeerInfoPersonalChannelData, + requestLayout: @escaping (Bool) -> Void, + action: @escaping () -> Void + ) { + self.id = id + self.context = context + self.data = data + self.requestLayout = requestLayout + self.action = action + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenPersonalChannelItemNode() + } +} + +private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNode { + private let containerNode: ContextControllerSourceNode + private let contextSourceNode: ContextExtractedContentContainingNode + + private let extractedBackgroundImageNode: ASImageNode + + private var extractedRect: CGRect? + private var nonExtractedRect: CGRect? + + private let maskNode: ASImageNode + + private let bottomSeparatorNode: ASDisplayNode + + private let activateArea: AccessibilityAreaNode + + private var item: PeerInfoScreenPersonalChannelItem? + private var presentationData: PresentationData? + private var theme: PresentationTheme? + + private var itemNode: ListViewItemNode? + + override init() { + self.contextSourceNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.extractedBackgroundImageNode = ASImageNode() + self.extractedBackgroundImageNode.displaysAsynchronously = false + self.extractedBackgroundImageNode.alpha = 0.0 + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + self.activateArea = AccessibilityAreaNode() + + super.init() + + self.addSubnode(self.bottomSeparatorNode) + + self.containerNode.addSubnode(self.contextSourceNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode + self.addSubnode(self.containerNode) + + self.addSubnode(self.maskNode) + + self.contextSourceNode.contentNode.clipsToBounds = true + + self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) + + self.addSubnode(self.activateArea) + + self.containerNode.isGestureEnabled = false + + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self, let theme = strongSelf.theme else { + return + } + + if isExtracted { + strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: theme.list.plainBackgroundColor) + } + + if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { + let rect = isExtracted ? extractedRect : nonExtractedRect + transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) + } + + transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in + if !isExtracted { + self?.extractedBackgroundImageNode.image = nil + } + }) + } + } + + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { _ in + return .waitForSingleTap + } + recognizer.highlight = { [weak self] point in + guard let strongSelf = self else { + return + } + strongSelf.updateTouchesAtPoint(point) + } + self.view.addGestureRecognizer(recognizer) + } + + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + self.item?.action() + case .longTap: + break + default: + break + } + } + default: + break + } + } + + override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenPersonalChannelItem else { + return 50.0 + } + + self.item = item + self.presentationData = presentationData + self.theme = presentationData.theme + + let sideInset: CGFloat = 16.0 + safeInsets.left + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let chatListPresentationData = ChatListPresentationData( + theme: presentationData.theme, + fontSize: presentationData.listsFontSize, + strings: presentationData.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameSortOrder: presentationData.nameSortOrder, + nameDisplayOrder: presentationData.nameDisplayOrder, + disableAnimations: false + ) + + let chatListNodeInteraction = ChatListNodeInteraction( + context: item.context, + animationCache: item.context.animationCache, + animationRenderer: item.context.animationRenderer, + activateSearch: { + }, + peerSelected: { _, _, _, _ in + }, + disabledPeerSelected: { _, _, _ in + }, + togglePeerSelected: { _, _ in + }, + togglePeersSelection: { _, _ in + }, + additionalCategorySelected: { _ in + }, + messageSelected: { _, _, _, _ in + }, + groupSelected: { _ in + }, + addContact: { _ in + }, + setPeerIdWithRevealedOptions: { _, _ in + }, + setItemPinned: { _, _ in + }, + setPeerMuted: { _, _ in + }, + setPeerThreadMuted: { _, _, _ in + }, + deletePeer: { _, _ in + }, + deletePeerThread: { _, _ in + }, + setPeerThreadStopped: { _, _, _ in + }, + setPeerThreadPinned: { _, _, _ in + }, + setPeerThreadHidden: { _, _, _ in + }, + updatePeerGrouping: { _, _ in + }, + togglePeerMarkedUnread: { _, _ in + }, + toggleArchivedFolderHiddenByDefault: { + }, + toggleThreadsSelection: { _, _ in + }, + hidePsa: { _ in + }, + activateChatPreview: { _, _, _, _, _ in + }, + present: { _ in + }, + openForumThread: { _, _ in + }, + openStorageManagement: { + }, + openPasswordSetup: { + }, + openPremiumIntro: { + }, + openPremiumGift: { _ in + }, + openActiveSessions: { + }, + openBirthdaySetup: { + }, + performActiveSessionAction: { _, _ in + }, + openChatFolderUpdates: { + }, + hideChatFolderUpdates: { + }, + openStories: { _, _ in + }, + dismissNotice: { _ in + }, + editPeer: { _ in + } + ) + + let index: EngineChatList.Item.Index + let messages: [EngineMessage] + if let message = item.data.topMessage { + index = EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: message.index)) + messages = [message] + } else { + index = EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: item.data.peer.id, namespace: Namespaces.Message.Cloud, id: 1), timestamp: 0))) + messages = [] + } + + let chatListItem = ChatListItem( + presentationData: chatListPresentationData, + context: item.context, + chatListLocation: .chatList(groupId: .root), + filterData: nil, + index: index, + content: .peer(ChatListItemContent.PeerData( + messages: messages, + peer: EngineRenderedPeer(peer: item.data.peer), + threadInfo: nil, + combinedReadState: nil, + isRemovedFromTotalUnreadCount: false, + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + draftState: nil, + mediaDraftContentType: nil, + inputActivities: nil, + promoInfo: nil, + ignoreUnreadBadge: false, + displayAsMessage: false, + hasFailedMessages: false, + forumTopicData: nil, + topForumTopicItems: [], + autoremoveTimeout: nil, + storyState: nil, + requiresPremiumForMessaging: false, + displayAsTopicList: false, + tags: [], + customMessageListData: ChatListItemContent.CustomMessageListData( + commandPrefix: nil, + searchQuery: nil, + messageCount: nil, + hideSeparator: true, + hideDate: false + ) + )), + editing: false, + hasActiveRevealControls: false, + selected: false, + header: nil, + enableContextActions: false, + hiddenOffset: false, + interaction: chatListNodeInteraction + ) + var itemNode: ListViewItemNode? + let params = ListViewItemLayoutParams(width: width - safeInsets.left - safeInsets.right, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0) + if let current = self.itemNode { + itemNode = current + chatListItem.updateNode( + async: { f in f () }, + node: { + return current + }, + params: params, + previousItem: nil, + nextItem: nil, animation: .None, + completion: { layout, apply in + let nodeFrame = CGRect(origin: current.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + current.contentSize = layout.contentSize + current.insets = layout.insets + current.frame = nodeFrame + + apply(ListViewItemApply(isOnScreen: true)) + }) + } else { + var outItemNode: ListViewItemNode? + chatListItem.nodeConfiguredForParams( + async: { f in f() }, + params: params, + synchronousLoads: true, + previousItem: nil, + nextItem: nil, + completion: { node, apply in + outItemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + } + ) + itemNode = outItemNode + } + + let height = itemNode?.contentSize.height ?? 50.0 + + if self.itemNode !== itemNode { + self.itemNode?.removeFromSupernode() + + self.itemNode = itemNode + if let itemNode { + itemNode.isUserInteractionEnabled = false + self.contextSourceNode.contentNode.addSubnode(itemNode) + } + } + if let itemNode = self.itemNode { + itemNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)) + } + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + let hasCorners = hasCorners && (topItem == nil || bottomItem == nil) + let hasTopCorners = hasCorners && topItem == nil + let hasBottomCorners = hasCorners && bottomItem == nil + + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)) + self.bottomSeparatorNode.isHidden = hasBottomCorners + + self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)) + + let contentSize = CGSize(width: width, height: height) + self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: contentSize) + transition.updateFrame(node: self.contextSourceNode.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize)) + + let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: contentSize.height)) + let extractedRect = nonExtractedRect + self.extractedRect = extractedRect + self.nonExtractedRect = nonExtractedRect + + if self.contextSourceNode.isExtractedToContextPreview { + self.extractedBackgroundImageNode.frame = extractedRect + } else { + self.extractedBackgroundImageNode.frame = nonExtractedRect + } + self.contextSourceNode.contentRect = extractedRect + + return height + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 34e2a35e7d..8a2ea1e9fe 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -228,6 +228,34 @@ final class TelegramGlobalSettings { } } +final class PeerInfoPersonalChannelData: Equatable { + let peer: EnginePeer + let subscriberCount: Int + let topMessage: EngineMessage? + + init(peer: EnginePeer, subscriberCount: Int, topMessage: EngineMessage?) { + self.peer = peer + self.subscriberCount = subscriberCount + self.topMessage = topMessage + } + + static func ==(lhs: PeerInfoPersonalChannelData, rhs: PeerInfoPersonalChannelData) -> Bool { + if lhs === rhs { + return true + } + if lhs.peer != rhs.peer { + return false + } + if lhs.subscriberCount != rhs.subscriberCount { + return false + } + if lhs.topMessage != rhs.topMessage { + return false + } + return true + } +} + final class PeerInfoScreenData { let peer: Peer? let chatPeer: Peer? @@ -253,6 +281,7 @@ final class PeerInfoScreenData { let accountIsPremium: Bool let hasSavedMessageTags: Bool let isPremiumRequiredForStoryPosting: Bool + let personalChannel: PeerInfoPersonalChannelData? let _isContact: Bool var forceIsContact: Bool = false @@ -290,7 +319,8 @@ final class PeerInfoScreenData { isPowerSavingEnabled: Bool?, accountIsPremium: Bool, hasSavedMessageTags: Bool, - isPremiumRequiredForStoryPosting: Bool + isPremiumRequiredForStoryPosting: Bool, + personalChannel: PeerInfoPersonalChannelData? ) { self.peer = peer self.chatPeer = chatPeer @@ -317,6 +347,7 @@ final class PeerInfoScreenData { self.accountIsPremium = accountIsPremium self.hasSavedMessageTags = hasSavedMessageTags self.isPremiumRequiredForStoryPosting = isPremiumRequiredForStoryPosting + self.personalChannel = personalChannel } } @@ -500,6 +531,31 @@ public func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId, c } } +private func peerInfoPersonalChannel(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + return context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: EnginePeer.Id.Id._internalFromInt64Value(1006503122))) + ) + |> mapToSignal { peer -> Signal in + guard let peer else { + return .single(nil) + } + + return context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peer.id, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 5, fixedCombinedReadStates: nil) + |> map { view, _, _ -> PeerInfoPersonalChannelData? in + guard let entry = view.entries.last else { + return nil + } + + return PeerInfoPersonalChannelData( + peer: peer, + subscriberCount: 2000000, + topMessage: EngineMessage(entry.message) + ) + } + } + |> distinctUntilChanged +} + func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, accountsAndPeers: Signal<[(AccountContext, EnginePeer, Int32)], NoError>, activeSessionsContextAndCount: Signal<(ActiveSessionsContext, Int, WebSessionsContext)?, NoError>, notificationExceptions: Signal, privacySettings: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, hasPassport: Signal) -> Signal { let preferences = context.sharedContext.accountManager.sharedData(keys: [ SharedDataKeys.proxySettings, @@ -642,9 +698,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, } |> distinctUntilChanged, hasStories, - bots + bots, + peerInfoPersonalChannel(context: context, peerId: peerId) ) - |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots -> PeerInfoScreenData in + |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots, personalChannel -> PeerInfoScreenData in let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications let (featuredStickerPacks, archivedStickerPacks) = stickerPacks @@ -714,7 +771,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, isPowerSavingEnabled: isPowerSavingEnabled, accountIsPremium: peer?.isPremium ?? false, hasSavedMessageTags: false, - isPremiumRequiredForStoryPosting: true + isPremiumRequiredForStoryPosting: true, + personalChannel: personalChannel ) } } @@ -751,7 +809,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen isPowerSavingEnabled: nil, accountIsPremium: false, hasSavedMessageTags: false, - isPremiumRequiredForStoryPosting: true + isPremiumRequiredForStoryPosting: true, + personalChannel: nil )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -960,9 +1019,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, - hasSavedMessageTags + hasSavedMessageTags, + peerInfoPersonalChannel(context: context, peerId: peerId) ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, personalChannel -> PeerInfoScreenData in var availablePanes = availablePanes if let hasStories { @@ -1023,7 +1083,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen isPowerSavingEnabled: nil, accountIsPremium: accountIsPremium, hasSavedMessageTags: hasSavedMessageTags, - isPremiumRequiredForStoryPosting: false + isPremiumRequiredForStoryPosting: false, + personalChannel: personalChannel ) } case .channel: @@ -1192,7 +1253,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen isPowerSavingEnabled: nil, accountIsPremium: accountIsPremium, hasSavedMessageTags: hasSavedMessageTags, - isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting + isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting, + personalChannel: nil ) } case let .group(groupId): @@ -1484,7 +1546,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen isPowerSavingEnabled: nil, accountIsPremium: accountIsPremium, hasSavedMessageTags: hasSavedMessageTags, - isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting + isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting, + personalChannel: nil )) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index e575f46b62..bdd42569db 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -106,6 +106,7 @@ import PeerInfoChatPaneNode import PeerInfoChatListPaneNode import GroupStickerPackSetupController import PeerNameColorItem +import PeerSelectionScreen public enum PeerInfoAvatarEditingMode { case generic @@ -531,7 +532,7 @@ private enum TopicsLimitedReason { } private final class PeerInfoInteraction { - let openChat: () -> Void + let openChat: (EnginePeer.Id?) -> Void let openUsername: (String, Bool, Promise?) -> Void let openPhone: (String, ASDisplayNode, ContextGesture?) -> Void let editingOpenNotificationSettings: () -> Void @@ -588,6 +589,7 @@ private final class PeerInfoInteraction { let updateIsEditingBirthdate: (Bool) -> Void let openBirthdatePrivacy: () -> Void let openPremiumGift: () -> Void + let editingOpenPersonalChannel: () -> Void init( openUsername: @escaping (String, Bool, Promise?) -> Void, @@ -599,7 +601,7 @@ private final class PeerInfoInteraction { suggestPhoto: @escaping () -> Void, setCustomPhoto: @escaping () -> Void, resetCustomPhoto: @escaping () -> Void, - openChat: @escaping () -> Void, + openChat: @escaping (EnginePeer.Id?) -> Void, openAddContact: @escaping () -> Void, updateBlocked: @escaping (Bool) -> Void, openReport: @escaping (PeerInfoReportType) -> Void, @@ -646,7 +648,8 @@ private final class PeerInfoInteraction { updateBirthdate: @escaping (TelegramBirthday??) -> Void, updateIsEditingBirthdate: @escaping (Bool) -> Void, openBirthdatePrivacy: @escaping () -> Void, - openPremiumGift: @escaping () -> Void + openPremiumGift: @escaping () -> Void, + editingOpenPersonalChannel: @escaping () -> Void ) { self.openUsername = openUsername self.openPhone = openPhone @@ -705,6 +708,7 @@ private final class PeerInfoInteraction { self.updateIsEditingBirthdate = updateIsEditingBirthdate self.openBirthdatePrivacy = openBirthdatePrivacy self.openPremiumGift = openPremiumGift + self.editingOpenPersonalChannel = editingOpenPersonalChannel } } @@ -1016,6 +1020,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat let ItemBirthdayPicker = 10 let ItemBirthdayRemove = 11 let ItemBirthdayHelp = 12 + let ItemPeerPersonalChannel = 13 items[.help]!.append(PeerInfoScreenCommentItem(id: ItemNameHelp, text: presentationData.strings.EditProfile_NameAndPhotoOrVideoHelp)) @@ -1091,6 +1096,11 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), text: presentationData.strings.Settings_YourColor, icon: nil, action: { interaction.editingOpenNameColorSetup() })) + + //TODO:localize + items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerPersonalChannel, label: .text("Add"), text: "Personal Channel", icon: nil, action: { + interaction.editingOpenPersonalChannel() + })) } items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: { @@ -1130,6 +1140,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat private enum InfoSection: Int, CaseIterable { case groupLocation case calls + case personalChannel case peerInfo case peerMembers } @@ -1156,6 +1167,18 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese items[.calls]!.append(PeerInfoScreenCallListItem(id: 20, messages: callMessages)) } + if let personalChannel = data.personalChannel, !"".isEmpty { + let peerId = personalChannel.peer.id + items[.personalChannel]?.append(PeerInfoScreenHeaderItem(id: 0, text: "PERSONAL CHANNEL", label: "2M subscribers")) + items[.personalChannel]?.append(PeerInfoScreenPersonalChannelItem(id: 1, context: context, data: personalChannel, requestLayout: { _ in + }, action: { [weak interaction] in + guard let interaction else { + return + } + interaction.openChat(peerId) + })) + } + if let phone = user.phone { let formattedPhone = formatPhoneNumber(context: context, number: phone) let label: String @@ -1282,7 +1305,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } if let reactionSourceMessageId = reactionSourceMessageId, !data.isContact { items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_SendMessage, action: { - interaction.openChat() + interaction.openChat(nil) })) items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_ReportReaction, color: .destructive, action: { @@ -1290,7 +1313,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) } else if let _ = nearbyPeerDistance { items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_SendMessage, action: { - interaction.openChat() + interaction.openChat(nil) })) items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_Report, color: .destructive, action: { @@ -2493,8 +2516,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro resetCustomPhoto: { [weak self] in self?.resetCustomPhoto() }, - openChat: { [weak self] in - self?.openChat() + openChat: { [weak self] peerId in + self?.openChat(peerId: peerId) }, openAddContact: { [weak self] in self?.openAddContact() @@ -2669,6 +2692,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let self { self.openPremiumGift() } + }, + editingOpenPersonalChannel: { [weak self] in + guard let self else { + return + } + self.editingOpenPersonalChannel() } ) @@ -6983,7 +7012,24 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.controller?.present(actionSheet, in: .window(.root)) } - private func openChat() { + private func openChat(peerId: EnginePeer.Id?) { + if let peerId { + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .always)) + }) + return + } + if let peer = self.data?.peer, let navigationController = self.controller?.navigationController as? NavigationController { self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: self.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { @@ -7568,6 +7614,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.controller?.push(ChannelAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, boostStatus: self.boostStatus)) } } + + private func editingOpenPersonalChannel() { + self.controller?.push(PeerSelectionScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, completion: { [weak self] channel in + guard let self else { + return + } + + let _ = self.context.engine.accountData.updatePersonalChannel(personalChannel: TelegramPersonalChannel(peerId: channel.peer.id, subscriberCount: channel.subscriberCount.flatMap(Int32.init(clamping:)), topMessageId: nil)).startStandalone() + })) + } private func editingOpenInviteLinksSetup() { self.controller?.push(inviteLinkListController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, admin: nil)) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD index b7f87546eb..7329e7c0bc 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD @@ -48,6 +48,10 @@ swift_library( "//submodules/TelegramUI/Components/ChatListHeaderComponent", "//submodules/AttachmentUI", "//submodules/SearchBarNode", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/ListItemSwipeOptionContainer", + "//submodules/UndoUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index dc1e0bd903..481cb62d72 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -241,7 +241,8 @@ final class GreetingMessageListItemComponent: Component { commandPrefix: nil, searchQuery: nil, messageCount: component.count, - hideSeparator: true + hideSeparator: true, + hideDate: true ) )), editing: false, diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift index b002aa3cc5..cde85a0476 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift @@ -213,6 +213,8 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco switch kind { case let .quickReplyMessageInput(shortcut, _): initialShortcut = shortcut + case .businessLinkSetup: + initialShortcut = "" } let queue = Queue() @@ -247,6 +249,11 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco self.impl.with { impl in impl.quickReplyUpdateShortcut(value: value) } + case .businessLinkSetup: + break } } + + func businessLinkUpdate(message: String, entities: [MessageTextEntity], title: String?) { + } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkChatContents.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkChatContents.swift new file mode 100644 index 0000000000..3b69806c61 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkChatContents.swift @@ -0,0 +1,88 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext + +final class BusinessLinkChatContents: ChatCustomContentsProtocol { + private final class Impl { + let queue: Queue + let context: AccountContext + + init(queue: Queue, context: AccountContext) { + self.queue = queue + self.context = context + } + + deinit { + } + + func enqueueMessages(messages: [EnqueueMessage]) { + } + + func deleteMessages(ids: [EngineMessage.Id]) { + } + + func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) { + } + } + + var kind: ChatCustomContentsKind + + var historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError> { + let view = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: [], holeEarlier: false, holeLater: false, isLoading: false) + + return .single((view, .Initial)) + } + + var messageLimit: Int? { + return 20 + } + + private let queue: Queue + private let impl: QueueLocalObject + + init(context: AccountContext, kind: ChatCustomContentsKind) { + self.kind = kind + + let queue = Queue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, context: context) + }) + } + + func enqueueMessages(messages: [EnqueueMessage]) { + self.impl.with { impl in + impl.enqueueMessages(messages: messages) + } + } + + func deleteMessages(ids: [EngineMessage.Id]) { + self.impl.with { impl in + impl.deleteMessages(ids: ids) + } + } + + func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) { + self.impl.with { impl in + impl.editMessage(id: id, text: text, media: media, entities: entities, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview) + } + } + + func quickReplyUpdateShortcut(value: String) { + } + + func businessLinkUpdate(message: String, entities: [MessageTextEntity], title: String?) { + if case let .businessLinkSetup(link) = self.kind { + self.kind = .businessLinkSetup(link: TelegramBusinessChatLinks.Link( + url: link.url, + message: message, + entities: entities, + title: title, + viewCount: link.viewCount + )) + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift new file mode 100644 index 0000000000..7656dcd092 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift @@ -0,0 +1,266 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ListSectionComponent +import TelegramPresentationData +import AppBundle +import AccountContext +import Postbox +import TelegramCore +import TextNodeWithEntities +import MultilineTextComponent +import TextFormat +import ListItemSwipeOptionContainer + +final class BusinessLinkListItemComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let link: TelegramBusinessChatLinks.Link + let action: () -> Void + let deleteAction: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + link: TelegramBusinessChatLinks.Link, + action: @escaping () -> Void, + deleteAction: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.link = link + self.action = action + self.deleteAction = deleteAction + } + + static func ==(lhs: BusinessLinkListItemComponent, rhs: BusinessLinkListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.link != rhs.link { + return false + } + return true + } + + final class View: UIView, ListSectionComponent.ChildView { + private let containerButton: HighlightTrackingButton + private let swipeOptionContainer: ListItemSwipeOptionContainer + + private let iconView = UIImageView() + private let viewCount = ComponentView() + private let title = ComponentView() + private let text = TextNodeWithEntities() + + private var component: BusinessLinkListItemComponent? + private weak var componentState: EmptyComponentState? + + var customUpdateIsHighlighted: ((Bool) -> Void)? + private(set) var separatorInset: CGFloat = 0.0 + + override init(frame: CGRect) { + self.containerButton = HighlightTrackingButton() + self.containerButton.layer.anchorPoint = CGPoint() + self.containerButton.isExclusiveTouch = true + + self.swipeOptionContainer = ListItemSwipeOptionContainer(frame: CGRect()) + + super.init(frame: frame) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.containerButton.internalHighligthedChanged = { [weak self] isHighlighted in + guard let self else { + return + } + if let customUpdateIsHighlighted = self.customUpdateIsHighlighted { + customUpdateIsHighlighted(isHighlighted) + } + } + + self.swipeOptionContainer.updateRevealOffset = { [weak self] offset, transition in + guard let self else { + return + } + transition.setBounds(view: self.containerButton, bounds: CGRect(origin: CGPoint(x: -offset, y: 0.0), size: self.containerButton.bounds.size)) + } + self.swipeOptionContainer.revealOptionSelected = { [weak self] option, _ in + guard let self, let component = self.component else { + return + } + self.swipeOptionContainer.setRevealOptionsOpened(false, animated: true) + component.deleteAction() + } + + self.addSubview(self.swipeOptionContainer) + + self.swipeOptionContainer.addSubview(self.containerButton) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.component?.action() + } + + func update(component: BusinessLinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + let _ = previousComponent + + self.component = component + self.componentState = state + + let leftContentInset: CGFloat = 62.0 + let rightInset: CGFloat = 8.0 + let topInset: CGFloat = 9.0 + let bottomInset: CGFloat = 9.0 + let titleViewCountSpacing: CGFloat = 4.0 + let titleTextSpacing: CGFloat = 4.0 + + //TODO:localize + + let viewCountText: String + if component.link.viewCount == 0 { + viewCountText = "no clicks" + } else { + viewCountText = "\(component.link.viewCount) clicks" + } + let viewCountSize = self.viewCount.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: viewCountText, font: Font.regular(14.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let viewCountFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - viewCountSize.width, y: topInset + 2.0), size: viewCountSize) + if let viewCountView = self.viewCount.view { + if viewCountView.superview == nil { + viewCountView.isUserInteractionEnabled = false + viewCountView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + self.containerButton.addSubview(viewCountView) + } + transition.setPosition(view: viewCountView, position: CGPoint(x: viewCountFrame.maxX, y: viewCountFrame.minY)) + viewCountView.bounds = CGRect(origin: CGPoint(), size: viewCountFrame.size) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.link.title ?? component.link.url, font: Font.regular(16.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftContentInset - rightInset - viewCountSize.width - titleViewCountSpacing, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: leftContentInset, y: topInset), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + titleView.layer.anchorPoint = CGPoint() + self.containerButton.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.origin) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + + let asyncLayout = TextNodeWithEntities.asyncLayout(self.text) + let textString = stringWithAppliedEntities( + component.link.message.isEmpty ? "No text" : component.link.message, + entities: component.link.entities, + baseColor: component.theme.list.itemSecondaryTextColor, + linkColor: component.theme.list.itemSecondaryTextColor, + baseQuoteTintColor: nil, + baseQuoteSecondaryTintColor: nil, + baseQuoteTertiaryTintColor: nil, + codeBlockTitleColor: nil, + codeBlockAccentColor: nil, + codeBlockBackgroundColor: nil, + baseFont: Font.regular(15.0), + linkFont: Font.regular(15.0), + boldFont: Font.semibold(15.0), + italicFont: Font.italic(15.0), + boldItalicFont: Font.semiboldItalic(15.0), + fixedFont: Font.monospace(15.0), + blockQuoteFont: Font.regular(15.0), + underlineLinks: false, + external: false, + message: nil, + entityFiles: [:], + adjustQuoteFontSize: false, + cachedMessageSyntaxHighlight: nil + ) + let (textLayout, textApply) = asyncLayout(TextNodeLayoutArguments(attributedString: textString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: availableSize.width - leftContentInset - rightInset, height: 100.0))) + let _ = textApply(TextNodeWithEntities.Arguments( + context: component.context, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + placeholderColor: component.theme.list.mediaPlaceholderColor, + attemptSynchronous: true + )) + let textSize = textLayout.size + let textFrame = CGRect(origin: CGPoint(x: leftContentInset, y: titleFrame.maxY + titleTextSpacing), size: textLayout.size) + if self.text.textNode.view.superview == nil { + self.text.textNode.view.isUserInteractionEnabled = false + self.containerButton.addSubview(self.text.textNode.view) + } + transition.setFrame(view: self.text.textNode.view, frame: textFrame) + + let size = CGSize(width: availableSize.width, height: topInset + titleSize.height + titleTextSpacing + textSize.height + bottomInset) + + self.iconView.image = PresentationResourcesItemList.sharedLinkIcon(component.theme) + if let image = self.iconView.image { + if self.iconView.superview == nil { + self.iconView.isUserInteractionEnabled = false + self.containerButton.addSubview(self.iconView) + } + let iconFrame = CGRect(origin: CGPoint(x: floor((leftContentInset - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size) + transition.setFrame(view: self.iconView, frame: iconFrame) + } + + let swipeOptionContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) + transition.setFrame(view: self.swipeOptionContainer, frame: swipeOptionContainerFrame) + + transition.setPosition(view: self.containerButton, position: CGPoint()) + transition.setBounds(view: self.containerButton, bounds: CGRect(origin: self.containerButton.bounds.origin, size: size)) + + self.swipeOptionContainer.updateLayout(size: swipeOptionContainerFrame.size, leftInset: 0.0, rightInset: 0.0) + + var rightOptions: [ListItemSwipeOptionContainer.Option] = [] + let color: UIColor = component.theme.list.itemDisclosureActions.destructive.fillColor + let textColor: UIColor = component.theme.list.itemDisclosureActions.destructive.foregroundColor + rightOptions = [ + ListItemSwipeOptionContainer.Option( + key: 0, + title: component.strings.Common_Delete, + icon: .none, + color: color, + textColor: textColor + ) + ] + self.swipeOptionContainer.setRevealOptions(([], rightOptions)) + + self.separatorInset = leftContentInset + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift new file mode 100644 index 0000000000..a70cba1794 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift @@ -0,0 +1,667 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ListSectionComponent +import TelegramPresentationData +import AppBundle +import AccountContext +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import LottieComponent +import Markdown +import SwiftSignalKit +import TelegramCore +import ListActionItemComponent +import BundleIconComponent +import TextFormat +import UndoUI + +final class BusinessLinksSetupScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialData: BusinessLinksSetupScreen.InitialData + + init( + context: AccountContext, + initialData: BusinessLinksSetupScreen.InitialData + ) { + self.context = context + self.initialData = initialData + } + + static func ==(lhs: BusinessLinksSetupScreenComponent, rhs: BusinessLinksSetupScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + + private let navigationTitle = ComponentView() + private let icon = ComponentView() + private let subtitle = ComponentView() + private let createLinkSection = ComponentView() + private let linksSection = ComponentView() + + private var isUpdating: Bool = false + + private var component: BusinessLinksSetupScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var refreshLinksDisposable: Disposable? + private var links: [TelegramBusinessChatLinks.Link] = [] + private var linksDisposable: Disposable? + + private var isCreatingLink: Bool = false + private var createLinkDisposable: Disposable? + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.refreshLinksDisposable?.dispose() + self.linksDisposable?.dispose() + self.createLinkDisposable?.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + var scrolledUp = true + private func updateScrolling(transition: Transition) { + let navigationRevealOffsetY: CGFloat = 0.0 + + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + + var scrolledUp = false + if navigationAlpha < 0.5 { + scrolledUp = true + } else if navigationAlpha > 0.5 { + scrolledUp = false + } + + if self.scrolledUp != scrolledUp { + self.scrolledUp = scrolledUp + if !self.isUpdating { + self.state?.updated() + } + } + + if let navigationTitleView = self.navigationTitle.view { + transition.setAlpha(view: navigationTitleView, alpha: 1.0) + } + } + + private func createLink() { + guard let component = self.component else { + return + } + if self.isCreatingLink { + return + } + self.isCreatingLink = true + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + + self.createLinkDisposable?.dispose() + self.createLinkDisposable = (component.context.engine.accountData.createBusinessChatLink(message: "", entities: [], title: nil) + |> deliverOnMainQueue).startStrict(next: { [weak self] link in + guard let self else { + return + } + + self.isCreatingLink = false + self.state?.updated(transition: .immediate) + + self.openLink(link: link) + }, error: { [weak self] error in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + self.isCreatingLink = false + self.state?.updated(transition: .immediate) + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + let errorText: String + switch error { + case .generic: + errorText = presentationData.strings.Login_UnknownError + case .tooManyLinks: + errorText = "You can't create more links" + } + + environment.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + }) + ]), in: .window(.root)) + }) + } + + private func openLink(url: String) { + if let link = self.links.first(where: { $0.url == url }) { + self.openLink(link: link) + } + } + + private func openLink(link: TelegramBusinessChatLinks.Link) { + guard let component = self.component else { + return + } + + let contents = BusinessLinkChatContents( + context: component.context, + kind: .businessLinkSetup(link: link) + ) + let chatController = component.context.sharedContext.makeChatController( + context: component.context, + chatLocation: .customChatContents, + subject: .customChatContents(contents: contents), + botStart: nil, + mode: .standard(.default) + ) + chatController.navigationPresentation = .modal + self.environment?.controller()?.push(chatController) + } + + private func openDeleteLink(url: String) { + guard let component = self.component else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Delete Link", color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.accountData.deleteBusinessChatLink(url: url).startStandalone() + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.environment?.controller()?.present(actionSheet, in: .window(.root)) + } + + func update(component: BusinessLinksSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + self.links = component.initialData.businessLinks?.links ?? [] + self.linksDisposable = (component.context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.BusinessChatLinks(id: component.context.account.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak self] links in + guard let self else { + return + } + let links = links?.links ?? [] + if self.links != links { + self.links = links + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + }) + + self.refreshLinksDisposable = component.context.engine.accountData.refreshBusinessChatLinks().startStrict() + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + let alphaTransition: Transition + if !transition.animation.isImmediate { + alphaTransition = .easeInOut(duration: 0.25) + } else { + alphaTransition = .immediate + } + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let _ = alphaTransition + let _ = presentationData + + //TODO:localize + let navigationTitleSize = self.navigationTitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Links to Chat", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview == nil { + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + navigationBar.view.addSubview(navigationTitleView) + } + } + transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) + } + + let bottomContentInset: CGFloat = 24.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 24.0 + + var contentHeight: CGFloat = 0.0 + + contentHeight += environment.navigationHeight + + let _ = sectionSpacing + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "MapEmoji"), + loop: false + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 11.0), size: iconSize) + if let iconView = self.icon.view as? LottieComponent.View { + if iconView.superview == nil { + self.scrollView.addSubview(iconView) + iconView.playOnce() + } + transition.setPosition(view: iconView, position: iconFrame.center) + iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + } + + contentHeight += 129.0 + + let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Give your customers short links that start a chat with you — and suggest the first message from them to you.", attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { attributes in + return ("URL", "") + }), textAlignment: .center + )) + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(subtitleString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.25, + highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { _, _ in + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.scrollView.addSubview(subtitleView) + } + transition.setPosition(view: subtitleView, position: subtitleFrame.center) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + } + contentHeight += subtitleSize.height + contentHeight += 27.0 + + var createLinkSectionItems: [AnyComponentWithIdentity] = [] + createLinkSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Create a Link to Chat", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + name: "Item List/AddLinkIcon", + tintColor: environment.theme.list.itemAccentColor + ))), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + self.createLink() + } + )))) + + let footerText: String + if let addressName = component.initialData.accountPeer?.addressName, let phoneNumber = component.initialData.accountPeer?.phone { + footerText = "You can also use a simple link for a chat with you — [t.me/\(addressName)](username) or [t.me/\u{2060}+\u{2060}\(phoneNumber)](phone)." + } else if let addressName = component.initialData.accountPeer?.addressName { + footerText = "You can also use a simple link for a chat with you — [t.me/\(addressName)](username)." + } else if let phoneNumber = component.initialData.accountPeer?.phone { + footerText = "You can also use a simple link for a chat with you — [t.me/\u{2060}+\u{2060}\(phoneNumber)](phone)." + } else { + footerText = "" + } + let createLinkSectionSize = self.createLinkSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .markdown(text: footerText, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { link in + return ("URL", link) + } + )), + maximumNumberOfLines: 0, + lineSpacing: 0.25, + highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] attributes, _ in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let url = attributes[NSAttributedString.Key(rawValue: "URL")] as? String else { + return + } + + let linkValue: String + if url == "phone", let phoneNumber = component.initialData.accountPeer?.phone { + linkValue = "t.me/+\(phoneNumber)" + } else if url == "username", let addressName = component.initialData.accountPeer?.addressName { + linkValue = "t.me/\(addressName)" + } else { + return + } + UIPasteboard.general.string = linkValue + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + var animateAsReplacement = false + if let controller = environment.controller() { + controller.forEachController { c in + if let c = c as? UndoOverlayController { + animateAsReplacement = true + c.dismiss() + } + return true + } + } + //TODO:localize + let controller = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.GroupInfo_InviteLink_CopyAlert_Success), elevatedLayout: false, position: .bottom, animateInAsReplacement: animateAsReplacement, action: { _ in + return false + }) + environment.controller()?.present(controller, in: .current) + } + )), + items: createLinkSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let createLinkSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: createLinkSectionSize) + if let createLinkSectionView = self.createLinkSection.view { + if createLinkSectionView.superview == nil { + self.scrollView.addSubview(createLinkSectionView) + self.createLinkSection.parentState = state + } + transition.setFrame(view: createLinkSectionView, frame: createLinkSectionFrame) + } + contentHeight += createLinkSectionSize.height + contentHeight += sectionSpacing + + var linksSectionItems: [AnyComponentWithIdentity] = [] + for link in self.links { + linksSectionItems.append(AnyComponentWithIdentity(id: link.url, component: AnyComponent(BusinessLinkListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + link: link, + action: { [weak self] in + guard let self else { + return + } + self.openLink(url: link.url) + }, + deleteAction: { [weak self] in + guard let self else { + return + } + self.openDeleteLink(url: link.url) + } + )))) + } + let linksSectionSize = self.linksSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "LINKS TO CHAT", + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: linksSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let linksSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: linksSectionSize) + if let linksSectionView = self.linksSection.view { + if linksSectionView.superview == nil { + self.scrollView.addSubview(linksSectionView) + self.linksSection.parentState = state + } + transition.setFrame(view: linksSectionView, frame: linksSectionFrame) + alphaTransition.setAlpha(view: linksSectionView, alpha: self.links.isEmpty ? 0.0 : 1.0) + } + contentHeight += linksSectionSize.height + + contentHeight += bottomContentInset + contentHeight += environment.safeInsets.bottom + + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class BusinessLinksSetupScreen: ViewControllerComponentContainer { + public final class InitialData: BusinessLinksSetupScreenInitialData { + fileprivate let accountPeer: TelegramUser? + fileprivate let businessLinks: TelegramBusinessChatLinks? + + fileprivate init(accountPeer: TelegramUser?, businessLinks: TelegramBusinessChatLinks?) { + self.accountPeer = accountPeer + self.businessLinks = businessLinks + } + } + + private let context: AccountContext + + public init( + context: AccountContext, + initialData: InitialData + ) { + self.context = context + + super.init(context: context, component: BusinessLinksSetupScreenComponent( + context: context, + initialData: initialData + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.title = "" + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? BusinessLinksSetupScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? BusinessLinksSetupScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + public static func makeInitialData(context: AccountContext) -> Signal { + return context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + TelegramEngine.EngineData.Item.Peer.BusinessChatLinks(id: context.account.peerId) + ) + |> map { peer, businessLinks in + var accountPeer: TelegramUser? + if case let .user(user) = peer { + accountPeer = user + } + return InitialData( + accountPeer: accountPeer, + businessLinks: businessLinks + ) + } + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 84557799c5..79afffec25 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -271,7 +271,8 @@ final class QuickReplySetupScreenComponent: Component { commandPrefix: "/\(item.shortcut)", searchQuery: nil, messageCount: item.totalCount, - hideSeparator: false + hideSeparator: false, + hideDate: true ) )), editing: isEditing, diff --git a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD new file mode 100644 index 0000000000..174ee14b59 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BusinessLinkNameAlertController", + module_name = "BusinessLinkNameAlertController", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift new file mode 100644 index 0000000000..6ff36abfdc --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift @@ -0,0 +1,543 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import AccountContext +import ComponentFlow +import MultilineTextComponent +import BalancedTextComponent + +private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { + private var theme: PresentationTheme + private let backgroundNode: ASImageNode + private let textInputNode: EditableTextNode + private let placeholderNode: ASTextNode + private let characterLimitView = ComponentView() + + private let characterLimit: Int + + var updateHeight: (() -> Void)? + var complete: (() -> Void)? + var textChanged: ((String) -> Void)? + + private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) + private let inputInsets: UIEdgeInsets + + private let validCharacterSets: [CharacterSet] + + var text: String { + get { + return self.textInputNode.attributedText?.string ?? "" + } + set { + self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputTextColor) + self.placeholderNode.isHidden = !newValue.isEmpty + } + } + + var placeholder: String = "" { + didSet { + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + } + } + + init(theme: PresentationTheme, placeholder: String, characterLimit: Int) { + self.theme = theme + self.characterLimit = characterLimit + + self.inputInsets = UIEdgeInsets(top: 9.0, left: 6.0, bottom: 9.0, right: 16.0) + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + + self.textInputNode = EditableTextNode() + self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(13.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] + self.textInputNode.clipsToBounds = true + self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: self.inputInsets.left, bottom: self.inputInsets.bottom, right: self.inputInsets.right) + self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance + self.textInputNode.keyboardType = .default + self.textInputNode.autocapitalizationType = .none + self.textInputNode.returnKeyType = .done + self.textInputNode.autocorrectionType = .no + self.textInputNode.tintColor = theme.actionSheet.controlAccentColor + + self.placeholderNode = ASTextNode() + self.placeholderNode.isUserInteractionEnabled = false + self.placeholderNode.displaysAsynchronously = false + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + + self.validCharacterSets = [ + CharacterSet.alphanumerics, + CharacterSet(charactersIn: "0123456789_ "), + ] + + super.init() + + self.textInputNode.delegate = self + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textInputNode) + self.addSubnode(self.placeholderNode) + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: width) + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + + let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) + transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left + 5.0, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) + + transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) + + let characterLimitString: String + let characterLimitColor: UIColor + if self.text.count <= self.characterLimit { + let remaining = self.characterLimit - self.text.count + if remaining < 5 { + characterLimitString = "\(remaining)" + } else { + characterLimitString = " " + } + characterLimitColor = self.theme.list.itemPlaceholderTextColor + } else { + characterLimitString = "\(self.characterLimit - self.text.count)" + characterLimitColor = self.theme.list.itemDestructiveColor + } + + let characterLimitSize = self.characterLimitView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: characterLimitString, font: Font.regular(13.0), textColor: characterLimitColor)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let characterLimitComponentView = self.characterLimitView.view { + if characterLimitComponentView.superview == nil { + self.view.addSubview(characterLimitComponentView) + } + characterLimitComponentView.frame = CGRect(origin: CGPoint(x: width - 23.0 - characterLimitSize.width, y: 18.0), size: characterLimitSize) + } + + return panelHeight + } + + func activateInput() { + self.textInputNode.becomeFirstResponder() + } + + func deactivateInput() { + self.textInputNode.resignFirstResponder() + } + + @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.updateTextNodeText(animated: true) + self.textChanged?(editableTextNode.textView.text) + self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty + } + + func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if text == "\n" { + self.complete?() + return false + } + if text.unicodeScalars.contains(where: { c in + return !self.validCharacterSets.contains(where: { set in + return set.contains(c) + }) + }) { + return false + } + return true + } + + private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let unboundTextFieldHeight = max(34.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) + + return min(61.0, max(34.0, unboundTextFieldHeight)) + } + + private func updateTextNodeText(animated: Bool) { + let backgroundInsets = self.backgroundInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) + + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + if !self.bounds.size.height.isEqual(to: panelHeight) { + self.updateHeight?() + } + } + + @objc func clearPressed() { + self.textInputNode.attributedText = nil + self.deactivateInput() + } +} + +public final class BusinessLinkNameAlertContentNode: AlertContentNode { + private let context: AccountContext + private var theme: AlertControllerTheme + private let strings: PresentationStrings + private let text: String + private let subtext: String + private let titleFont: PromptControllerTitleFont + + private let textView = ComponentView() + private let subtextView = ComponentView() + + fileprivate let inputFieldNode: PromptInputFieldNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + private var errorText: String? + + private let hapticFeedback = HapticFeedback() + + var complete: (() -> Void)? + + override public var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, subtext: String, titleFont: PromptControllerTitleFont, value: String?, characterLimit: Int) { + self.context = context + self.theme = theme + self.strings = strings + self.text = text + self.subtext = subtext + self.titleFont = titleFont + + //TODO:localize + self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: "Name this link...", characterLimit: characterLimit) + self.inputFieldNode.text = value ?? "" + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.inputFieldNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + self.actionNodes.last?.actionEnabled = true + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.inputFieldNode.updateHeight = { [weak self] in + if let strongSelf = self { + if let _ = strongSelf.validLayout { + strongSelf.requestLayout?(.immediate) + } + } + } + + self.inputFieldNode.textChanged = { [weak self] text in + if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { + lastNode.actionEnabled = text.count <= characterLimit + strongSelf.requestLayout?(.immediate) + } + } + + self.updateTheme(theme) + + self.inputFieldNode.complete = { [weak self] in + guard let self else { + return + } + if let lastNode = self.actionNodes.last, lastNode.actionEnabled { + self.complete?() + } + } + } + + deinit { + self.disposable.dispose() + } + + var value: String { + return self.inputFieldNode.text + } + + public func setErrorText(errorText: String?) { + if self.errorText != errorText { + self.errorText = errorText + self.requestLayout?(.immediate) + } + + if errorText != nil { + HapticFeedback().error() + self.inputFieldNode.layer.addShakeAnimation() + } + } + + override public func updateTheme(_ theme: AlertControllerTheme) { + self.theme = theme + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) + + let hadValidLayout = self.validLayout != nil + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 16.0) + let spacing: CGFloat = 5.0 + let subtextSpacing: CGFloat = -1.0 + + let textSize = self.textView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: self.text, font: Font.semibold(17.0), textColor: self.theme.primaryColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: measureSize.width, height: 1000.0) + ) + let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: origin.y), size: textSize) + if let textComponentView = self.textView.view { + if textComponentView.superview == nil { + textComponentView.layer.anchorPoint = CGPoint() + self.view.addSubview(textComponentView) + } + textComponentView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + transition.updatePosition(layer: textComponentView.layer, position: textFrame.origin) + } + origin.y += textSize.height + 6.0 + subtextSpacing + + let subtextSize = self.subtextView.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: self.errorText ?? self.subtext, font: Font.regular(13.0), textColor: self.errorText != nil ? self.theme.destructiveColor : self.theme.primaryColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: measureSize.width, height: 1000.0) + ) + let subtextFrame = CGRect(origin: CGPoint(x: floor((size.width - subtextSize.width) * 0.5), y: origin.y), size: subtextSize) + if let subtextComponentView = self.subtextView.view { + if subtextComponentView.superview == nil { + subtextComponentView.layer.anchorPoint = CGPoint() + self.view.addSubview(subtextComponentView) + } + subtextComponentView.bounds = CGRect(origin: CGPoint(), size: subtextFrame.size) + transition.updatePosition(layer: subtextComponentView.layer, position: subtextFrame.origin) + } + origin.y += subtextSize.height + 6.0 + spacing + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) + + var contentWidth = max(textSize.width, minActionsWidth) + contentWidth = max(subtextSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + + let inputFieldWidth = resultWidth + let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) + let inputHeight = inputFieldHeight + let inputFieldFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight) + transition.updateFrame(node: self.inputFieldNode, frame: inputFieldFrame) + transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) + + let resultSize = CGSize(width: resultWidth, height: textSize.height + subtextSpacing + subtextSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if !hadValidLayout { + self.inputFieldNode.activateInput() + } + + return resultSize + } + + func animateError() { + self.inputFieldNode.layer.addShakeAnimation() + self.hapticFeedback.error() + } +} + +public enum PromptControllerTitleFont { + case regular + case bold +} + +public func businessLinkNameAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, text: String, subtext: String, titleFont: PromptControllerTitleFont = .regular, value: String?, characterLimit: Int = 1000, apply: @escaping (String?) -> Void) -> AlertController { + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + + var dismissImpl: ((Bool) -> Void)? + var applyImpl: (() -> Void)? + + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + apply(nil) + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { + applyImpl?() + })] + + let contentNode = BusinessLinkNameAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, subtext: subtext, titleFont: titleFont, value: value, characterLimit: characterLimit) + contentNode.complete = { + applyImpl?() + } + applyImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + apply(contentNode.value) + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + contentNode?.inputFieldNode.updateTheme(presentationData.theme) + }) + controller.dismissed = { _ in + presentationDataDisposable.dispose() + } + dismissImpl = { [weak controller] animated in + contentNode.inputFieldNode.deactivateInput() + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/BUILD b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/BUILD new file mode 100644 index 0000000000..70032c9159 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/BUILD @@ -0,0 +1,33 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerSelectionScreen", + module_name = "PeerSelectionScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/MergeLists", + "//submodules/ItemListUI", + "//submodules/PresentationDataUtils", + "//submodules/ContactsPeerItem", + "//submodules/TelegramUI/Components/ChatListHeaderComponent", + "//submodules/SearchBarNode", + "//submodules/Components/ViewControllerComponent", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift new file mode 100644 index 0000000000..b5649c1b55 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift @@ -0,0 +1,673 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import MergeLists +import ItemListUI +import PresentationDataUtils +import AccountContext +import ChatListHeaderComponent +import SearchBarNode +import ContactsPeerItem +import ViewControllerComponent +import ComponentFlow +import BalancedTextComponent +import MultilineTextComponent + +final class PeerSelectionScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialData: PeerSelectionScreen.InitialData + let completion: (PeerSelectionScreen.ChannelInfo) -> Void + + init( + context: AccountContext, + initialData: PeerSelectionScreen.InitialData, + completion: @escaping (PeerSelectionScreen.ChannelInfo) -> Void + ) { + self.context = context + self.initialData = initialData + self.completion = completion + } + + static func ==(lhs: PeerSelectionScreenComponent, rhs: PeerSelectionScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + + return true + } + + private enum ContentEntry: Comparable, Identifiable { + enum Id: Hashable { + case item(EnginePeer.Id) + } + + var stableId: Id { + switch self { + case let .item(peer, _, _): + return .item(peer.id) + } + } + + case item(peer: EnginePeer, subscriberCount: Int?, sortIndex: Int) + + static func <(lhs: ContentEntry, rhs: ContentEntry) -> Bool { + switch lhs { + case let .item(lhsPeer, _, lhsSortIndex): + switch rhs { + case let .item(rhsPeer, _, rhsSortIndex): + if lhsSortIndex != rhsSortIndex { + return lhsSortIndex < rhsSortIndex + } + return lhsPeer.id < rhsPeer.id + } + } + } + + func item(listNode: ContentListNode) -> ListViewItem { + switch self { + case let .item(peer, subscriberCount, _): + //TODO:localize + let statusText: String + if let subscriberCount, subscriberCount != 0 { + statusText = "\(subscriberCount) subscribers" + } else { + statusText = "channel" + } + + return ContactsPeerItem( + presentationData: ItemListPresentationData(listNode.presentationData), + style: .plain, + sectionId: 0, + sortOrder: listNode.presentationData.nameSortOrder, + displayOrder: listNode.presentationData.nameDisplayOrder, + context: listNode.context, + peerMode: .peer, + peer: .peer(peer: peer, chatPeer: peer), + status: .custom(string: statusText, multiline: false, isActive: false, icon: nil), + badge: nil, + requiresPremiumForMessaging: false, + enabled: true, + selection: .none, + selectionPosition: .left, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + options: [], + additionalActions: [], + actionIcon: .none, + index: nil, + header: nil, + action: { [weak listNode] _ in + guard let listNode, let parentView = listNode.parentView else { + return + } + parentView.peerSelected(peer: peer) + } + ) + } + } + } + + private final class ContentListNode: ListView { + weak var parentView: View? + let context: AccountContext + var presentationData: PresentationData + private var currentEntries: [ContentEntry] = [] + + init(parentView: View, context: AccountContext) { + self.parentView = parentView + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + + super.init() + } + + func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) { + let (listViewDuration, listViewCurve) = listViewAnimationDurationAndCurve(transition: transition.containedViewLayoutTransition) + self.transaction( + deleteIndices: [], + insertIndicesAndItems: [], + updateIndicesAndItems: [], + options: [.Synchronous, .LowLatency, .PreferSynchronousResourceLoading], + additionalScrollDistance: 0.0, + updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: listViewDuration, curve: listViewCurve), + updateOpaqueState: nil + ) + } + + func setEntries(entries: [ContentEntry], animated: Bool) { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries) + self.currentEntries = entries + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) } + + var options: ListViewDeleteAndInsertOptions = [.Synchronous, .LowLatency] + if animated { + options.insert(.AnimateInsertion) + } else { + options.insert(.PreferSynchronousResourceLoading) + } + + self.transaction( + deleteIndices: deletions, + insertIndicesAndItems: insertions, + updateIndicesAndItems: updates, + options: options, + scrollToItem: nil, + stationaryItemRange: nil, + updateOpaqueState: nil, + completion: { _ in + } + ) + } + } + + final class View: UIView { + private var emptyState: ComponentView? + private var contentListNode: ContentListNode? + private var emptySearchState: ComponentView? + + private let navigationBarView = ComponentView() + private var navigationHeight: CGFloat? + + private var searchBarNode: SearchBarNode? + + private var isUpdating: Bool = false + + private var component: PeerSelectionScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var channels: [PeerSelectionScreen.ChannelInfo] = [] + private var channelsDisposable: Disposable? + + private var isSearchDisplayControllerActive: Bool = false + private var searchQuery: String = "" + private let searchQueryComponentSeparationCharacterSet: CharacterSet + + override init(frame: CGRect) { + self.searchQueryComponentSeparationCharacterSet = CharacterSet(charactersIn: " _.:/") + + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.channelsDisposable?.dispose() + } + + func scrollToTop() { + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + return true + } + + func peerSelected(peer: EnginePeer) { + guard let component = self.component, let environment = self.environment else { + return + } + guard let channel = self.channels.first(where: { $0.peer.id == peer.id }) else { + return + } + component.completion(channel) + environment.controller()?.dismiss() + } + + private func updateNavigationBar( + component: PeerSelectionScreenComponent, + theme: PresentationTheme, + strings: PresentationStrings, + size: CGSize, + insets: UIEdgeInsets, + statusBarHeight: CGFloat, + isModal: Bool, + transition: Transition, + deferScrollApplication: Bool + ) -> CGFloat { + let rightButtons: [AnyComponentWithIdentity] = [] + + //TODO:localize + let closeTitle: String = strings.Common_Cancel + + let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content( + title: "", + navigationBackTitle: nil, + titleComponent: AnyComponent(VStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Personal Channel", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)) + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "select your channel", font: Font.regular(12.0), textColor: theme.rootController.navigationBar.secondaryTextColor)) + ))) + ], spacing: 2.0)), + chatListTitle: nil, + leftButton: isModal ? AnyComponentWithIdentity(id: "close", component: AnyComponent(NavigationButtonComponent( + content: .text(title: closeTitle, isBold: false), + pressed: { [weak self] _ in + guard let self else { + return + } + if self.attemptNavigation(complete: {}) { + self.environment?.controller()?.dismiss() + } + } + ))) : nil, + rightButtons: rightButtons, + backTitle: isModal ? nil : strings.Common_Back, + backPressed: { [weak self] in + guard let self else { + return + } + + if self.attemptNavigation(complete: {}) { + self.environment?.controller()?.dismiss() + } + } + ) + + let navigationBarSize = self.navigationBarView.update( + transition: transition, + component: AnyComponent(ChatListNavigationBar( + context: component.context, + theme: theme, + strings: strings, + statusBarHeight: statusBarHeight, + sideInset: insets.left, + isSearchActive: self.isSearchDisplayControllerActive, + isSearchEnabled: true, + primaryContent: headerContent, + secondaryContent: nil, + secondaryTransition: 0.0, + storySubscriptions: nil, + storiesIncludeHidden: false, + uploadProgress: [:], + tabsNode: nil, + tabsNodeIsSearch: false, + accessoryPanelContainer: nil, + accessoryPanelContainerHeight: 0.0, + activateSearch: { [weak self] _ in + guard let self else { + return + } + + self.isSearchDisplayControllerActive = true + self.state?.updated(transition: .spring(duration: 0.4)) + }, + openStatusSetup: { _ in + }, + allowAutomaticOrder: { + } + )), + environment: {}, + containerSize: size + ) + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + if deferScrollApplication { + navigationBarComponentView.deferScrollApplication = true + } + + if navigationBarComponentView.superview == nil { + self.addSubview(navigationBarComponentView) + } + transition.setFrame(view: navigationBarComponentView, frame: CGRect(origin: CGPoint(), size: navigationBarSize)) + + return navigationBarSize.height + } else { + return 0.0 + } + } + + private func updateNavigationScrolling(navigationHeight: CGFloat, transition: Transition) { + var mainOffset: CGFloat + if let contentListNode = self.contentListNode { + switch contentListNode.visibleContentOffset() { + case .none: + mainOffset = 0.0 + case .unknown: + mainOffset = navigationHeight + case let .known(value): + mainOffset = value + } + } else { + mainOffset = navigationHeight + } + + mainOffset = min(mainOffset, ChatListNavigationBar.searchScrollHeight) + if abs(mainOffset) < 0.1 { + mainOffset = 0.0 + } + + let resultingOffset = mainOffset + + var offset = resultingOffset + if self.isSearchDisplayControllerActive { + offset = 0.0 + } + + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: false, forceUpdate: false, transition: transition.withUserData(ChatListNavigationBar.AnimationHint( + disableStoriesAnimations: false, + crossfadeStoryPeers: false + ))) + } + } + + func update(component: PeerSelectionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + self.channelsDisposable = (component.context.engine.peers.adminedPublicChannels(scope: .forPersonalProfile) + |> deliverOnMainQueue).startStrict(next: { [weak self] peers in + guard let self else { + return + } + self.channels = peers.map { peer in + return PeerSelectionScreen.ChannelInfo(peer: peer.peer, subscriberCount: peer.subscriberCount) + } + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + }) + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + let alphaTransition: Transition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) + let _ = alphaTransition + + if themeUpdated { + self.backgroundColor = environment.theme.list.plainBackgroundColor + } + + var isModal = false + if let controller = environment.controller(), controller.navigationPresentation == .modal { + isModal = true + } + + var statusBarHeight = environment.statusBarHeight + if isModal { + statusBarHeight = max(statusBarHeight, 1.0) + } + + var listBottomInset = environment.safeInsets.bottom + environment.additionalInsets.bottom + listBottomInset = max(listBottomInset, environment.inputHeight) + let navigationHeight = self.updateNavigationBar( + component: component, + theme: environment.theme, + strings: environment.strings, + size: availableSize, + insets: environment.safeInsets, + statusBarHeight: statusBarHeight, + isModal: isModal, + transition: transition, + deferScrollApplication: true + ) + self.navigationHeight = navigationHeight + + var removedSearchBar: SearchBarNode? + if self.isSearchDisplayControllerActive { + let searchBarNode: SearchBarNode + var searchBarTransition = transition + if let current = self.searchBarNode { + searchBarNode = current + } else { + searchBarTransition = .immediate + let searchBarTheme = SearchBarNodeTheme(theme: environment.theme, hasSeparator: false) + searchBarNode = SearchBarNode( + theme: searchBarTheme, + strings: environment.strings, + fieldStyle: .modern, + displayBackground: false + ) + searchBarNode.placeholderString = NSAttributedString(string: environment.strings.Common_Search, font: Font.regular(17.0), textColor: searchBarTheme.placeholder) + self.searchBarNode = searchBarNode + searchBarNode.cancel = { [weak self] in + guard let self else { + return + } + self.isSearchDisplayControllerActive = false + self.state?.updated(transition: .spring(duration: 0.4)) + } + searchBarNode.textUpdated = { [weak self] query, _ in + guard let self else { + return + } + if self.searchQuery != query { + self.searchQuery = query.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + self.state?.updated(transition: .immediate) + } + } + DispatchQueue.main.async { [weak self, weak searchBarNode] in + guard let self, let searchBarNode, self.searchBarNode === searchBarNode else { + return + } + searchBarNode.activate() + } + } + + var searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight - 54.0 + 2.0), size: CGSize(width: availableSize.width, height: 54.0)) + if isModal { + searchBarFrame.origin.y += 2.0 + } + searchBarNode.updateLayout(boundingSize: searchBarFrame.size, leftInset: environment.safeInsets.left + 6.0, rightInset: environment.safeInsets.right, transition: searchBarTransition.containedViewLayoutTransition) + searchBarTransition.setFrame(view: searchBarNode.view, frame: searchBarFrame) + if searchBarNode.view.superview == nil { + self.addSubview(searchBarNode.view) + + if case let .curve(duration, curve) = transition.animation, let navigationBarView = self.navigationBarView.view as? ChatListNavigationBar.View, let placeholderNode = navigationBarView.searchContentNode?.placeholderNode { + let timingFunction: String + switch curve { + case .easeInOut: + timingFunction = CAMediaTimingFunctionName.easeOut.rawValue + case .linear: + timingFunction = CAMediaTimingFunctionName.linear.rawValue + case .spring: + timingFunction = kCAMediaTimingFunctionSpring + case .custom: + timingFunction = kCAMediaTimingFunctionSpring + } + + searchBarNode.animateIn(from: placeholderNode, duration: duration, timingFunction: timingFunction) + } + } + } else { + self.searchQuery = "" + if let searchBarNode = self.searchBarNode { + self.searchBarNode = nil + removedSearchBar = searchBarNode + } + } + + let contentListNode: ContentListNode + if let current = self.contentListNode { + contentListNode = current + } else { + contentListNode = ContentListNode(parentView: self, context: component.context) + self.contentListNode = contentListNode + + contentListNode.visibleContentOffsetChanged = { [weak self] offset in + guard let self else { + return + } + guard let navigationHeight = self.navigationHeight else { + return + } + + self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: .immediate) + } + + if let navigationBarComponentView = self.navigationBarView.view { + self.insertSubview(contentListNode.view, belowSubview: navigationBarComponentView) + } else { + self.addSubview(contentListNode.view) + } + } + + transition.setFrame(view: contentListNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + contentListNode.update(size: availableSize, insets: UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: listBottomInset, right: environment.safeInsets.right), transition: transition) + + var entries: [ContentEntry] = [] + for channel in self.channels { + if !self.searchQuery.isEmpty { + var matches = false + inner: for nameComponent in channel.peer.compactDisplayTitle.lowercased().components(separatedBy: self.searchQueryComponentSeparationCharacterSet) { + if nameComponent.lowercased().hasPrefix(self.searchQuery) { + matches = true + break inner + } + } + if !matches { + continue + } + } + entries.append(.item(peer: channel.peer, subscriberCount: channel.subscriberCount, sortIndex: entries.count)) + } + contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate) + + if !self.searchQuery.isEmpty && entries.isEmpty { + var emptySearchStateTransition = transition + let emptySearchState: ComponentView + if let current = self.emptySearchState { + emptySearchState = current + } else { + emptySearchStateTransition = emptySearchStateTransition.withAnimation(.none) + emptySearchState = ComponentView() + self.emptySearchState = emptySearchState + } + let emptySearchStateSize = emptySearchState.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Conversation_SearchNoResults, font: Font.regular(17.0), textColor: environment.theme.list.freeTextColor, paragraphAlignment: .center)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: availableSize.height) + ) + var emptySearchStateBottomInset = listBottomInset + emptySearchStateBottomInset = max(emptySearchStateBottomInset, environment.inputHeight) + let emptySearchStateFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - emptySearchStateSize.width) * 0.5), y: navigationHeight + floor((availableSize.height - emptySearchStateBottomInset - navigationHeight) * 0.5)), size: emptySearchStateSize) + if let emptySearchStateView = emptySearchState.view { + if emptySearchStateView.superview == nil { + if let navigationBarComponentView = self.navigationBarView.view { + self.insertSubview(emptySearchStateView, belowSubview: navigationBarComponentView) + } else { + self.addSubview(emptySearchStateView) + } + } + emptySearchStateTransition.containedViewLayoutTransition.updatePosition(layer: emptySearchStateView.layer, position: emptySearchStateFrame.center) + emptySearchStateView.bounds = CGRect(origin: CGPoint(), size: emptySearchStateFrame.size) + } + } else if let emptySearchState = self.emptySearchState { + self.emptySearchState = nil + emptySearchState.view?.removeFromSuperview() + } + + self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: transition) + + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarComponentView.deferScrollApplication = false + navigationBarComponentView.applyCurrentScroll(transition: transition) + } + + if let removedSearchBar { + if !transition.animation.isImmediate, let navigationBarView = self.navigationBarView.view as? ChatListNavigationBar.View, let placeholderNode = + navigationBarView.searchContentNode?.placeholderNode { + removedSearchBar.transitionOut(to: placeholderNode, transition: transition.containedViewLayoutTransition, completion: { [weak removedSearchBar] in + removedSearchBar?.view.removeFromSuperview() + }) + } else { + removedSearchBar.view.removeFromSuperview() + } + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class PeerSelectionScreen: ViewControllerComponentContainer { + public final class InitialData { + init() { + } + } + + public struct ChannelInfo: Equatable { + public var peer: EnginePeer + public var subscriberCount: Int? + + public init(peer: EnginePeer, subscriberCount: Int?) { + self.peer = peer + self.subscriberCount = subscriberCount + } + } + + private let context: AccountContext + + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, completion: @escaping (ChannelInfo) -> Void) { + self.context = context + + super.init(context: context, component: PeerSelectionScreenComponent( + context: context, + initialData: InitialData(), + completion: completion + ), navigationBarAppearance: .none, theme: .default, updatedPresentationData: updatedPresentationData) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? PeerSelectionScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? PeerSelectionScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index f23ef6eb2a..e32a2575a9 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -442,14 +442,14 @@ public final class PeerListItemComponent: Component { } transition.setBounds(view: self.containerButton, bounds: CGRect(origin: CGPoint(x: -offset, y: 0.0), size: self.containerButton.bounds.size)) } - self.swipeOptionContainer.revealOptionSelected = { [weak self] option, animated in + self.swipeOptionContainer.revealOptionSelected = { [weak self] option, _ in guard let self, let component = self.component else { return } guard let inlineActions = component.inlineActions else { return } - self.swipeOptionContainer.setRevealOptionsOpened(false, animated: animated) + self.swipeOptionContainer.setRevealOptionsOpened(false, animated: true) if let inlineAction = inlineActions.actions.first(where: { $0.id == option.key }) { inlineAction.action() } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/BusinessLink.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/BusinessLink.imageset/Contents.json new file mode 100644 index 0000000000..035fb8512f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/BusinessLink.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "linktichat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/BusinessLink.imageset/linktichat.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/BusinessLink.imageset/linktichat.pdf new file mode 100644 index 0000000000..12f43a0f2f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/BusinessLink.imageset/linktichat.pdf @@ -0,0 +1,217 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 100.500000 100.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +-1.000000 -0.000000 -0.000000 1.000000 91.000000 22.727596 cm +0.000000 0.000000 0.000000 scn +0.000000 27.611782 m +0.000000 40.163708 11.192883 50.339054 25.000000 50.339054 c +38.807121 50.339054 50.000000 40.163708 50.000000 27.611782 c +50.000000 20.453026 46.656067 14.585060 40.965954 10.419197 c +40.238289 9.886456 39.568069 6.926376 41.190041 4.439991 c +42.812008 1.953606 44.834194 0.817352 43.820072 0.391979 c +43.194855 0.129726 39.500584 0.000000 36.834217 1.495331 c +33.021576 3.633507 31.955683 5.762112 31.135277 5.573940 c +29.172245 5.123688 27.116848 4.884510 25.000000 4.884510 c +11.192883 4.884510 0.000000 15.059856 0.000000 27.611782 c +h +f +n +Q +q +-1.000000 -0.000000 -0.000000 1.000000 41.000000 8.000000 cm +0.000000 0.000000 0.000000 scn +0.908279 25.449921 m +0.000000 23.667324 0.000000 21.333771 0.000000 16.666668 c +0.000000 13.333334 l +0.000000 8.666229 0.000000 6.332676 0.908279 4.550079 c +1.707224 2.982061 2.982062 1.707224 4.550079 0.908279 c +6.332677 0.000000 8.666228 0.000000 13.333332 0.000000 c +16.666666 0.000000 l +21.333771 0.000000 23.667324 0.000000 25.449921 0.908279 c +27.017939 1.707224 28.292776 2.982061 29.091721 4.550079 c +30.000000 6.332676 30.000000 8.666229 30.000000 13.333332 c +30.000000 16.666668 l +30.000000 21.333771 30.000000 23.667324 29.091721 25.449921 c +28.292776 27.017939 27.017939 28.292776 25.449921 29.091721 c +23.667324 30.000000 21.333771 30.000000 16.666668 30.000000 c +13.333333 30.000000 l +8.666229 30.000000 6.332677 30.000000 4.550079 29.091721 c +2.982062 28.292776 1.707224 27.017939 0.908279 25.449921 c +h +7.326992 22.673008 m +8.943098 24.289116 11.563326 24.289116 13.179433 22.673008 c +15.679433 20.173008 l +16.134394 19.718046 16.872030 19.718046 17.326992 20.173008 c +17.781952 20.627968 17.781952 21.365604 17.326992 21.820566 c +14.826992 24.320566 l +12.300963 26.846596 8.205462 26.846596 5.679434 24.320566 c +3.153406 21.794537 3.153406 17.699036 5.679434 15.173008 c +8.179434 12.673008 l +10.705462 10.146980 14.800962 10.146980 17.326992 12.673008 c +17.660618 13.006634 17.950953 13.368685 18.197357 13.751846 c +18.545376 14.293015 18.388796 15.013842 17.847628 15.361858 c +17.306459 15.709876 16.585632 15.553297 16.237616 15.012129 c +16.080584 14.767944 15.894735 14.535869 15.679433 14.320566 c +14.063326 12.704460 11.443098 12.704460 9.826992 14.320566 c +7.326992 16.820566 l +5.710886 18.436674 5.710886 21.056900 7.326992 22.673008 c +h +22.677927 7.320566 m +21.061819 5.704458 18.441591 5.704458 16.825483 7.320566 c +14.325483 9.820566 l +13.870523 10.275528 13.132886 10.275528 12.677925 9.820566 c +12.222964 9.365606 12.222964 8.627968 12.677925 8.173008 c +15.177925 5.673008 l +17.703955 3.146978 21.799456 3.146978 24.325485 5.673008 c +26.851515 8.199036 26.851515 12.294538 24.325485 14.820567 c +21.825485 17.320568 l +19.299456 19.846596 15.203954 19.846596 12.677925 17.320568 c +12.344298 16.986942 12.053965 16.624889 11.807559 16.241730 c +11.459541 15.700561 11.616121 14.979733 12.157289 14.631717 c +12.698457 14.283699 13.419284 14.440279 13.767301 14.981446 c +13.924333 15.225631 14.110182 15.457706 14.325483 15.673009 c +15.941591 17.289116 18.561819 17.289116 20.177927 15.673009 c +22.677927 13.173008 l +24.294033 11.556900 24.294033 8.936672 22.677927 7.320566 c +h +f* +n +Q + +endstream +endobj + +2 0 obj + 3294 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 100.500000 100.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +-1.000000 -0.000000 -0.000000 1.000000 100.500000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 62.666668 m +0.000000 75.734558 0.000000 82.268509 2.543181 87.259781 c +4.780226 91.650230 8.349772 95.219772 12.740221 97.456818 c +17.731495 100.000000 24.265440 100.000000 37.333332 100.000000 c +62.666668 100.000000 l +75.734558 100.000000 82.268509 100.000000 87.259781 97.456818 c +91.650230 95.219772 95.219772 91.650230 97.456818 87.259781 c +100.000000 82.268509 100.000000 75.734558 100.000000 62.666668 c +100.000000 37.333332 l +100.000000 24.265442 100.000000 17.731491 97.456818 12.740219 c +95.219772 8.349770 91.650230 4.780228 87.259781 2.543182 c +82.268509 0.000000 75.734558 0.000000 62.666668 0.000000 c +37.333332 0.000000 l +24.265440 0.000000 17.731495 0.000000 12.740221 2.543182 c +8.349772 4.780228 4.780226 8.349770 2.543181 12.740219 c +0.000000 17.731491 0.000000 24.265442 0.000000 37.333332 c +0.000000 62.666668 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 972 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 100.500000 100.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000003554 00000 n +0000003577 00000 n +0000004799 00000 n +0000004821 00000 n +0000005119 00000 n +0000005221 00000 n +0000005242 00000 n +0000005417 00000 n +0000005491 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +5551 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Item List/AddLinkIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/AddLinkIcon.imageset/Contents.json new file mode 100644 index 0000000000..ad95bc0690 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/AddLinkIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "addlink_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/AddLinkIcon.imageset/addlink_30.pdf b/submodules/TelegramUI/Images.xcassets/Item List/AddLinkIcon.imageset/addlink_30.pdf new file mode 100644 index 0000000000..e128be50f0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/AddLinkIcon.imageset/addlink_30.pdf @@ -0,0 +1,119 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.834961 3.695801 cm +0.000000 0.000000 0.000000 scn +4.415000 24.469238 m +4.782269 24.469238 5.080000 24.171507 5.080000 23.804237 c +5.080000 20.719238 l +8.165000 20.719238 l +8.532269 20.719238 8.830000 20.421509 8.830000 20.054237 c +8.830000 19.686970 8.532269 19.389238 8.165000 19.389238 c +5.080000 19.389238 l +5.080000 16.304237 l +5.080000 15.936969 4.782269 15.639238 4.415000 15.639238 c +4.047730 15.639238 3.750000 15.936969 3.750000 16.304237 c +3.750000 19.389238 l +0.665000 19.389238 l +0.297731 19.389238 0.000000 19.686970 0.000000 20.054237 c +0.000000 20.421509 0.297731 20.719238 0.665000 20.719238 c +3.750000 20.719238 l +3.750000 23.804237 l +3.750000 24.171507 4.047730 24.469238 4.415000 24.469238 c +h +14.636975 19.330723 m +16.448345 21.142092 19.385155 21.142092 21.196522 19.330723 c +23.007893 17.519352 23.007893 14.582543 21.196522 12.771174 c +18.696524 10.271174 l +16.885155 8.459805 13.948345 8.459805 12.136976 10.271174 c +11.896282 10.511868 11.688011 10.771835 11.511801 11.045843 c +11.313148 11.354750 10.901689 11.444128 10.592781 11.245475 c +10.283875 11.046821 10.194496 10.635362 10.393150 10.326454 c +10.620377 9.973117 10.888288 9.638957 11.196524 9.330723 c +13.527290 6.999956 17.306210 6.999956 19.636974 9.330723 c +22.136974 11.830723 l +24.467743 14.161489 24.467743 17.940407 22.136974 20.271173 c +19.806210 22.601940 16.027290 22.601940 13.696524 20.271173 c +11.196524 17.771175 l +10.936825 17.511475 10.936825 17.090420 11.196524 16.830723 c +11.456223 16.571024 11.877277 16.571024 12.136976 16.830723 c +14.636975 19.330723 l +h +11.697906 3.271173 m +9.886538 1.459805 6.949728 1.459805 5.138359 3.271173 c +3.326989 5.082542 3.326989 8.019352 5.138359 9.830723 c +7.638359 12.330723 l +9.449728 14.142093 12.386538 14.142093 14.197906 12.330723 c +14.438601 12.090030 14.646872 11.830062 14.823082 11.556054 c +15.021735 11.247148 15.433194 11.157769 15.742102 11.356423 c +16.051008 11.555077 16.140387 11.966536 15.941732 12.275443 c +15.714506 12.628781 15.446593 12.962940 15.138359 13.271175 c +12.807592 15.601942 9.028673 15.601942 6.697906 13.271175 c +4.197906 10.771174 l +1.867140 8.440407 1.867140 4.661488 4.197906 2.330721 c +6.528673 -0.000046 10.307592 -0.000046 12.638359 2.330721 c +15.138359 4.830723 l +15.398058 5.090420 15.398057 5.511475 15.138359 5.771173 c +14.878660 6.030872 14.457605 6.030872 14.197906 5.771173 c +11.697906 3.271173 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2459 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002549 00000 n +0000002572 00000 n +0000002745 00000 n +0000002819 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2878 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/ChatLinks.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/ChatLinks.imageset/Contents.json new file mode 100644 index 0000000000..53ce79613f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/ChatLinks.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "linktochat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/ChatLinks.imageset/linktochat.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/ChatLinks.imageset/linktochat.pdf new file mode 100644 index 0000000000..ff94f7e126 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/ChatLinks.imageset/linktochat.pdf @@ -0,0 +1,195 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.000000 4.484375 cm +0.000000 0.000000 0.000000 scn +10.000000 20.135742 m +15.522848 20.135742 20.000000 16.065603 20.000000 11.044833 c +20.000000 6.024063 15.522848 1.953924 10.000000 1.953924 c +9.153261 1.953924 8.331102 2.049595 7.545889 2.229696 c +7.397357 2.263765 7.228708 2.107983 6.947171 1.847927 c +6.606689 1.533424 6.101101 1.066412 5.266314 0.598253 c +4.199766 0.000120 2.722059 0.052011 2.471971 0.156912 c +2.232201 0.257484 2.416753 0.457399 2.741760 0.809465 c +2.966608 1.053034 3.258681 1.369423 3.523984 1.776117 c +4.172771 2.770672 3.904685 3.954702 3.613619 4.167799 c +1.337573 5.834144 0.000000 8.181331 0.000000 11.044833 c +0.000000 16.065603 4.477152 20.135742 10.000000 20.135742 c +h +14.116262 15.129036 m +13.242537 16.002762 11.825946 16.002762 10.952220 15.129036 c +9.618887 13.795702 l +9.388043 13.564859 9.013773 13.564859 8.782929 13.795702 c +8.552086 14.026546 8.552086 14.400816 8.782929 14.631660 c +10.116262 15.964993 l +11.451675 17.300406 13.616807 17.300406 14.952219 15.964993 c +16.287632 14.629580 16.287632 12.464449 14.952219 11.129036 c +13.618887 9.795703 l +12.283474 8.460291 10.118341 8.460291 8.782929 9.795703 c +8.606529 9.972102 8.453041 10.163509 8.322783 10.366060 c +8.146202 10.640644 8.225649 11.006386 8.500233 11.182966 c +8.774817 11.359548 9.140558 11.280101 9.317140 11.005517 c +9.402049 10.873483 9.502523 10.748023 9.618887 10.631660 c +10.492613 9.757934 11.909203 9.757934 12.782929 10.631660 c +14.116262 11.964993 l +14.989988 12.838720 14.989988 14.255310 14.116262 15.129036 c +h +5.886345 6.898323 m +6.760071 6.024597 8.176661 6.024597 9.050387 6.898323 c +10.383720 8.231657 l +10.614564 8.462500 10.988834 8.462500 11.219678 8.231657 c +11.450521 8.000813 11.450521 7.626543 11.219679 7.395700 c +9.886345 6.062366 l +8.550932 4.726954 6.385800 4.726954 5.050388 6.062366 c +3.714975 7.397779 3.714975 9.562910 5.050388 10.898323 c +6.383721 12.231657 l +7.719133 13.567070 9.884266 13.567070 11.219678 12.231657 c +11.396078 12.055257 11.549566 11.863850 11.679824 11.661299 c +11.856405 11.386715 11.776958 11.020973 11.502375 10.844393 c +11.227790 10.667811 10.862049 10.747258 10.685468 11.021843 c +10.600558 11.153877 10.500084 11.279336 10.383720 11.395700 c +9.509995 12.269425 8.093405 12.269425 7.219678 11.395700 c +5.886345 10.062366 l +5.012619 9.188640 5.012619 7.772050 5.886345 6.898323 c +h +f* +n +Q + +endstream +endobj + +2 0 obj + 2424 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 944 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000002682 00000 n +0000002705 00000 n +0000003897 00000 n +0000003919 00000 n +0000004217 00000 n +0000004319 00000 n +0000004340 00000 n +0000004513 00000 n +0000004587 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +4647 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift new file mode 100644 index 0000000000..86ba99eae0 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift @@ -0,0 +1,238 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import ChatPresentationInterfaceState +import ComponentFlow +import AvatarNode +import MultilineTextComponent +import PlainButtonComponent +import ComponentDisplayAdapters +import AccountContext +import TelegramCore +import SwiftSignalKit +import UndoUI +import ShareController + +private final class ChatBusinessLinkTitlePanelComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let insets: UIEdgeInsets + let copyAction: () -> Void + let shareAction: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + insets: UIEdgeInsets, + copyAction: @escaping () -> Void, + shareAction: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.insets = insets + self.copyAction = copyAction + self.shareAction = shareAction + } + + static func ==(lhs: ChatBusinessLinkTitlePanelComponent, rhs: ChatBusinessLinkTitlePanelComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings != rhs.strings { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + + final class View: UIView { + private let copyButton = ComponentView() + private let shareButton = ComponentView() + + private var component: ChatBusinessLinkTitlePanelComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ChatBusinessLinkTitlePanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let size = CGSize(width: availableSize.width, height: 40.0) + + let copyButtonSize = self.copyButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Copy Link", font: Font.regular(17.0), textColor: component.theme.rootController.navigationBar.accentTextColor)) + )), + effectAlignment: .center, + minSize: CGSize(width: floor(availableSize.width * 0.5), height: size.height), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.copyAction() + }, + animateAlpha: true, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: availableSize + ) + if let copyButtonView = self.copyButton.view { + if copyButtonView.superview == nil { + self.addSubview(copyButtonView) + } + transition.setFrame(view: copyButtonView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: copyButtonSize)) + } + + let shareButtonSize = self.shareButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Share Link", font: Font.regular(17.0), textColor: component.theme.rootController.navigationBar.accentTextColor)) + )), + effectAlignment: .center, + minSize: CGSize(width: floor(availableSize.width * 0.5), height: size.height), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.shareAction() + }, + animateAlpha: true, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: availableSize + ) + if let shareButtonView = self.shareButton.view { + if shareButtonView.superview == nil { + self.addSubview(shareButtonView) + } + transition.setFrame(view: shareButtonView, frame: CGRect(origin: CGPoint(x: floor(availableSize.width * 0.5), y: 0.0), size: shareButtonSize)) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class ChatBusinessLinkTitlePanelNode: ChatTitleAccessoryPanelNode { + private let context: AccountContext + private let separatorNode: ASDisplayNode + private let content = ComponentView() + + private var theme: PresentationTheme? + private var link: TelegramBusinessChatLinks.Link? + + init(context: AccountContext) { + self.context = context + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.separatorNode) + } + + private func copyAction() { + guard let link = self.link, let interfaceInteraction = self.interfaceInteraction else { + return + } + + UIPasteboard.general.string = link.url + + let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }) + + //TODO:localize + let controller = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.GroupInfo_InviteLink_CopyAlert_Success), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { _ in + return false + }) + interfaceInteraction.presentControllerInCurrent(controller, nil) + } + + private func shareAction() { + guard let link = self.link, let interfaceInteraction = self.interfaceInteraction else { + return + } + + interfaceInteraction.presentController(ShareController(context: self.context, subject: .url(link.url), showInChat: nil, externalShare: false, immediateExternalShare: false), nil) + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { + switch interfaceState.subject { + case let .customChatContents(customChatContents): + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case let .businessLinkSetup(link): + self.link = link + } + default: + break + } + + if interfaceState.theme !== self.theme { + self.theme = interfaceState.theme + + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor + } + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + + let contentSize = self.content.update( + transition: Transition(transition), + component: AnyComponent(ChatBusinessLinkTitlePanelComponent( + context: self.context, + theme: interfaceState.theme, + strings: interfaceState.strings, + insets: UIEdgeInsets(top: 0.0, left: leftInset, bottom: 0.0, right: rightInset), + copyAction: { [weak self] in + self?.copyAction() + }, + shareAction: { [weak self] in + self?.shareAction() + } + )), + environment: {}, + containerSize: CGSize(width: width, height: 1000.0) + ) + if let contentView = self.content.view { + if contentView.superview == nil { + self.view.addSubview(contentView) + } + transition.updateFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: contentSize)) + } + + return LayoutResult(backgroundHeight: contentSize.height, insetHeight: contentSize.height, hitTestSlop: 0.0) + + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 68b63a72b6..29c5fc2bf9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -663,6 +663,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.stickerSettings = ChatInterfaceStickerSettings() self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: mode, chatLocation: chatLocation, subject: subject, peerNearbyData: peerNearbyData, greetingData: context.prefetchManager?.preloadedGreetingSticker, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) + + if case let .customChatContents(customChatContents) = subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case let .businessLinkSetup(link): + if !link.message.isEmpty { + self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState({ interfaceState in + return interfaceState.withUpdatedEffectiveInputState(ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(link.message, entities: link.entities))) + }) + } + } + } + self.presentationInterfaceStatePromise = ValuePromise(self.presentationInterfaceState) var mediaAccessoryPanelVisibility = MediaAccessoryPanelVisibility.none @@ -745,6 +759,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) ]), in: .window(.root)) + return false + } + case let .businessLinkSetup(link): + let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText + let entities = generateTextEntities(inputText.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(inputText, maxAnimatedEmojisInText: 0)) + + let message = inputText.string + + if message != link.message || entities != link.entities { + //TODO:localize + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "You have unsaved changes. Reset?", actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: "Reset", action: { [weak strongSelf] in + strongSelf?.dismiss() + }) + ]), in: .window(.root)) + return false } } @@ -6271,6 +6302,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .away: self.chatTitleView?.titleContent = .custom(self.presentationData.strings.QuickReply_TitleAwayMessage, nil, false) } + case let .businessLinkSetup(link): + let linkUrl: String + if link.url.hasPrefix("https://") { + linkUrl = String(link.url[link.url.index(link.url.startIndex, offsetBy: "https://".count)...]) + } else { + linkUrl = link.url + } + + //TODO:localize + self.chatTitleView?.titleContent = .custom(link.title ?? "Link to Chat", linkUrl, false) } } else { self.chatTitleView?.titleContent = .custom(" ", nil, false) @@ -8403,8 +8444,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) } else if case let .customChatContents(customChatContents) = strongSelf.subject { - customChatContents.enqueueMessages(messages: messages) - strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + switch customChatContents.kind { + case .quickReplyMessageInput: + customChatContents.enqueueMessages(messages: messages) + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + case let .businessLinkSetup(link): + var text: String = "" + var entities: [MessageTextEntity] = [] + if let message = messages.first { + if case let .message(textValue, attributes, _, _, _, _, _, _, _, _) = message { + text = textValue + for attribute in attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + entities = attribute.entities + } + } + } + } + + let _ = strongSelf.context.engine.accountData.editBusinessChatLink(url: link.url, message: text, entities: entities, title: link.title).start() + if case let .customChatContents(customChatContents) = strongSelf.subject { + customChatContents.businessLinkUpdate(message: text, entities: entities, title: link.title) + } + + //TODO:localize + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: "Preset message saved.", timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + } } strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) diff --git a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift index 57a7666b2a..e15cd2ba87 100644 --- a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift +++ b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift @@ -8,6 +8,7 @@ import Display import TelegramPresentationData import PresentationDataUtils import QuickReplyNameAlertController +import BusinessLinkNameAlertController extension ChatControllerImpl { func editChat() { @@ -59,6 +60,50 @@ extension ChatControllerImpl { } } self.present(alertController, in: .window(.root)) + } else if case let .customChatContents(customChatContents) = self.subject, case let .businessLinkSetup(link) = customChatContents.kind { + let currentValue = link.title ?? "" + + var completion: ((String?) -> Void)? + let alertController = businessLinkNameAlertController( + context: self.context, + text: "Link Name", + subtext: "Add a name for this link that only you will see.", + value: currentValue, + characterLimit: 32, + apply: { value in + completion?(value) + } + ) + completion = { [weak self, weak alertController] value in + guard let self else { + alertController?.dismissAnimated() + return + } + if let value { + if value == currentValue { + alertController?.dismissAnimated() + return + } + + let _ = self.context.engine.accountData.editBusinessChatLink(url: link.url, message: link.message, entities: link.entities, title: value.isEmpty ? nil : value).startStandalone() + + let linkUrl: String + if link.url.hasPrefix("https://") { + linkUrl = String(link.url[link.url.index(link.url.startIndex, offsetBy: "https://".count)...]) + } else { + linkUrl = link.url + } + //TODO:localize + self.chatTitleView?.titleContent = .custom(value.isEmpty ? "Link to Chat" : value, linkUrl, false) + if case let .customChatContents(customChatContents) = self.subject { + customChatContents.businessLinkUpdate(message: link.message, entities: link.entities, title: value.isEmpty ? nil : value) + } + + alertController?.view.endEditing(true) + alertController?.dismissAnimated() + } + } + self.present(alertController, in: .window(.root)) } } } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index fd487a266f..3786654f24 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2939,13 +2939,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, transition: ContainedViewLayoutTransition, interactive: Bool, completion: @escaping (ContainedViewLayoutTransition) -> Void) { self.selectedMessages = chatPresentationInterfaceState.interfaceState.selectionState?.selectedIds + var textStateUpdated = false if let textInputPanelNode = self.textInputPanelNode { + let wasEmpty = self.chatPresentationInterfaceState.interfaceState.effectiveInputState.inputText.length != 0 + let isEmpty = chatPresentationInterfaceState.interfaceState.effectiveInputState.inputText.length != 0 + if wasEmpty != isEmpty { + textStateUpdated = true + } + self.chatPresentationInterfaceState = self.chatPresentationInterfaceState.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputPanelNode.inputTextState) } } let presentationReadyUpdated = self.chatPresentationInterfaceState.presentationReady != chatPresentationInterfaceState.presentationReady - if self.chatPresentationInterfaceState != chatPresentationInterfaceState && chatPresentationInterfaceState.presentationReady { + if (self.chatPresentationInterfaceState != chatPresentationInterfaceState && chatPresentationInterfaceState.presentationReady) || textStateUpdated { self.onLayoutCompletions.append(completion) let themeUpdated = presentationReadyUpdated || (self.chatPresentationInterfaceState.theme !== chatPresentationInterfaceState.theme) @@ -4007,7 +4014,17 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - if !messages.isEmpty || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil { + var postEmptyMessages = false + if case let .customChatContents(customChatContents) = self.chatPresentationInterfaceState.subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case .businessLinkSetup: + postEmptyMessages = true + } + } + + if !messages.isEmpty || postEmptyMessages || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil { if let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds { var attributes: [MessageAttribute] = [] attributes.append(ForwardOptionsMessageAttribute(hideNames: self.chatPresentationInterfaceState.interfaceState.forwardOptionsState?.hideNames == true, hideCaptions: self.chatPresentationInterfaceState.interfaceState.forwardOptionsState?.hideCaptions == true)) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 2f97c2e562..abde9d1ec7 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -220,6 +220,15 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte } } + if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case .businessLinkSetup: + stickersEnabled = false + } + } + if isTextEmpty && chatPresentationInterfaceState.hasBots && chatPresentationInterfaceState.hasBotCommands && !hasForward { accessoryItems.append(.commands) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index d094b5b42e..94d0aedbad 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1990,6 +1990,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState customChatContents.deleteMessages(ids: messages.map(\.id)) }))) } + case .businessLinkSetup: + break } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 549e6e05e5..98cef19649 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -403,7 +403,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject { switch customChatContents.kind { - case .quickReplyMessageInput: + case .quickReplyMessageInput, .businessLinkSetup: displayInputTextPanel = true } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index 689cc49425..b96441ca5f 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -57,7 +57,7 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha if case let .customChatContents(customChatContents) = presentationInterfaceState.subject { switch customChatContents.kind { - case .quickReplyMessageInput: + case .quickReplyMessageInput, .businessLinkSetup: if let currentButton = currentButton, currentButton.action == .dismiss { return currentButton } else { @@ -137,6 +137,14 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present case .greeting, .away: return nil } + case .businessLinkSetup: + if let currentButton = currentButton, currentButton.action == .edit { + return currentButton + } else { + let buttonItem = UIBarButtonItem(title: strings.Common_Edit, style: .plain, target: target, action: selector) + buttonItem.accessibilityLabel = strings.Common_Done + return ChatNavigationButton(action: .edit, buttonItem: buttonItem) + } } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index 694a5266a2..2f8c67a0d9 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -52,6 +52,19 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat return nil case .scheduledMessages, .pinnedMessages: inhibitTitlePanelDisplay = true + case let .customChatContents(customChatContents): + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case .businessLinkSetup: + if let currentPanel = currentPanel as? ChatBusinessLinkTitlePanelNode { + return currentPanel + } else { + let panel = ChatBusinessLinkTitlePanelNode(context: context) + panel.interfaceInteraction = interfaceInteraction + return panel + } + } default: break } diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 265336934c..5ba7e6c67b 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -104,7 +104,9 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { let displayCount: Int switch customChatContents.kind { case .quickReplyMessageInput: - displayCount = 20 + displayCount = customChatContents.messageLimit ?? 20 + case .businessLinkSetup: + displayCount = 0 } self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_QuickReplyMessageLimitReachedText(Int32(displayCount)), font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 8553f1c941..1bbd83579c 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -574,6 +574,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool, Bool)? private var leftMenuInset: CGFloat = 0.0 private var rightSlowModeInset: CGFloat = 0.0 + private var currentTextInputBackgroundWidthOffset: CGFloat = 0.0 var displayAttachmentMenu: () -> Void = { } var sendMessage: () -> Void = { } @@ -1474,6 +1475,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch isMediaEnabled = false } } + var isRecording = false if let _ = interfaceState.inputTextPanelState.mediaRecordingState { isRecording = true @@ -1491,7 +1493,24 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch isMediaEnabled = false } } - transition.updateAlpha(layer: self.attachmentButton.layer, alpha: isMediaEnabled ? 1.0 : 0.4) + + var displayMediaButton = true + if case let .customChatContents(customChatContents) = interfaceState.subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case .businessLinkSetup: + displayMediaButton = false + } + } + + let attachmentButtonAlpha: CGFloat + if displayMediaButton { + attachmentButtonAlpha = isMediaEnabled ? 1.0 : 0.4 + } else { + attachmentButtonAlpha = 0.0 + } + transition.updateAlpha(layer: self.attachmentButton.layer, alpha: attachmentButtonAlpha) self.attachmentButton.isEnabled = isMediaEnabled && !isRecording self.attachmentButton.accessibilityTraits = (!isSlowmodeActive || isMediaEnabled) ? [.button] : [.button, .notEnabled] self.attachmentButtonDisabledNode.isHidden = !isSlowmodeActive || isMediaEnabled @@ -1853,6 +1872,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch case .away: placeholder = interfaceState.strings.Chat_Placeholder_AwayMessage } + case .businessLinkSetup: + //TODO:localize + placeholder = "Add a preset message..." } } @@ -1870,7 +1892,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.actionButtons.sendButtonLongPressEnabled = !isScheduledMessages } - let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil + var sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil + if let interfaceState = self.presentationInterfaceState { + if case let .customChatContents(customChatContents) = interfaceState.subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case .businessLinkSetup: + sendButtonHasApplyIcon = true + } + } + } if updateSendButtonIcon { if !self.actionButtons.animatingSendButton { @@ -1987,6 +2019,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch transition = .animated(duration: 0.3, curve: .easeInOut) } + var leftInset = leftInset + + var textInputBackgroundWidthOffset: CGFloat = 0.0 + var attachmentButtonX: CGFloat = hideOffset.x + leftInset + 2.0 - UIScreenPixel + if !displayMediaButton { + attachmentButtonX = -40.0 + let inputFieldAdditionalWidth = 40.0 - 4.0 + leftInset -= inputFieldAdditionalWidth + textInputBackgroundWidthOffset += inputFieldAdditionalWidth + } + let baseWidth = width - leftInset - leftMenuInset - rightInset - rightSlowModeInset let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics) var panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) @@ -2357,14 +2400,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - var leftInset = leftInset leftInset += leftMenuInset - transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: hideOffset.x + leftInset + 2.0 - UIScreenPixel, y: hideOffset.y + panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight))) + transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: attachmentButtonX, y: hideOffset.y + panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight))) transition.updateFrame(node: self.attachmentButtonDisabledNode, frame: self.attachmentButton.frame) var composeButtonsOffset: CGFloat = 0.0 - var textInputBackgroundWidthOffset: CGFloat = 0.0 if self.extendedSearchLayout { composeButtonsOffset = 44.0 textInputBackgroundWidthOffset = 36.0 @@ -2439,7 +2480,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) } - let textInputFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) + let textInputFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) transition.updateFrame(node: self.textInputContainer, frame: textInputFrame) transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: CGRect(origin: CGPoint(), size: textInputFrame.size)) transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha) @@ -2536,7 +2577,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch nextButtonTopRight.x -= accessoryButtonSpacing } - let textInputBackgroundFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) + let textInputBackgroundFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) + self.currentTextInputBackgroundWidthOffset = textInputBackgroundWidthOffset transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputBackgroundFrame) transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha) @@ -3296,7 +3338,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch composeButtonsOffset = 44.0 } - let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - self.leftMenuInset - self.rightSlowModeInset, maxHeight: maxHeight, metrics: metrics) + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, maxHeight: maxHeight, metrics: metrics) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) var textFieldMinHeight: CGFloat = 33.0 if let presentationInterfaceState = self.presentationInterfaceState { @@ -3610,6 +3652,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var hideMicButton = hideMicButton var mediaInputIsActive = false + var keepSendButtonEnabled = self.keepSendButtonEnabled if let presentationInterfaceState = self.presentationInterfaceState { if let mediaRecordingState = presentationInterfaceState.inputTextPanelState.mediaRecordingState { if case .video(.editing, false) = mediaRecordingState { @@ -3619,6 +3662,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if case .media = presentationInterfaceState.inputMode { mediaInputIsActive = true } + + if case let .customChatContents(customChatContents) = presentationInterfaceState.subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case .businessLinkSetup: + keepSendButtonEnabled = true + } + } } var animateWithBounce = false @@ -3684,7 +3736,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - if (hasText || self.keepSendButtonEnabled && !mediaInputIsActive && !hasSlowModeButton) { + if (hasText || keepSendButtonEnabled && !mediaInputIsActive && !hasSlowModeButton) { hideMicButton = true if self.actionButtons.sendContainerNode.alpha.isZero && self.rightSlowModeInset.isZero { @@ -3728,6 +3780,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch hideMicButton = true } + if let interfaceState = self.presentationInterfaceState { + if case let .customChatContents(customChatContents) = interfaceState.subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case .businessLinkSetup: + hideMicButton = true + } + } + } + if hideMicButton { if !self.actionButtons.micButton.alpha.isZero { self.actionButtons.micButton.alpha = 0.0 @@ -3776,7 +3839,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private func updateTextHeight(animated: Bool) { if let (width, leftInset, rightInset, _, additionalSideInsets, maxHeight, metrics, _, _) = self.validLayout { - let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right - self.leftMenuInset - self.rightSlowModeInset, maxHeight: maxHeight, metrics: metrics) + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, maxHeight: maxHeight, metrics: metrics) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) if !self.bounds.size.height.isEqual(to: panelHeight) { self.updateHeight(animated) @@ -3820,7 +3883,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private func applyUpdateSendButtonIcon() { if let interfaceState = self.presentationInterfaceState { - let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil + var sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil + if case let .customChatContents(customChatContents) = interfaceState.subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case .businessLinkSetup: + sendButtonHasApplyIcon = true + } + } if sendButtonHasApplyIcon != self.actionButtons.sendButtonHasApplyIcon { self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index c8fe99abd0..44e04bd4b1 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -226,7 +226,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { commandPrefix: "/\(shortcut.shortcut)", searchQuery: command.searchQuery.flatMap { "/\($0)"}, messageCount: shortcut.totalCount, - hideSeparator: false + hideSeparator: false, + hideDate: true ) )), editing: false, diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 15720f5ebc..9ec02dd8e9 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -33,6 +33,7 @@ import ChatFolderLinkPreviewScreen import StoryContainerScreen import WallpaperGalleryScreen import TelegramStringFormatting +import TextFormat private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -1057,5 +1058,15 @@ func openResolvedUrlImpl( present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } }) + case let .messageLink(link): + if let navigationController = navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: context, + chatLocation: .peer(link.peer), + updateTextInputState: ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(link.message, entities: link.entities)), + keepStack: .always + )) + } } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 68db042443..855cdfba53 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1941,6 +1941,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { return BusinessIntroSetupScreen.initialData(context: context) } + public func makeBusinessLinksSetupScreen(context: AccountContext, initialData: BusinessLinksSetupScreenInitialData) -> ViewController { + return BusinessLinksSetupScreen(context: context, initialData: initialData as! BusinessLinksSetupScreen.InitialData) + } + + public func makeBusinessLinksSetupScreenInitialData(context: AccountContext) -> Signal { + return BusinessLinksSetupScreen.makeInitialData(context: context) + } + public func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController { return CollectibleItemInfoScreen(context: context, initialData: initialData as! CollectibleItemInfoScreen.InitialData) } diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 353031f32b..db7467dd21 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -826,7 +826,7 @@ public final class OngoingCallContext { } } - public init(account: Account, callSessionManager: CallSessionManager, callId: CallId, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal, serializedData: String?, dataSaving: VoiceCallDataSaving, key: Data, isOutgoing: Bool, video: OngoingCallVideoCapturer?, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: [String: Any], allowP2P: Bool, enableTCP: Bool, enableStunMarking: Bool, audioSessionActive: Signal, logName: String, preferredVideoCodec: String?, audioDevice: AudioDevice?) { + public init(account: Account, callSessionManager: CallSessionManager, callId: CallId, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal, serializedData: String?, dataSaving: VoiceCallDataSaving, key: Data, isOutgoing: Bool, video: OngoingCallVideoCapturer?, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowP2P: Bool, enableTCP: Bool, enableStunMarking: Bool, audioSessionActive: Signal, logName: String, preferredVideoCodec: String?, audioDevice: AudioDevice?) { let _ = setupLogs OngoingCallThreadLocalContext.applyServerConfig(serializedData) diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index 611031d10f..d7b772dfd0 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -243,7 +243,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) { @property (nonatomic, copy) void (^ _Nullable audioLevelUpdated)(float); - (instancetype _Nonnull)initWithVersion:(NSString * _Nonnull)version - customParameters:(NSDictionary * _Nonnull)customParameters + customParameters:(NSString * _Nullable)customParameters queue:(id _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index c6e6b09ed7..74f1f6f5a9 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -996,7 +996,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; } - (instancetype _Nonnull)initWithVersion:(NSString * _Nonnull)version - customParameters:(NSDictionary * _Nonnull)customParameters + customParameters:(NSString * _Nullable)customParameters queue:(id _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving @@ -1107,13 +1107,9 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; std::vector endpoints; - NSData *customParametersJsonData = [NSJSONSerialization dataWithJSONObject:customParameters options:0 error:nil]; std::string customParametersString = "{}"; - if (customParametersJsonData) { - NSString *customParametersJson = [[NSString alloc] initWithData:customParametersJsonData encoding:NSUTF8StringEncoding]; - if (customParametersJson && customParametersJson.length != 0) { - customParametersString = std::string(customParametersJson.UTF8String); - } + if (customParameters && customParameters.length != 0) { + customParametersString = std::string(customParameters.UTF8String); } tgcalls::Config config = { diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 70c50e3073..e67b477be2 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -102,6 +102,7 @@ public enum ParsedInternalUrl { case contactToken(String) case chatFolder(slug: String) case premiumGiftCode(slug: String) + case messageLink(slug: String) } private enum ParsedUrl { @@ -464,6 +465,8 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { return .peer(.name(pathComponents[1]), .boost) } else if pathComponents[0] == "giftcode", pathComponents.count == 2 { return .premiumGiftCode(slug: pathComponents[1]) + } else if pathComponents[0] == "m" { + return .messageLink(slug: pathComponents[1]) } else if pathComponents.count == 3 && pathComponents[0] == "c" { if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]) { var threadId: Int32? @@ -992,6 +995,28 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } case let .premiumGiftCode(slug): return .single(.result(.premiumGiftCode(slug: slug))) + case let .messageLink(slug): + return .single(.progress) + |> then(context.engine.peers.resolveMessageLink(slug: slug) + |> mapToSignal { result -> Signal in + guard let result else { + return .single(.result(nil)) + } + var customEmojiIds: [Int64] = [] + for entity in result.entities { + if case let .CustomEmoji(_, fileId) = entity.type { + if !customEmojiIds.contains(fileId) { + customEmojiIds.append(fileId) + } + } + } + + return context.engine.stickers.resolveInlineStickers(fileIds: customEmojiIds) + |> mapToSignal { _ -> Signal in + return .single(.result(.messageLink(link: result))) + } + }) + } }