From 33ed284712876418c383ebe5c642dbb74263591a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 21 Feb 2024 13:55:47 +0000 Subject: [PATCH 1/7] Fix contacts deletion --- .../Sources/ContactsController.swift | 32 ++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 5f95d9bf26..c24b039122 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -682,37 +682,16 @@ public class ContactsController: ViewController { let deleteContactsFromDevice: Signal if let contactDataManager = self.context.sharedContext.contactDataManager { - deleteContactsFromDevice = contactDataManager.deleteContactWithAppSpecificReference(peerId: peerIds.first!) + deleteContactsFromDevice = combineLatest(peerIds.map { contactDataManager.deleteContactWithAppSpecificReference(peerId: $0) } + ) + |> ignoreValues } else { deleteContactsFromDevice = .complete() } - var deleteSignal = self.context.engine.contacts.deleteContacts(peerIds: peerIds) + let deleteSignal = self.context.engine.contacts.deleteContacts(peerIds: peerIds) |> then(deleteContactsFromDevice) - let progressSignal = Signal { [weak self] subscriber in - guard let self else { - return EmptyDisposable - } - let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) - self.present(statusController, in: .window(.root)) - return ActionDisposable { [weak statusController] in - Queue.mainQueue().async() { - statusController?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - deleteSignal = deleteSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - for peerId in peerIds { deleteSendMessageIntents(peerId: peerId) } @@ -724,6 +703,9 @@ public class ContactsController: ViewController { } return state } + + let _ = deleteSignal.start() + return true } else if value == .undo { self.contactsNode.contactListNode.updatePendingRemovalPeerIds { state in From 511cebbeaa6a1c5846d13c2f0f34537f15835c10 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 21 Feb 2024 14:03:02 +0000 Subject: [PATCH 2/7] Fix build --- submodules/ContactListUI/Sources/ContactsController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index c24b039122..c79c7a5bb5 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -678,8 +678,6 @@ public class ContactsController: ViewController { return false } if value == .commit { - let presentationData = self.presentationData - let deleteContactsFromDevice: Signal if let contactDataManager = self.context.sharedContext.contactDataManager { deleteContactsFromDevice = combineLatest(peerIds.map { contactDataManager.deleteContactWithAppSpecificReference(peerId: $0) } From 7232d36c3a1745072dea8a00e88fe840951bb342 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 21 Feb 2024 20:08:46 +0400 Subject: [PATCH 3/7] [WIP] Business --- .../SyncCore/SyncCore_CachedUserData.swift | 10 +++ .../PeerInfoScreenBusinessHoursItem.swift | 73 ++++++++++++++---- .../Sources/BusinessLocationSetupScreen.swift | 6 +- .../Resources/Animations/MapEmoji.tgs | Bin 0 -> 64974 bytes 4 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 submodules/TelegramUI/Resources/Animations/MapEmoji.tgs diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 4099a39f97..402f4fa81d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -343,6 +343,16 @@ public final class TelegramBusinessHours: Equatable, Codable { } } } + + public func weekMinuteSet() -> IndexSet { + var result = IndexSet() + + for interval in self.weeklyTimeIntervals { + result.insert(integersIn: interval.startMinute ..< interval.endMinute) + } + + return result + } } public final class TelegramBusinessLocation: Equatable, Codable { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift index 2c10f091cc..0323734314 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift @@ -91,6 +91,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode private var theme: PresentationTheme? private var cachedDays: [TelegramBusinessHours.WeekDay] = [] + private var cachedWeekMinuteSet = IndexSet() private var isExpanded: Bool = false @@ -197,6 +198,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode if self.item?.businessHours != item.businessHours { businessDays = item.businessHours.splitIntoWeekDays() self.cachedDays = businessDays + self.cachedWeekMinuteSet = item.businessHours.weekMinuteSet() } else { businessDays = self.cachedDays } @@ -239,10 +241,64 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode transition.updateTransformRotation(view: arrowIconView, angle: self.isExpanded ? CGFloat.pi * 1.0 : CGFloat.pi * 0.0) } + var currentCalendar = Calendar(identifier: .gregorian) + currentCalendar.timeZone = TimeZone(identifier: item.businessHours.timezoneId) ?? TimeZone.current + let currentDate = Date() + var currentDayIndex = currentCalendar.component(.weekday, from: currentDate) + if currentDayIndex == 1 { + currentDayIndex = 6 + } else { + currentDayIndex -= 2 + } + + let currentMinute = currentCalendar.component(.minute, from: currentDate) + let currentHour = currentCalendar.component(.hour, from: currentDate) + let currentWeekMinute = currentDayIndex * 24 * 60 + currentHour * 60 + currentMinute + + let isOpen = self.cachedWeekMinuteSet.contains(currentWeekMinute) + //TODO:localize + let openStatusText = isOpen ? "Open" : "Closed" + + var currentDayStatusText = currentDayIndex >= 0 && currentDayIndex < businessDays.count ? dayBusinessHoursText(businessDays[currentDayIndex]) : " " + + if !isOpen { + for range in self.cachedWeekMinuteSet.rangeView { + if range.lowerBound > currentWeekMinute { + let openInMinutes = range.lowerBound - currentWeekMinute + let _ = openInMinutes + /*if openInMinutes < 60 { + openStatusText = "Opens in \(openInMinutes) minutes" + } else if openInMinutes < 6 * 60 { + openStatusText = "Opens in \(openInMinutes / 60) hours" + } else*/ do { + let openDate = currentDate.addingTimeInterval(Double(openInMinutes * 60)) + let openTimestamp = Int32(openDate.timeIntervalSince1970) + Int32(currentCalendar.timeZone.secondsFromGMT() - TimeZone.current.secondsFromGMT()) + + let dateText = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: openTimestamp, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat( + dateFormatString: { value in + return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_Date(value).string, ranges: []) + }, + tomorrowFormatString: { value in + return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_TodayAt(value).string, ranges: []) + }, + todayFormatString: { value in + return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_TodayAt(value).string, ranges: []) + }, + yesterdayFormatString: { value in + return PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_YesterdayAt(value).string, ranges: []) + } + )).string + currentDayStatusText = "opens \(dateText)" + } + break + } + } + } + let currentStatusTextSize = self.currentStatusText.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Open", font: Font.regular(15.0), textColor: presentationData.theme.list.freeTextSuccessColor)) + text: .plain(NSAttributedString(string: openStatusText, font: Font.regular(15.0), textColor: isOpen ? presentationData.theme.list.freeTextSuccessColor : presentationData.theme.list.itemDestructiveColor)) )), environment: {}, containerSize: CGSize(width: width - sideInset * 2.0, height: 100.0) @@ -259,23 +315,10 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode let dayRightInset = sideInset + 17.0 - var currentCalendar = Calendar(identifier: .gregorian) - currentCalendar.timeZone = TimeZone.current - var currentDayIndex = currentCalendar.component(.weekday, from: Date()) - if currentDayIndex == 1 { - currentDayIndex = 6 - } else { - currentDayIndex -= 2 - } - - var targetCalendar = Calendar(identifier: .gregorian) - targetCalendar.timeZone = TimeZone(identifier: item.businessHours.timezoneId) ?? TimeZone.current - //targetCalendar.component(<#T##component: Calendar.Component##Calendar.Component#>, from: <#T##Date#>) - let currentDayTextSize = self.currentDayText.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: currentDayIndex >= 0 && currentDayIndex < businessDays.count ? dayBusinessHoursText(businessDays[currentDayIndex]) : " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)), + text: .plain(NSAttributedString(string: currentDayStatusText, font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)), horizontalAlignment: .right, maximumNumberOfLines: 0 )), diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift index f3b2410cd2..6c2727f32c 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift @@ -239,9 +239,9 @@ final class BusinessLocationSetupScreenComponent: Component { let iconSize = self.icon.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "🗺", font: Font.semibold(90.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), - horizontalAlignment: .center + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "MapEmoji"), + loop: true )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) diff --git a/submodules/TelegramUI/Resources/Animations/MapEmoji.tgs b/submodules/TelegramUI/Resources/Animations/MapEmoji.tgs new file mode 100644 index 0000000000000000000000000000000000000000..32ba54f16cd28f4f5143ac316b1eb66d1afbbcef GIT binary patch literal 64974 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 }a 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 literal 0 HcmV?d00001 From ae7df78e97660d2c844c7ebb6281b7f0bf3f12d6 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 23 Feb 2024 06:31:35 +0400 Subject: [PATCH 4/7] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 ++++ .../Sources/Node/ChatListItem.swift | 5 ++--- submodules/PremiumUI/Resources/coin.png | Bin 16191 -> 0 bytes .../Sources/BusinessPageComponent.swift | 5 ++--- .../PremiumUI/Sources/GiftOptionItem.swift | 4 ++++ .../Sources/PremiumIntroScreen.swift | 2 -- .../ChatRecentActionsHistoryTransition.swift | 3 +++ .../Sources/PeerInfoScreen.swift | 5 ++--- .../Sources/PeerNameColorItem.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 20 ++++++++++++------ .../ChatRestrictedInputPanelNode.swift | 5 ++--- 11 files changed, 33 insertions(+), 22 deletions(-) delete mode 100644 submodules/PremiumUI/Resources/coin.png diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 9fd23425c5..cba160b51a 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11376,3 +11376,7 @@ to respond to messages faster."; "ChannelBoost.Title" = "Boost Channel"; "ChannelBoost.Info" = "Subscribers of your channel can **boost** it so that it **levels up** and gets **exclusive features**."; + +"Conversation.BoostToUnrestrictText" = "Boost this group to send messages"; + +"Settings.Business" = "Telegram Business"; diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index c307c7573e..7be4910561 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2260,12 +2260,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { hasDraft = true authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor) - //TODO:localize switch mediaDraftContentType { case .audio: - attributedText = NSAttributedString(string: "Voice Message", font: textFont, textColor: theme.messageTextColor) + attributedText = NSAttributedString(string: item.presentationData.strings.Message_VoiceMessage, font: textFont, textColor: theme.messageTextColor) case .video: - attributedText = NSAttributedString(string: "Video Message", font: textFont, textColor: theme.messageTextColor) + attributedText = NSAttributedString(string: item.presentationData.strings.Message_VideoMessage, font: textFont, textColor: theme.messageTextColor) } } else if inlineAuthorPrefix == nil, let draftState = draftState { hasDraft = true diff --git a/submodules/PremiumUI/Resources/coin.png b/submodules/PremiumUI/Resources/coin.png deleted file mode 100644 index 90f45f3bd103815072a399448597c11408949617..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16191 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 diff --git a/submodules/PremiumUI/Sources/BusinessPageComponent.swift b/submodules/PremiumUI/Sources/BusinessPageComponent.swift index 9d57a177f5..f11fcc9ac5 100644 --- a/submodules/PremiumUI/Sources/BusinessPageComponent.swift +++ b/submodules/PremiumUI/Sources/BusinessPageComponent.swift @@ -546,7 +546,7 @@ final class BusinessPageComponent: CombinedComponent { state.isDisplaying = environment.isDisplaying let theme = context.component.theme -// let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings + let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings let topInset: CGFloat = 56.0 @@ -604,10 +604,9 @@ final class BusinessPageComponent: CombinedComponent { transition: context.transition ) - //TODO:localize let title = title.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: "Telegram Business", font: Font.semibold(20.0), textColor: theme.rootController.navigationBar.primaryTextColor)), + text: .plain(NSAttributedString(string: strings.Premium_Business, font: Font.semibold(20.0), textColor: theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 diff --git a/submodules/PremiumUI/Sources/GiftOptionItem.swift b/submodules/PremiumUI/Sources/GiftOptionItem.swift index 9a304ed762..882912171b 100644 --- a/submodules/PremiumUI/Sources/GiftOptionItem.swift +++ b/submodules/PremiumUI/Sources/GiftOptionItem.swift @@ -309,6 +309,10 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode { textConstrainedWidth -= 54.0 subtitleConstrainedWidth -= 30.0 } + if let _ = item.titleBadge { + textConstrainedWidth -= 32.0 + subtitleConstrainedWidth -= 32.0 + } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: .greatestFiniteMagnitude))) let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: subtitleConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index baf8af3d2d..a879a4fb62 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2167,7 +2167,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { size.height += 8.0 let status = state.peer?.emojiStatus -// let statusColor = state.peer?.nameColor.flatMap { context.component.context.peerNameColors.get($0, dark: environment.theme.overallDarkAppearance).main } ?? .blue let accentColor = environment.theme.list.itemAccentColor var perksItems: [AnyComponentWithIdentity] = [] @@ -2347,7 +2346,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } else { layoutPerks() - let textPadding: CGFloat = 13.0 let infoTitle = infoTitle.update( diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index 35f4d5ab0e..cfc70b0418 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -825,6 +825,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { order = [ (.canChangeInfo, self.presentationData.strings.Channel_AdminLog_CanChangeInfo), (.canDeleteMessages, self.presentationData.strings.Channel_AdminLog_CanDeleteMessages), + (.canPostStories, self.presentationData.strings.Channel_AdminLog_CanPostStories), + (.canDeleteStories, self.presentationData.strings.Channel_AdminLog_CanDeleteStoriesOfOthers), + (.canEditStories, self.presentationData.strings.Channel_AdminLog_CanEditStoriesOfOthers), (.canBanUsers, self.presentationData.strings.Channel_AdminLog_CanBanUsers), (.canInviteUsers, self.presentationData.strings.Channel_AdminLog_CanInviteUsersViaLink), (.canPinMessages, self.presentationData.strings.Channel_AdminLog_CanPinMessages), diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 443fc510f8..cdd84b941b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -929,11 +929,10 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: presentationData.strings.Settings_Premium, icon: PresentationResourcesSettings.premium, action: { interaction.openSettings(.premium) })) - //TODO:localize - items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Telegram Business", icon: PresentationResourcesSettings.business, action: { + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_Business, icon: PresentationResourcesSettings.business, action: { interaction.openSettings(.businessSetup) })) - items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: { + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(""), text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: { interaction.openSettings(.premiumGift) })) } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift index 0df4e747c1..9c0e7060b8 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift @@ -442,7 +442,7 @@ public final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { let sideInset: CGFloat = params.leftInset + 10.0 let iconSize = CGSize(width: 32.0, height: 32.0) - let spacing = (params.width - sideInset * 2.0 - iconSize.width * CGFloat(itemsPerRow)) / CGFloat(itemsPerRow - 1) + let spacing = floorToScreenPixels((params.width - sideInset * 2.0 - iconSize.width * CGFloat(itemsPerRow)) / CGFloat(itemsPerRow - 1)) var origin = CGPoint(x: sideInset, y: 10.0) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index d26e815e00..fb99b10775 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -15468,14 +15468,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } if let giveaway { - Queue.mainQueue().after(0.2) { - let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings) - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: { - commit() - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - })], parseMarkdown: true), in: .window(.root)) + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if currentTime < giveaway.untilDate { + Queue.mainQueue().after(0.2) { + let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings) + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: { + commit() + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + })], parseMarkdown: true), in: .window(.root)) + } + f(.default) + } else { + f(.dismissWithoutContent) + commit() } - f(.default) } else { if "".isEmpty { f(.dismissWithoutContent) diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 96bc589143..fbe0af4414 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -91,11 +91,10 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { } else if personal { self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RestrictedText, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } else { - if "".isEmpty { - //TODO:localize + if (self.presentationInterfaceState?.boostsToUnrestrict ?? 0) > 0 { iconSpacing = 0.0 iconImage = PresentationResourcesChat.chatPanelBoostIcon(interfaceState.theme) - self.textNode.attributedText = NSAttributedString(string: "Boost this group to send messages", font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.panelControlAccentColor) + self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_BoostToUnrestrictText, font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.panelControlAccentColor) isUserInteractionEnabled = true } else { self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_DefaultRestrictedText, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) From 5afdedede25e9aad94c591c287fbddb75a018cf7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 23 Feb 2024 14:08:38 +0400 Subject: [PATCH 5/7] Fix build --- submodules/ChatListUI/Sources/Node/ChatListItem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 7be4910561..6d79803d62 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2262,7 +2262,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { switch mediaDraftContentType { case .audio: - attributedText = NSAttributedString(string: item.presentationData.strings.Message_VoiceMessage, font: textFont, textColor: theme.messageTextColor) + attributedText = NSAttributedString(string: item.presentationData.strings.Message_Audio, font: textFont, textColor: theme.messageTextColor) case .video: attributedText = NSAttributedString(string: item.presentationData.strings.Message_VideoMessage, font: textFont, textColor: theme.messageTextColor) } From 6ed9cef40a43d3b6b258cb65d0b2c532cfbae8c2 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 23 Feb 2024 15:26:33 +0400 Subject: [PATCH 6/7] [WIP] Quick replies --- Telegram/SiriIntents/IntentMessages.swift | 2 +- .../Sources/ChatController.swift | 5 +- .../ChatPanelInterfaceInteraction.swift | 4 +- .../GalleryUI/Sources/GalleryController.swift | 8 +- .../Items/UniversalVideoGalleryItem.swift | 2 +- .../AdditionalMessageHistoryViewData.swift | 2 +- submodules/Postbox/Sources/Message.swift | 22 +- .../Postbox/Sources/MessageHistoryView.swift | 22 +- .../Sources/MessageOfInterestHolesView.swift | 6 +- submodules/Postbox/Sources/Postbox.swift | 8 +- submodules/Postbox/Sources/PostboxView.swift | 71 ++- submodules/Postbox/Sources/ViewTracker.swift | 37 +- submodules/Postbox/Sources/Views.swift | 75 +++ submodules/TelegramApi/Sources/Api0.swift | 37 +- submodules/TelegramApi/Sources/Api10.swift | 136 ++--- submodules/TelegramApi/Sources/Api11.swift | 242 +++----- submodules/TelegramApi/Sources/Api12.swift | 162 ++++++ submodules/TelegramApi/Sources/Api18.swift | 48 ++ submodules/TelegramApi/Sources/Api2.swift | 210 ++++++- submodules/TelegramApi/Sources/Api22.swift | 16 +- submodules/TelegramApi/Sources/Api23.swift | 26 +- submodules/TelegramApi/Sources/Api28.swift | 56 +- submodules/TelegramApi/Sources/Api32.swift | 81 ++- submodules/TelegramApi/Sources/Api7.swift | 110 ++++ .../Account/AccountIntermediateState.swift | 18 + .../Sources/Account/AccountManager.swift | 1 + .../ApiUtils/StoreMessage_Telegram.swift | 15 +- .../PendingMessages/EnqueueMessage.swift | 27 +- .../PendingMessages/RequestEditMessage.swift | 8 +- .../State/AccountStateManagementUtils.swift | 38 ++ .../Sources/State/AccountViewTracker.swift | 96 +++- .../Sources/State/ApplyUpdateMessage.swift | 48 +- .../CloudChatRemoveMessagesOperation.swift | 25 +- .../State/HistoryViewStateValidation.swift | 164 ++++-- ...gedCloudChatRemoveMessagesOperations.swift | 55 +- ...anagedConsumePersonalMessagesActions.swift | 2 +- .../Sources/State/PendingMessageManager.swift | 99 +++- .../Sources/State/UpdatesApiUtils.swift | 6 + .../SyncCore/QuickReplyMessageAttribute.swift | 23 + .../SyncCore/SyncCore_CachedUserData.swift | 4 +- ...ore_CloudChatRemoveMessagesOperation.swift | 11 +- .../SyncCore/SyncCore_MediaReference.swift | 38 +- .../SyncCore/SyncCore_Namespaces.swift | 6 +- .../TelegramEngineAccountData.swift | 30 +- .../DeleteMessagesInteractively.swift | 56 +- .../Messages/QuickReplyMessages.swift | 395 +++++++++---- .../Messages/ReplyThreadHistory.swift | 2 +- .../Messages/SearchMessages.swift | 8 +- .../Messages/SparseMessageList.swift | 2 +- .../Peers/TelegramEnginePeers.swift | 6 +- .../Peers/UpdateCachedPeerData.swift | 4 +- .../Sources/Utils/MessageUtils.swift | 30 + .../ChatMessageAnimatedStickerItemNode.swift | 6 +- .../Sources/ChatMessageBubbleItemNode.swift | 4 +- .../ChatMessageInstantVideoItemNode.swift | 4 +- ...atMessageInteractiveInstantVideoNode.swift | 2 +- .../Sources/ChatMessageDateHeader.swift | 4 +- .../ChatMessagePollBubbleContentNode.swift | 10 +- .../Sources/ChatMessageStickerItemNode.swift | 2 +- .../PeerInfoScreenBusinessHoursItem.swift | 10 +- ...aticBusinessMessageSetupChatContents.swift | 284 +++++----- .../AutomaticBusinessMessageSetupScreen.swift | 15 +- .../Sources/BottomPanelComponent.swift | 117 ++++ .../QuickReplyEmptyStateComponent.swift | 138 ++--- .../Sources/QuickReplySetupScreen.swift | 518 +++++++++++++----- .../QuickReplies.imageset/Contents.json | 12 + .../quickrepliesdemo.pdf | 127 +++++ .../Resources/Animations/WriteEmoji.tgs | Bin 0 -> 64148 bytes .../ChatControllerNavigateToMessage.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 78 ++- .../Sources/ChatControllerEditChat.swift | 26 +- .../Sources/ChatControllerNode.swift | 10 +- .../ChatControllerOpenMessageShareMenu.swift | 2 +- .../TelegramUI/Sources/ChatEmptyNode.swift | 26 +- .../Sources/ChatHistoryListNode.swift | 93 +++- .../ChatInterfaceStateContextMenus.swift | 2 +- .../ChatInterfaceStateContextQueries.swift | 8 +- ...hatMessageThrottledProcessingManager.swift | 24 +- .../CommandChatInputContextPanelNode.swift | 4 +- .../Sources/EditAccessoryPanelNode.swift | 5 +- .../Sources/PeerMessagesMediaPlaylist.swift | 4 +- .../TelegramUI/Sources/PrefetchManager.swift | 6 +- 82 files changed, 2999 insertions(+), 1149 deletions(-) create mode 100644 submodules/TelegramCore/Sources/SyncCore/QuickReplyMessageAttribute.swift create mode 100644 submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BottomPanelComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/quickrepliesdemo.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/WriteEmoji.tgs diff --git a/Telegram/SiriIntents/IntentMessages.swift b/Telegram/SiriIntents/IntentMessages.swift index 935c3a1db4..37360288e3 100644 --- a/Telegram/SiriIntents/IntentMessages.swift +++ b/Telegram/SiriIntents/IntentMessages.swift @@ -53,7 +53,7 @@ func unreadMessages(account: Account) -> Signal<[INMessage], NoError> { } if !isMuted && hasUnread { - signals.append(account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: index.messageIndex.id.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 10, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: .combinedLocation) + signals.append(account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: index.messageIndex.id.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 10, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: .combinedLocation) |> take(1) |> map { view -> [INMessage] in var messages: [INMessage] = [] diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 230c49dd13..66dde6dd3d 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -805,7 +805,7 @@ public enum ChatControllerPresentationMode: Equatable { public enum ChatInputTextCommand: Equatable { case command(PeerCommand) - case shortcut(QuickReplyMessageShortcut) + case shortcut(ShortcutMessageList.Item) } public struct ChatInputQueryCommandsResult: Equatable { @@ -1077,6 +1077,7 @@ public enum ChatHistoryListSource { case `default` case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId?, quote: Quote?, loadMore: (() -> Void)?) + case customView(historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError>) } public enum ChatCustomContentsKind: Equatable { @@ -1087,7 +1088,7 @@ public enum ChatCustomContentsKind: Equatable { public protocol ChatCustomContentsProtocol: AnyObject { var kind: ChatCustomContentsKind { get } - var messages: Signal<[Message], NoError> { get } + var historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError> { get } var messageLimit: Int? { get } func enqueueMessages(messages: [EnqueueMessage]) diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 98187511ea..6b4bf1eebd 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -104,7 +104,7 @@ public final class ChatPanelInterfaceInteraction { public let togglePeerNotifications: () -> Void public let sendContextResult: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool public let sendBotCommand: (Peer, String) -> Void - public let sendShortcut: (QuickReplyMessageShortcut) -> Void + public let sendShortcut: (Int32) -> Void public let openEditShortcuts: () -> Void public let sendBotStart: (String?) -> Void public let botSwitchChatWithPayload: (PeerId, String) -> Void @@ -219,7 +219,7 @@ public final class ChatPanelInterfaceInteraction { togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, sendBotCommand: @escaping (Peer, String) -> Void, - sendShortcut: @escaping (QuickReplyMessageShortcut) -> Void, + sendShortcut: @escaping (Int32) -> Void, openEditShortcuts: @escaping () -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index a715bd32af..131a2319b4 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -656,8 +656,10 @@ public class GalleryController: ViewController, StandalonePresentableController, let namespaces: MessageIdNamespaces if Namespaces.Message.allScheduled.contains(message.id.namespace) { namespaces = .just(Namespaces.Message.allScheduled) + } else if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + namespaces = .just(Namespaces.Message.allQuickReply) } else { - namespaces = .not(Namespaces.Message.allScheduled) + namespaces = .not(Namespaces.Message.allNonRegular) } let inputTag: HistoryViewInputTag if let customTag { @@ -1391,8 +1393,10 @@ public class GalleryController: ViewController, StandalonePresentableController, let namespaces: MessageIdNamespaces if Namespaces.Message.allScheduled.contains(message.id.namespace) { namespaces = .just(Namespaces.Message.allScheduled) + } else if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + namespaces = .just(Namespaces.Message.allQuickReply) } else { - namespaces = .not(Namespaces.Message.allScheduled) + namespaces = .not(Namespaces.Message.allNonRegular) } let signal = strongSelf.context.account.postbox.aroundMessageHistoryViewForLocation(strongSelf.context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), anchor: .index(reloadAroundIndex), ignoreMessagesInTimestampRange: nil, count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: tag, appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: [.combinedLocation]) |> mapToSignal { (view, _, _) -> Signal in diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 688f3ea1b6..11d90442d4 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1184,7 +1184,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var hintSeekable = false if let contentInfo = item.contentInfo, case let .message(message) = contentInfo { - if Namespaces.Message.allScheduled.contains(message.id.namespace) { + if Namespaces.Message.allNonRegular.contains(message.id.namespace) { disablePictureInPicture = true } else { let throttledSignal = videoNode.status diff --git a/submodules/Postbox/Sources/AdditionalMessageHistoryViewData.swift b/submodules/Postbox/Sources/AdditionalMessageHistoryViewData.swift index 9cf5cb60e6..88ccb841c3 100644 --- a/submodules/Postbox/Sources/AdditionalMessageHistoryViewData.swift +++ b/submodules/Postbox/Sources/AdditionalMessageHistoryViewData.swift @@ -1,6 +1,6 @@ import Foundation -public enum AdditionalMessageHistoryViewData { +public enum AdditionalMessageHistoryViewData: Equatable { case cachedPeerData(PeerId) case cachedPeerDataMessages(PeerId) case peerChatState(PeerId) diff --git a/submodules/Postbox/Sources/Message.swift b/submodules/Postbox/Sources/Message.swift index 58c14eda19..255cf48937 100644 --- a/submodules/Postbox/Sources/Message.swift +++ b/submodules/Postbox/Sources/Message.swift @@ -1029,7 +1029,7 @@ final class InternalStoreMessage { } } -public enum MessageIdNamespaces { +public enum MessageIdNamespaces: Equatable { case all case just(Set) case not(Set) @@ -1045,3 +1045,23 @@ public enum MessageIdNamespaces { } } } + +public struct PeerAndThreadId: Hashable { + public var peerId: PeerId + public var threadId: Int64? + + public init(peerId: PeerId, threadId: Int64?) { + self.peerId = peerId + self.threadId = threadId + } +} + +public struct MessageAndThreadId: Hashable { + public var messageId: MessageId + public var threadId: Int64? + + public init(messageId: MessageId, threadId: Int64?) { + self.messageId = messageId + self.threadId = threadId + } +} diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index 593d94ad7d..cca6c8d97d 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -237,7 +237,7 @@ public enum MessageHistoryViewRelativeHoleDirection: Equatable, Hashable, Custom } } -public struct MessageHistoryViewOrderStatistics: OptionSet { +public struct MessageHistoryViewOrderStatistics: OptionSet, Equatable { public var rawValue: Int32 public init(rawValue: Int32) { @@ -290,7 +290,7 @@ public enum MessageHistoryViewInput: Equatable { case external(MessageHistoryViewExternalInput) } -public enum MessageHistoryViewReadState { +public enum MessageHistoryViewReadState: Equatable { case peer([PeerId: CombinedPeerReadState]) } @@ -302,7 +302,7 @@ public enum HistoryViewInputAnchor: Equatable { case unread } -final class MutableMessageHistoryView { +final class MutableMessageHistoryView: MutablePostboxView { private(set) var peerIds: MessageHistoryViewInput private let ignoreMessagesInTimestampRange: ClosedRange? let tag: HistoryViewInputTag? @@ -310,6 +310,7 @@ final class MutableMessageHistoryView { let namespaces: MessageIdNamespaces private let orderStatistics: MessageHistoryViewOrderStatistics private let clipHoles: Bool + private let trackHoles: Bool private let anchor: HistoryViewInputAnchor fileprivate var combinedReadStates: MessageHistoryViewReadState? @@ -333,6 +334,7 @@ final class MutableMessageHistoryView { postbox: PostboxImpl, orderStatistics: MessageHistoryViewOrderStatistics, clipHoles: Bool, + trackHoles: Bool, peerIds: MessageHistoryViewInput, ignoreMessagesInTimestampRange: ClosedRange?, anchor inputAnchor: HistoryViewInputAnchor, @@ -343,13 +345,13 @@ final class MutableMessageHistoryView { namespaces: MessageIdNamespaces, count: Int, topTaggedMessages: [MessageId.Namespace: MessageHistoryTopTaggedMessage?], - additionalDatas: [AdditionalMessageHistoryViewDataEntry], - getMessageCountInRange: (MessageIndex, MessageIndex) -> Int32 + additionalDatas: [AdditionalMessageHistoryViewDataEntry] ) { self.anchor = inputAnchor self.orderStatistics = orderStatistics self.clipHoles = clipHoles + self.trackHoles = trackHoles self.peerIds = peerIds self.ignoreMessagesInTimestampRange = ignoreMessagesInTimestampRange self.combinedReadStates = combinedReadStates @@ -1040,6 +1042,10 @@ final class MutableMessageHistoryView { } func firstHole() -> (MessageHistoryViewHole, MessageHistoryViewRelativeHoleDirection, Int, Int64?)? { + if !self.trackHoles { + return nil + } + switch self.sampledState { case let .loading(loadingSample): switch loadingSample { @@ -1065,9 +1071,13 @@ final class MutableMessageHistoryView { } } } + + func immutableView() -> PostboxView { + return MessageHistoryView(self) + } } -public final class MessageHistoryView { +public final class MessageHistoryView: PostboxView { public let tag: HistoryViewInputTag? public let namespaces: MessageIdNamespaces public let anchorIndex: MessageHistoryAnchorIndex diff --git a/submodules/Postbox/Sources/MessageOfInterestHolesView.swift b/submodules/Postbox/Sources/MessageOfInterestHolesView.swift index 978b7b52c3..48ab8b21d8 100644 --- a/submodules/Postbox/Sources/MessageOfInterestHolesView.swift +++ b/submodules/Postbox/Sources/MessageOfInterestHolesView.swift @@ -60,7 +60,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { } } self.anchor = anchor - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) let _ = self.updateFromView() } @@ -134,7 +134,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { case let .peer(id, threadId): peerIds = postbox.peerIdsForLocation(.peer(peerId: id, threadId: threadId), ignoreRelatedChats: false) } - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) return self.updateFromView() } else if self.wrappedView.replay(postbox: postbox, transaction: transaction) { var reloadView = false @@ -167,7 +167,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { case let .peer(id, threadId): peerIds = postbox.peerIdsForLocation(.peer(peerId: id, threadId: threadId), ignoreRelatedChats: false) } - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: []) } return self.updateFromView() diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 33201d99a7..a5be948077 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -3344,13 +3344,7 @@ final class PostboxImpl { readStates = transientReadStates } - let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, clipHoles: clipHoles, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries, getMessageCountInRange: { lowerBound, upperBound in - if case let .tag(tagMask) = tag { - return Int32(self.messageHistoryTable.getMessageCountInRange(peerId: lowerBound.id.peerId, namespace: lowerBound.id.namespace, tag: tagMask, lowerBound: lowerBound, upperBound: upperBound)) - } else { - return 0 - } - }) + let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, clipHoles: clipHoles, trackHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries) let initialUpdateType: ViewUpdateType = .Initial diff --git a/submodules/Postbox/Sources/PostboxView.swift b/submodules/Postbox/Sources/PostboxView.swift index 7cf89152ae..eb629a5d16 100644 --- a/submodules/Postbox/Sources/PostboxView.swift +++ b/submodules/Postbox/Sources/PostboxView.swift @@ -1,9 +1,9 @@ import Foundation -public protocol PostboxView { +public protocol PostboxView: AnyObject { } -protocol MutablePostboxView { +protocol MutablePostboxView: AnyObject { func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool func immutableView() -> PostboxView @@ -16,14 +16,71 @@ final class CombinedMutableView { self.views = views } - func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool { - var updated = false + func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> (updated: Bool, updateTrackedHoles: Bool) { + var anyUpdated = false + var updateTrackedHoles = false for (_, view) in self.views { - if view.replay(postbox: postbox, transaction: transaction) { - updated = true + if let mutableView = view as? MutableMessageHistoryView { + var innerUpdated = false + + let previousPeerIds = mutableView.peerIds + + if mutableView.replay(postbox: postbox, transaction: transaction) { + innerUpdated = true + } + + var updateType: ViewUpdateType = .Generic + switch mutableView.peerIds { + case let .single(peerId, threadId): + for key in transaction.currentPeerHoleOperations.keys { + if key.peerId == peerId && key.threadId == threadId { + updateType = .FillHole + break + } + } + case .associated: + var ids = Set() + switch mutableView.peerIds { + case .single, .external: + assertionFailure() + case let .associated(mainPeerId, associatedId): + ids.insert(mainPeerId) + if let associatedId = associatedId { + ids.insert(associatedId.peerId) + } + } + + if !ids.isEmpty { + for key in transaction.currentPeerHoleOperations.keys { + if ids.contains(key.peerId) { + updateType = .FillHole + break + } + } + } + case .external: + break + } + + mutableView.updatePeerIds(transaction: transaction) + if mutableView.peerIds != previousPeerIds { + updateType = .UpdateVisible + + let _ = mutableView.refreshDueToExternalTransaction(postbox: postbox) + innerUpdated = true + } + + if innerUpdated { + anyUpdated = true + updateTrackedHoles = true + let _ = updateType + //pipe.putNext((MessageHistoryView(mutableView), updateType)) + } + } else if view.replay(postbox: postbox, transaction: transaction) { + anyUpdated = true } } - return updated + return (anyUpdated, updateTrackedHoles) } func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { diff --git a/submodules/Postbox/Sources/ViewTracker.swift b/submodules/Postbox/Sources/ViewTracker.swift index 12d96f1118..ff3f3252c8 100644 --- a/submodules/Postbox/Sources/ViewTracker.swift +++ b/submodules/Postbox/Sources/ViewTracker.swift @@ -1,7 +1,7 @@ import Foundation import SwiftSignalKit -public enum ViewUpdateType : Equatable { +public enum ViewUpdateType: Equatable { case Initial case InitialUnread(MessageIndex) case Generic @@ -351,10 +351,6 @@ final class ViewTracker { self.updateTrackedChatListHoles() - if updateTrackedHoles { - self.updateTrackedHoles() - } - if self.unsentMessageView.replay(transaction.unsentMessageOperations) { self.unsentViewUpdated() } @@ -414,9 +410,14 @@ final class ViewTracker { } for (mutableView, pipe) in self.combinedViews.copyItems() { - if mutableView.replay(postbox: postbox, transaction: transaction) { + let result = mutableView.replay(postbox: postbox, transaction: transaction) + + if result.updated { pipe.putNext(mutableView.immutableView()) } + if result.updateTrackedHoles { + updateTrackedHoles = true + } } for (view, pipe) in self.failedMessageIdsViews.copyItems() { @@ -425,6 +426,10 @@ final class ViewTracker { } } + if updateTrackedHoles { + self.updateTrackedHoles() + } + self.updateTrackedForumTopicListHoles() } @@ -486,6 +491,26 @@ final class ViewTracker { firstHolesAndTags.insert(MessageHistoryHolesViewEntry(hole: hole, direction: direction, space: space, count: count, userId: userId)) } } + for (view, _) in self.combinedViews.copyItems() { + for (_, subview) in view.views { + if let subview = subview as? MutableMessageHistoryView { + if let (hole, direction, count, userId) = subview.firstHole() { + let space: MessageHistoryHoleOperationSpace + if let tag = subview.tag { + switch tag { + case let .tag(value): + space = .tag(value) + case let .customTag(value, regularTag): + space = .customTag(value, regularTag) + } + } else { + space = .everywhere + } + firstHolesAndTags.insert(MessageHistoryHolesViewEntry(hole: hole, direction: direction, space: space, count: count, userId: userId)) + } + } + } + } if self.messageHistoryHolesView.update(firstHolesAndTags) { for subscriber in self.messageHistoryHolesViewSubscribers.copyItems() { diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index 9fd7e8b9e4..c3b0e95f18 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -1,6 +1,52 @@ import Foundation public enum PostboxViewKey: Hashable { + public struct HistoryView: Equatable { + public var peerId: PeerId + public var threadId: Int64? + public var clipHoles: Bool + public var trackHoles: Bool + public var orderStatistics: MessageHistoryViewOrderStatistics + public var ignoreMessagesInTimestampRange: ClosedRange? + public var anchor: HistoryViewInputAnchor + public var combinedReadStates: MessageHistoryViewReadState? + public var transientReadStates: MessageHistoryViewReadState? + public var tag: HistoryViewInputTag? + public var appendMessagesFromTheSameGroup: Bool + public var namespaces: MessageIdNamespaces + public var count: Int + + public init( + peerId: PeerId, + threadId: Int64?, + clipHoles: Bool, + trackHoles: Bool, + orderStatistics: MessageHistoryViewOrderStatistics = [], + ignoreMessagesInTimestampRange: ClosedRange? = nil, + anchor: HistoryViewInputAnchor, + combinedReadStates: MessageHistoryViewReadState? = nil, + transientReadStates: MessageHistoryViewReadState? = nil, + tag: HistoryViewInputTag? = nil, + appendMessagesFromTheSameGroup: Bool, + namespaces: MessageIdNamespaces, + count: Int + ) { + self.peerId = peerId + self.threadId = threadId + self.clipHoles = clipHoles + self.trackHoles = trackHoles + self.orderStatistics = orderStatistics + self.ignoreMessagesInTimestampRange = ignoreMessagesInTimestampRange + self.anchor = anchor + self.combinedReadStates = combinedReadStates + self.transientReadStates = transientReadStates + self.tag = tag + self.appendMessagesFromTheSameGroup = appendMessagesFromTheSameGroup + self.namespaces = namespaces + self.count = count + } + } + case itemCollectionInfos(namespaces: [ItemCollectionId.Namespace]) case itemCollectionIds(namespaces: [ItemCollectionId.Namespace]) case itemCollectionInfo(id: ItemCollectionId) @@ -50,6 +96,7 @@ public enum PostboxViewKey: Hashable { case savedMessagesIndex(peerId: PeerId) case savedMessagesStats(peerId: PeerId) case chatInterfaceState(peerId: PeerId) + case historyView(HistoryView) public func hash(into hasher: inout Hasher) { switch self { @@ -168,6 +215,10 @@ public enum PostboxViewKey: Hashable { hasher.combine(peerId) case let .chatInterfaceState(peerId): hasher.combine(peerId) + case let .historyView(historyView): + hasher.combine(20) + hasher.combine(historyView.peerId) + hasher.combine(historyView.threadId) } } @@ -467,6 +518,12 @@ public enum PostboxViewKey: Hashable { } else { return false } + case let .historyView(historyView): + if case .historyView(historyView) = rhs { + return true + } else { + return false + } } } } @@ -571,5 +628,23 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost return MutableMessageHistorySavedMessagesStatsView(postbox: postbox, peerId: peerId) case let .chatInterfaceState(peerId): return MutableChatInterfaceStateView(postbox: postbox, peerId: peerId) + case let .historyView(historyView): + return MutableMessageHistoryView( + postbox: postbox, + orderStatistics: historyView.orderStatistics, + clipHoles: historyView.clipHoles, + trackHoles: historyView.trackHoles, + peerIds: .single(peerId: historyView.peerId, threadId: historyView.threadId), + ignoreMessagesInTimestampRange: historyView.ignoreMessagesInTimestampRange, + anchor: historyView.anchor, + combinedReadStates: historyView.combinedReadStates, + transientReadStates: historyView.transientReadStates, + tag: historyView.tag, + appendMessagesFromTheSameGroup: historyView.appendMessagesFromTheSameGroup, + namespaces: historyView.namespaces, + count: historyView.count, + topTaggedMessages: [:], + additionalDatas: [] + ) } } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 47f52d79d8..763c13f933 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -101,7 +101,12 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-944407322] = { return Api.BotMenuButton.parse_botMenuButton($0) } dict[1113113093] = { return Api.BotMenuButton.parse_botMenuButtonCommands($0) } dict[1966318984] = { return Api.BotMenuButton.parse_botMenuButtonDefault($0) } - dict[-1104414653] = { return Api.BusinessLocation.parse_businessLocation($0) } + dict[-1697809899] = { return Api.BusinessAwayMessage.parse_businessAwayMessage($0) } + dict[-910564679] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleAlways($0) } + dict[-867328308] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleCustom($0) } + dict[-1007487743] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleOutsideWorkHours($0) } + dict[-1600596660] = { return Api.BusinessGreetingMessage.parse_businessGreetingMessage($0) } + dict[-1403249929] = { return Api.BusinessLocation.parse_businessLocation($0) } dict[302717625] = { return Api.BusinessWeeklyOpen.parse_businessWeeklyOpen($0) } dict[-1936543592] = { return Api.BusinessWorkHours.parse_businessWorkHours($0) } dict[1462101002] = { return Api.CdnConfig.parse_cdnConfig($0) } @@ -300,6 +305,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-459324] = { return Api.InputBotInlineResult.parse_inputBotInlineResultDocument($0) } dict[1336154098] = { return Api.InputBotInlineResult.parse_inputBotInlineResultGame($0) } dict[-1462213465] = { return Api.InputBotInlineResult.parse_inputBotInlineResultPhoto($0) } + dict[-831530424] = { return Api.InputBusinessAwayMessage.parse_inputBusinessAwayMessage($0) } + dict[2102015497] = { return Api.InputBusinessGreetingMessage.parse_inputBusinessGreetingMessage($0) } dict[-212145112] = { return Api.InputChannel.parse_inputChannel($0) } dict[-292807034] = { return Api.InputChannel.parse_inputChannelEmpty($0) } dict[1536380829] = { return Api.InputChannel.parse_inputChannelFromMessage($0) } @@ -401,6 +408,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-380694650] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowChatParticipants($0) } dict[195371015] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowContacts($0) } dict[-1877932953] = { return Api.InputPrivacyRule.parse_inputPrivacyValueDisallowUsers($0) } + dict[609840449] = { return Api.InputQuickReplyShortcut.parse_inputQuickReplyShortcut($0) } + dict[18418929] = { return Api.InputQuickReplyShortcut.parse_inputQuickReplyShortcutId($0) } dict[583071445] = { return Api.InputReplyTo.parse_inputReplyToMessage($0) } dict[1484862010] = { return Api.InputReplyTo.parse_inputReplyToStory($0) } dict[1399317950] = { return Api.InputSecureFile.parse_inputSecureFile($0) } @@ -710,6 +719,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-463335103] = { return Api.PrivacyRule.parse_privacyValueDisallowUsers($0) } dict[32685898] = { return Api.PublicForward.parse_publicForwardMessage($0) } dict[-302797360] = { return Api.PublicForward.parse_publicForwardStory($0) } + dict[110563371] = { return Api.QuickReply.parse_quickReply($0) } dict[-1992950669] = { return Api.Reaction.parse_reactionCustomEmoji($0) } dict[455247544] = { return Api.Reaction.parse_reactionEmoji($0) } dict[2046153753] = { return Api.Reaction.parse_reactionEmpty($0) } @@ -939,7 +949,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1656358105] = { return Api.Update.parse_updateNewChannelMessage($0) } dict[314359194] = { return Api.Update.parse_updateNewEncryptedMessage($0) } dict[522914557] = { return Api.Update.parse_updateNewMessage($0) } - dict[-1386034803] = { return Api.Update.parse_updateNewQuickReply($0) } + dict[-180508905] = { return Api.Update.parse_updateNewQuickReply($0) } dict[967122427] = { return Api.Update.parse_updateNewScheduledMessage($0) } dict[1753886890] = { return Api.Update.parse_updateNewStickerSet($0) } dict[-1094555409] = { return Api.Update.parse_updateNotifySettings($0) } @@ -957,7 +967,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1751942566] = { return Api.Update.parse_updatePinnedSavedDialogs($0) } dict[-298113238] = { return Api.Update.parse_updatePrivacy($0) } dict[861169551] = { return Api.Update.parse_updatePtsChanged($0) } - dict[230929261] = { return Api.Update.parse_updateQuickReplies($0) } + dict[-112784718] = { return Api.Update.parse_updateQuickReplies($0) } dict[1040518415] = { return Api.Update.parse_updateQuickReplyMessage($0) } dict[-693004986] = { return Api.Update.parse_updateReadChannelDiscussionInbox($0) } dict[1767677564] = { return Api.Update.parse_updateReadChannelDiscussionOutbox($0) } @@ -1006,7 +1016,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) } dict[559694904] = { return Api.User.parse_user($0) } dict[-742634630] = { return Api.User.parse_userEmpty($0) } - dict[-501688336] = { return Api.UserFull.parse_userFull($0) } + dict[587153029] = { return Api.UserFull.parse_userFull($0) } dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) } dict[1326562017] = { return Api.UserProfilePhoto.parse_userProfilePhotoEmpty($0) } dict[164646985] = { return Api.UserStatus.parse_userStatusEmpty($0) } @@ -1185,9 +1195,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[978610270] = { return Api.messages.Messages.parse_messagesSlice($0) } dict[863093588] = { return Api.messages.PeerDialogs.parse_peerDialogs($0) } dict[1753266509] = { return Api.messages.PeerSettings.parse_peerSettings($0) } - dict[2094438528] = { return Api.messages.QuickReplies.parse_quickReplies($0) } + dict[-963811691] = { return Api.messages.QuickReplies.parse_quickReplies($0) } dict[1603398491] = { return Api.messages.QuickReplies.parse_quickRepliesNotModified($0) } - dict[-1810973582] = { return Api.messages.QuickReply.parse_quickReply($0) } dict[-352454890] = { return Api.messages.Reactions.parse_reactions($0) } dict[-1334846497] = { return Api.messages.Reactions.parse_reactionsNotModified($0) } dict[-1999405994] = { return Api.messages.RecentStickers.parse_recentStickers($0) } @@ -1386,6 +1395,12 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.BotMenuButton: _1.serialize(buffer, boxed) + case let _1 as Api.BusinessAwayMessage: + _1.serialize(buffer, boxed) + case let _1 as Api.BusinessAwayMessageSchedule: + _1.serialize(buffer, boxed) + case let _1 as Api.BusinessGreetingMessage: + _1.serialize(buffer, boxed) case let _1 as Api.BusinessLocation: _1.serialize(buffer, boxed) case let _1 as Api.BusinessWeeklyOpen: @@ -1540,6 +1555,10 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.InputBotInlineResult: _1.serialize(buffer, boxed) + case let _1 as Api.InputBusinessAwayMessage: + _1.serialize(buffer, boxed) + case let _1 as Api.InputBusinessGreetingMessage: + _1.serialize(buffer, boxed) case let _1 as Api.InputChannel: _1.serialize(buffer, boxed) case let _1 as Api.InputChatPhoto: @@ -1594,6 +1613,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.InputPrivacyRule: _1.serialize(buffer, boxed) + case let _1 as Api.InputQuickReplyShortcut: + _1.serialize(buffer, boxed) case let _1 as Api.InputReplyTo: _1.serialize(buffer, boxed) case let _1 as Api.InputSecureFile: @@ -1764,6 +1785,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.PublicForward: _1.serialize(buffer, boxed) + case let _1 as Api.QuickReply: + _1.serialize(buffer, boxed) case let _1 as Api.Reaction: _1.serialize(buffer, boxed) case let _1 as Api.ReactionCount: @@ -2110,8 +2133,6 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.QuickReplies: _1.serialize(buffer, boxed) - case let _1 as Api.messages.QuickReply: - _1.serialize(buffer, boxed) case let _1 as Api.messages.Reactions: _1.serialize(buffer, boxed) case let _1 as Api.messages.RecentStickers: diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 7249151a22..4fe4c26f44 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -1,3 +1,59 @@ +public extension Api { + enum InputQuickReplyShortcut: TypeConstructorDescription { + case inputQuickReplyShortcut(shortcut: String) + case inputQuickReplyShortcutId(shortcutId: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputQuickReplyShortcut(let shortcut): + if boxed { + buffer.appendInt32(609840449) + } + serializeString(shortcut, buffer: buffer, boxed: false) + break + case .inputQuickReplyShortcutId(let shortcutId): + if boxed { + buffer.appendInt32(18418929) + } + serializeInt32(shortcutId, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputQuickReplyShortcut(let shortcut): + return ("inputQuickReplyShortcut", [("shortcut", shortcut as Any)]) + case .inputQuickReplyShortcutId(let shortcutId): + return ("inputQuickReplyShortcutId", [("shortcutId", shortcutId as Any)]) + } + } + + public static func parse_inputQuickReplyShortcut(_ reader: BufferReader) -> InputQuickReplyShortcut? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputQuickReplyShortcut.inputQuickReplyShortcut(shortcut: _1!) + } + else { + return nil + } + } + public static func parse_inputQuickReplyShortcutId(_ reader: BufferReader) -> InputQuickReplyShortcut? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.InputQuickReplyShortcut.inputQuickReplyShortcutId(shortcutId: _1!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputReplyTo: TypeConstructorDescription { case inputReplyToMessage(flags: Int32, replyToMsgId: Int32, topMsgId: Int32?, replyToPeerId: Api.InputPeer?, quoteText: String?, quoteEntities: [Api.MessageEntity]?, quoteOffset: Int32?) @@ -1014,83 +1070,3 @@ public extension Api { } } -public extension Api { - enum InputWallPaper: TypeConstructorDescription { - case inputWallPaper(id: Int64, accessHash: Int64) - case inputWallPaperNoFile(id: Int64) - case inputWallPaperSlug(slug: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputWallPaper(let id, let accessHash): - if boxed { - buffer.appendInt32(-433014407) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputWallPaperNoFile(let id): - if boxed { - buffer.appendInt32(-1770371538) - } - serializeInt64(id, buffer: buffer, boxed: false) - break - case .inputWallPaperSlug(let slug): - if boxed { - buffer.appendInt32(1913199744) - } - serializeString(slug, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputWallPaper(let id, let accessHash): - return ("inputWallPaper", [("id", id as Any), ("accessHash", accessHash as Any)]) - case .inputWallPaperNoFile(let id): - return ("inputWallPaperNoFile", [("id", id as Any)]) - case .inputWallPaperSlug(let slug): - return ("inputWallPaperSlug", [("slug", slug as Any)]) - } - } - - public static func parse_inputWallPaper(_ reader: BufferReader) -> InputWallPaper? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputWallPaper.inputWallPaper(id: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputWallPaperNoFile(_ reader: BufferReader) -> InputWallPaper? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.InputWallPaper.inputWallPaperNoFile(id: _1!) - } - else { - return nil - } - } - public static func parse_inputWallPaperSlug(_ reader: BufferReader) -> InputWallPaper? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputWallPaper.inputWallPaperSlug(slug: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 666ea7ff21..89037ef849 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -1,3 +1,83 @@ +public extension Api { + enum InputWallPaper: TypeConstructorDescription { + case inputWallPaper(id: Int64, accessHash: Int64) + case inputWallPaperNoFile(id: Int64) + case inputWallPaperSlug(slug: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputWallPaper(let id, let accessHash): + if boxed { + buffer.appendInt32(-433014407) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputWallPaperNoFile(let id): + if boxed { + buffer.appendInt32(-1770371538) + } + serializeInt64(id, buffer: buffer, boxed: false) + break + case .inputWallPaperSlug(let slug): + if boxed { + buffer.appendInt32(1913199744) + } + serializeString(slug, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputWallPaper(let id, let accessHash): + return ("inputWallPaper", [("id", id as Any), ("accessHash", accessHash as Any)]) + case .inputWallPaperNoFile(let id): + return ("inputWallPaperNoFile", [("id", id as Any)]) + case .inputWallPaperSlug(let slug): + return ("inputWallPaperSlug", [("slug", slug as Any)]) + } + } + + public static func parse_inputWallPaper(_ reader: BufferReader) -> InputWallPaper? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputWallPaper.inputWallPaper(id: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputWallPaperNoFile(_ reader: BufferReader) -> InputWallPaper? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.InputWallPaper.inputWallPaperNoFile(id: _1!) + } + else { + return nil + } + } + public static func parse_inputWallPaperSlug(_ reader: BufferReader) -> InputWallPaper? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputWallPaper.inputWallPaperSlug(slug: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputWebDocument: TypeConstructorDescription { case inputWebDocument(url: String, size: Int32, mimeType: String, attributes: [Api.DocumentAttribute]) @@ -904,165 +984,3 @@ public extension Api { } } -public extension Api { - enum LabeledPrice: TypeConstructorDescription { - case labeledPrice(label: String, amount: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .labeledPrice(let label, let amount): - if boxed { - buffer.appendInt32(-886477832) - } - serializeString(label, buffer: buffer, boxed: false) - serializeInt64(amount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .labeledPrice(let label, let amount): - return ("labeledPrice", [("label", label as Any), ("amount", amount as Any)]) - } - } - - public static func parse_labeledPrice(_ reader: BufferReader) -> LabeledPrice? { - var _1: String? - _1 = parseString(reader) - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.LabeledPrice.labeledPrice(label: _1!, amount: _2!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum LangPackDifference: TypeConstructorDescription { - case langPackDifference(langCode: String, fromVersion: Int32, version: Int32, strings: [Api.LangPackString]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .langPackDifference(let langCode, let fromVersion, let version, let strings): - if boxed { - buffer.appendInt32(-209337866) - } - serializeString(langCode, buffer: buffer, boxed: false) - serializeInt32(fromVersion, buffer: buffer, boxed: false) - serializeInt32(version, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(strings.count)) - for item in strings { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .langPackDifference(let langCode, let fromVersion, let version, let strings): - return ("langPackDifference", [("langCode", langCode as Any), ("fromVersion", fromVersion as Any), ("version", version as Any), ("strings", strings as Any)]) - } - } - - public static func parse_langPackDifference(_ reader: BufferReader) -> LangPackDifference? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: [Api.LangPackString]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.LangPackString.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.LangPackDifference.langPackDifference(langCode: _1!, fromVersion: _2!, version: _3!, strings: _4!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum LangPackLanguage: TypeConstructorDescription { - case langPackLanguage(flags: Int32, name: String, nativeName: String, langCode: String, baseLangCode: String?, pluralCode: String, stringsCount: Int32, translatedCount: Int32, translationsUrl: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): - if boxed { - buffer.appendInt32(-288727837) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(name, buffer: buffer, boxed: false) - serializeString(nativeName, buffer: buffer, boxed: false) - serializeString(langCode, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeString(baseLangCode!, buffer: buffer, boxed: false)} - serializeString(pluralCode, buffer: buffer, boxed: false) - serializeInt32(stringsCount, buffer: buffer, boxed: false) - serializeInt32(translatedCount, buffer: buffer, boxed: false) - serializeString(translationsUrl, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): - return ("langPackLanguage", [("flags", flags as Any), ("name", name as Any), ("nativeName", nativeName as Any), ("langCode", langCode as Any), ("baseLangCode", baseLangCode as Any), ("pluralCode", pluralCode as Any), ("stringsCount", stringsCount as Any), ("translatedCount", translatedCount as Any), ("translationsUrl", translationsUrl as Any)]) - } - } - - public static func parse_langPackLanguage(_ reader: BufferReader) -> LangPackLanguage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - var _5: String? - if Int(_1!) & Int(1 << 1) != 0 {_5 = parseString(reader) } - var _6: String? - _6 = parseString(reader) - var _7: Int32? - _7 = reader.readInt32() - var _8: Int32? - _8 = reader.readInt32() - var _9: String? - _9 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.LangPackLanguage.langPackLanguage(flags: _1!, name: _2!, nativeName: _3!, langCode: _4!, baseLangCode: _5, pluralCode: _6!, stringsCount: _7!, translatedCount: _8!, translationsUrl: _9!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index a4134abcf7..1edeff5668 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -1,3 +1,165 @@ +public extension Api { + enum LabeledPrice: TypeConstructorDescription { + case labeledPrice(label: String, amount: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .labeledPrice(let label, let amount): + if boxed { + buffer.appendInt32(-886477832) + } + serializeString(label, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .labeledPrice(let label, let amount): + return ("labeledPrice", [("label", label as Any), ("amount", amount as Any)]) + } + } + + public static func parse_labeledPrice(_ reader: BufferReader) -> LabeledPrice? { + var _1: String? + _1 = parseString(reader) + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.LabeledPrice.labeledPrice(label: _1!, amount: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum LangPackDifference: TypeConstructorDescription { + case langPackDifference(langCode: String, fromVersion: Int32, version: Int32, strings: [Api.LangPackString]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .langPackDifference(let langCode, let fromVersion, let version, let strings): + if boxed { + buffer.appendInt32(-209337866) + } + serializeString(langCode, buffer: buffer, boxed: false) + serializeInt32(fromVersion, buffer: buffer, boxed: false) + serializeInt32(version, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(strings.count)) + for item in strings { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .langPackDifference(let langCode, let fromVersion, let version, let strings): + return ("langPackDifference", [("langCode", langCode as Any), ("fromVersion", fromVersion as Any), ("version", version as Any), ("strings", strings as Any)]) + } + } + + public static func parse_langPackDifference(_ reader: BufferReader) -> LangPackDifference? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: [Api.LangPackString]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.LangPackString.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.LangPackDifference.langPackDifference(langCode: _1!, fromVersion: _2!, version: _3!, strings: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum LangPackLanguage: TypeConstructorDescription { + case langPackLanguage(flags: Int32, name: String, nativeName: String, langCode: String, baseLangCode: String?, pluralCode: String, stringsCount: Int32, translatedCount: Int32, translationsUrl: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): + if boxed { + buffer.appendInt32(-288727837) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(name, buffer: buffer, boxed: false) + serializeString(nativeName, buffer: buffer, boxed: false) + serializeString(langCode, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(baseLangCode!, buffer: buffer, boxed: false)} + serializeString(pluralCode, buffer: buffer, boxed: false) + serializeInt32(stringsCount, buffer: buffer, boxed: false) + serializeInt32(translatedCount, buffer: buffer, boxed: false) + serializeString(translationsUrl, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): + return ("langPackLanguage", [("flags", flags as Any), ("name", name as Any), ("nativeName", nativeName as Any), ("langCode", langCode as Any), ("baseLangCode", baseLangCode as Any), ("pluralCode", pluralCode as Any), ("stringsCount", stringsCount as Any), ("translatedCount", translatedCount as Any), ("translationsUrl", translationsUrl as Any)]) + } + } + + public static func parse_langPackLanguage(_ reader: BufferReader) -> LangPackLanguage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: String? + if Int(_1!) & Int(1 << 1) != 0 {_5 = parseString(reader) } + var _6: String? + _6 = parseString(reader) + var _7: Int32? + _7 = reader.readInt32() + var _8: Int32? + _8 = reader.readInt32() + var _9: String? + _9 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.LangPackLanguage.langPackLanguage(flags: _1!, name: _2!, nativeName: _3!, langCode: _4!, baseLangCode: _5, pluralCode: _6!, stringsCount: _7!, translatedCount: _8!, translationsUrl: _9!) + } + else { + return nil + } + } + + } +} public extension Api { enum LangPackString: TypeConstructorDescription { case langPackString(key: String, value: String) diff --git a/submodules/TelegramApi/Sources/Api18.swift b/submodules/TelegramApi/Sources/Api18.swift index cce8c834d7..144d26302e 100644 --- a/submodules/TelegramApi/Sources/Api18.swift +++ b/submodules/TelegramApi/Sources/Api18.swift @@ -244,6 +244,54 @@ public extension Api { } } +public extension Api { + enum QuickReply: TypeConstructorDescription { + case quickReply(shortcutId: Int32, shortcut: String, topMessage: Int32, count: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .quickReply(let shortcutId, let shortcut, let topMessage, let count): + if boxed { + buffer.appendInt32(110563371) + } + serializeInt32(shortcutId, buffer: buffer, boxed: false) + serializeString(shortcut, buffer: buffer, boxed: false) + serializeInt32(topMessage, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .quickReply(let shortcutId, let shortcut, let topMessage, let count): + return ("quickReply", [("shortcutId", shortcutId as Any), ("shortcut", shortcut as Any), ("topMessage", topMessage as Any), ("count", count as Any)]) + } + } + + public static func parse_quickReply(_ reader: BufferReader) -> QuickReply? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.QuickReply.quickReply(shortcutId: _1!, shortcut: _2!, topMessage: _3!, count: _4!) + } + else { + return nil + } + } + + } +} public extension Api { enum Reaction: TypeConstructorDescription { case reactionCustomEmoji(documentId: Int64) diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 4427b1efe7..302e1d26cc 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -589,16 +589,191 @@ public extension Api { } } public extension Api { - enum BusinessLocation: TypeConstructorDescription { - case businessLocation(geoPoint: Api.GeoPoint, address: String) + enum BusinessAwayMessage: TypeConstructorDescription { + case businessAwayMessage(flags: Int32, shortcutId: Int32, schedule: Api.BusinessAwayMessageSchedule, users: [Int64]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .businessLocation(let geoPoint, let address): + case .businessAwayMessage(let flags, let shortcutId, let schedule, let users): if boxed { - buffer.appendInt32(-1104414653) + buffer.appendInt32(-1697809899) } - geoPoint.serialize(buffer, true) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + schedule.serialize(buffer, true) + if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users!.count)) + for item in users! { + serializeInt64(item, buffer: buffer, boxed: false) + }} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessAwayMessage(let flags, let shortcutId, let schedule, let users): + return ("businessAwayMessage", [("flags", flags as Any), ("shortcutId", shortcutId as Any), ("schedule", schedule as Any), ("users", users as Any)]) + } + } + + public static func parse_businessAwayMessage(_ reader: BufferReader) -> BusinessAwayMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.BusinessAwayMessageSchedule? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessageSchedule + } + var _4: [Int64]? + if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.BusinessAwayMessage.businessAwayMessage(flags: _1!, shortcutId: _2!, schedule: _3!, users: _4) + } + else { + return nil + } + } + + } +} +public extension Api { + enum BusinessAwayMessageSchedule: TypeConstructorDescription { + case businessAwayMessageScheduleAlways + case businessAwayMessageScheduleCustom(startDate: Int32, endDate: Int32) + case businessAwayMessageScheduleOutsideWorkHours + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessAwayMessageScheduleAlways: + if boxed { + buffer.appendInt32(-910564679) + } + + break + case .businessAwayMessageScheduleCustom(let startDate, let endDate): + if boxed { + buffer.appendInt32(-867328308) + } + serializeInt32(startDate, buffer: buffer, boxed: false) + serializeInt32(endDate, buffer: buffer, boxed: false) + break + case .businessAwayMessageScheduleOutsideWorkHours: + if boxed { + buffer.appendInt32(-1007487743) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessAwayMessageScheduleAlways: + return ("businessAwayMessageScheduleAlways", []) + case .businessAwayMessageScheduleCustom(let startDate, let endDate): + return ("businessAwayMessageScheduleCustom", [("startDate", startDate as Any), ("endDate", endDate as Any)]) + case .businessAwayMessageScheduleOutsideWorkHours: + return ("businessAwayMessageScheduleOutsideWorkHours", []) + } + } + + public static func parse_businessAwayMessageScheduleAlways(_ reader: BufferReader) -> BusinessAwayMessageSchedule? { + return Api.BusinessAwayMessageSchedule.businessAwayMessageScheduleAlways + } + public static func parse_businessAwayMessageScheduleCustom(_ reader: BufferReader) -> BusinessAwayMessageSchedule? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.BusinessAwayMessageSchedule.businessAwayMessageScheduleCustom(startDate: _1!, endDate: _2!) + } + else { + return nil + } + } + public static func parse_businessAwayMessageScheduleOutsideWorkHours(_ reader: BufferReader) -> BusinessAwayMessageSchedule? { + return Api.BusinessAwayMessageSchedule.businessAwayMessageScheduleOutsideWorkHours + } + + } +} +public extension Api { + enum BusinessGreetingMessage: TypeConstructorDescription { + case businessGreetingMessage(flags: Int32, shortcutId: Int32, users: [Int64]?, noActivityDays: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessGreetingMessage(let flags, let shortcutId, let users, let noActivityDays): + if boxed { + buffer.appendInt32(-1600596660) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users!.count)) + for item in users! { + serializeInt64(item, buffer: buffer, boxed: false) + }} + serializeInt32(noActivityDays, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessGreetingMessage(let flags, let shortcutId, let users, let noActivityDays): + return ("businessGreetingMessage", [("flags", flags as Any), ("shortcutId", shortcutId as Any), ("users", users as Any), ("noActivityDays", noActivityDays as Any)]) + } + } + + public static func parse_businessGreetingMessage(_ reader: BufferReader) -> BusinessGreetingMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Int64]? + if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } } + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 4) == 0) || _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.BusinessGreetingMessage.businessGreetingMessage(flags: _1!, shortcutId: _2!, users: _3, noActivityDays: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum BusinessLocation: TypeConstructorDescription { + case businessLocation(flags: Int32, geoPoint: Api.GeoPoint?, address: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessLocation(let flags, let geoPoint, let address): + if boxed { + buffer.appendInt32(-1403249929) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {geoPoint!.serialize(buffer, true)} serializeString(address, buffer: buffer, boxed: false) break } @@ -606,22 +781,25 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .businessLocation(let geoPoint, let address): - return ("businessLocation", [("geoPoint", geoPoint as Any), ("address", address as Any)]) + case .businessLocation(let flags, let geoPoint, let address): + return ("businessLocation", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("address", address as Any)]) } } public static func parse_businessLocation(_ reader: BufferReader) -> BusinessLocation? { - var _1: Api.GeoPoint? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.GeoPoint - } - var _2: String? - _2 = parseString(reader) + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.GeoPoint? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.GeoPoint + } } + var _3: String? + _3 = parseString(reader) let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.BusinessLocation.businessLocation(geoPoint: _1!, address: _2!) + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.BusinessLocation.businessLocation(flags: _1!, geoPoint: _2, address: _3!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api22.swift b/submodules/TelegramApi/Sources/Api22.swift index b4715fe70e..1d2611a4b9 100644 --- a/submodules/TelegramApi/Sources/Api22.swift +++ b/submodules/TelegramApi/Sources/Api22.swift @@ -582,7 +582,7 @@ public extension Api { case updateNewChannelMessage(message: Api.Message, pts: Int32, ptsCount: Int32) case updateNewEncryptedMessage(message: Api.EncryptedMessage, qts: Int32) case updateNewMessage(message: Api.Message, pts: Int32, ptsCount: Int32) - case updateNewQuickReply(quickReply: Api.messages.QuickReply) + case updateNewQuickReply(quickReply: Api.QuickReply) case updateNewScheduledMessage(message: Api.Message) case updateNewStickerSet(stickerset: Api.messages.StickerSet) case updateNotifySettings(peer: Api.NotifyPeer, notifySettings: Api.PeerNotifySettings) @@ -600,7 +600,7 @@ public extension Api { case updatePinnedSavedDialogs(flags: Int32, order: [Api.DialogPeer]?) case updatePrivacy(key: Api.PrivacyKey, rules: [Api.PrivacyRule]) case updatePtsChanged - case updateQuickReplies(quickReplies: [Api.messages.QuickReply]) + case updateQuickReplies(quickReplies: [Api.QuickReply]) case updateQuickReplyMessage(message: Api.Message) case updateReadChannelDiscussionInbox(flags: Int32, channelId: Int64, topMsgId: Int32, readMaxId: Int32, broadcastId: Int64?, broadcastPost: Int32?) case updateReadChannelDiscussionOutbox(channelId: Int64, topMsgId: Int32, readMaxId: Int32) @@ -1320,7 +1320,7 @@ public extension Api { break case .updateNewQuickReply(let quickReply): if boxed { - buffer.appendInt32(-1386034803) + buffer.appendInt32(-180508905) } quickReply.serialize(buffer, true) break @@ -1478,7 +1478,7 @@ public extension Api { break case .updateQuickReplies(let quickReplies): if boxed { - buffer.appendInt32(230929261) + buffer.appendInt32(-112784718) } buffer.appendInt32(481674261) buffer.appendInt32(Int32(quickReplies.count)) @@ -3456,9 +3456,9 @@ public extension Api { } } public static func parse_updateNewQuickReply(_ reader: BufferReader) -> Update? { - var _1: Api.messages.QuickReply? + var _1: Api.QuickReply? if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.messages.QuickReply + _1 = Api.parse(reader, signature: signature) as? Api.QuickReply } let _c1 = _1 != nil if _c1 { @@ -3756,9 +3756,9 @@ public extension Api { return Api.Update.updatePtsChanged } public static func parse_updateQuickReplies(_ reader: BufferReader) -> Update? { - var _1: [Api.messages.QuickReply]? + var _1: [Api.QuickReply]? if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.messages.QuickReply.self) + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.QuickReply.self) } let _c1 = _1 != nil if _c1 { diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 320cf74bfb..12f4890d7c 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -602,13 +602,13 @@ public extension Api { } public extension Api { enum UserFull: TypeConstructorDescription { - case userFull(flags: Int32, flags2: Int32, id: Int64, about: String?, settings: Api.PeerSettings, personalPhoto: Api.Photo?, profilePhoto: Api.Photo?, fallbackPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?, wallpaper: Api.WallPaper?, stories: Api.PeerStories?, businessWorkHours: Api.BusinessWorkHours?, businessLocation: Api.BusinessLocation?) + case userFull(flags: Int32, flags2: Int32, id: Int64, about: String?, settings: Api.PeerSettings, personalPhoto: Api.Photo?, profilePhoto: Api.Photo?, fallbackPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?, wallpaper: Api.WallPaper?, stories: Api.PeerStories?, businessWorkHours: Api.BusinessWorkHours?, businessLocation: Api.BusinessLocation?, businessGreetingMessage: Api.BusinessGreetingMessage?, businessAwayMessage: Api.BusinessAwayMessage?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation): + case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage): if boxed { - buffer.appendInt32(-501688336) + buffer.appendInt32(587153029) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false) @@ -637,14 +637,16 @@ public extension Api { if Int(flags) & Int(1 << 25) != 0 {stories!.serialize(buffer, true)} if Int(flags2) & Int(1 << 0) != 0 {businessWorkHours!.serialize(buffer, true)} if Int(flags2) & Int(1 << 1) != 0 {businessLocation!.serialize(buffer, true)} + if Int(flags2) & Int(1 << 2) != 0 {businessGreetingMessage!.serialize(buffer, true)} + if Int(flags2) & Int(1 << 3) != 0 {businessAwayMessage!.serialize(buffer, true)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation): - return ("userFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("settings", settings as Any), ("personalPhoto", personalPhoto as Any), ("profilePhoto", profilePhoto as Any), ("fallbackPhoto", fallbackPhoto as Any), ("notifySettings", notifySettings as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("commonChatsCount", commonChatsCount as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any), ("themeEmoticon", themeEmoticon as Any), ("privateForwardName", privateForwardName as Any), ("botGroupAdminRights", botGroupAdminRights as Any), ("botBroadcastAdminRights", botBroadcastAdminRights as Any), ("premiumGifts", premiumGifts as Any), ("wallpaper", wallpaper as Any), ("stories", stories as Any), ("businessWorkHours", businessWorkHours as Any), ("businessLocation", businessLocation as Any)]) + case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage): + return ("userFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("settings", settings as Any), ("personalPhoto", personalPhoto as Any), ("profilePhoto", profilePhoto as Any), ("fallbackPhoto", fallbackPhoto as Any), ("notifySettings", notifySettings as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("commonChatsCount", commonChatsCount as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any), ("themeEmoticon", themeEmoticon as Any), ("privateForwardName", privateForwardName as Any), ("botGroupAdminRights", botGroupAdminRights as Any), ("botBroadcastAdminRights", botBroadcastAdminRights as Any), ("premiumGifts", premiumGifts as Any), ("wallpaper", wallpaper as Any), ("stories", stories as Any), ("businessWorkHours", businessWorkHours as Any), ("businessLocation", businessLocation as Any), ("businessGreetingMessage", businessGreetingMessage as Any), ("businessAwayMessage", businessAwayMessage as Any)]) } } @@ -721,6 +723,14 @@ public extension Api { if Int(_2!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { _23 = Api.parse(reader, signature: signature) as? Api.BusinessLocation } } + var _24: Api.BusinessGreetingMessage? + if Int(_2!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _24 = Api.parse(reader, signature: signature) as? Api.BusinessGreetingMessage + } } + var _25: Api.BusinessAwayMessage? + if Int(_2!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { + _25 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessage + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -744,8 +754,10 @@ public extension Api { let _c21 = (Int(_1!) & Int(1 << 25) == 0) || _21 != nil let _c22 = (Int(_2!) & Int(1 << 0) == 0) || _22 != nil let _c23 = (Int(_2!) & Int(1 << 1) == 0) || _23 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 { - return Api.UserFull.userFull(flags: _1!, flags2: _2!, id: _3!, about: _4, settings: _5!, personalPhoto: _6, profilePhoto: _7, fallbackPhoto: _8, notifySettings: _9!, botInfo: _10, pinnedMsgId: _11, commonChatsCount: _12!, folderId: _13, ttlPeriod: _14, themeEmoticon: _15, privateForwardName: _16, botGroupAdminRights: _17, botBroadcastAdminRights: _18, premiumGifts: _19, wallpaper: _20, stories: _21, businessWorkHours: _22, businessLocation: _23) + let _c24 = (Int(_2!) & Int(1 << 2) == 0) || _24 != nil + let _c25 = (Int(_2!) & Int(1 << 3) == 0) || _25 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 { + return Api.UserFull.userFull(flags: _1!, flags2: _2!, id: _3!, about: _4, settings: _5!, personalPhoto: _6, profilePhoto: _7, fallbackPhoto: _8, notifySettings: _9!, botInfo: _10, pinnedMsgId: _11, commonChatsCount: _12!, folderId: _13, ttlPeriod: _14, themeEmoticon: _15, privateForwardName: _16, botGroupAdminRights: _17, botBroadcastAdminRights: _18, premiumGifts: _19, wallpaper: _20, stories: _21, businessWorkHours: _22, businessLocation: _23, businessGreetingMessage: _24, businessAwayMessage: _25) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index 4b1dfe924a..30748cf3c0 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -1290,14 +1290,14 @@ public extension Api.messages { } public extension Api.messages { enum QuickReplies: TypeConstructorDescription { - case quickReplies(quickReplies: [Api.messages.QuickReply], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case quickReplies(quickReplies: [Api.QuickReply], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) case quickRepliesNotModified public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { case .quickReplies(let quickReplies, let messages, let chats, let users): if boxed { - buffer.appendInt32(2094438528) + buffer.appendInt32(-963811691) } buffer.appendInt32(481674261) buffer.appendInt32(Int32(quickReplies.count)) @@ -1339,9 +1339,9 @@ public extension Api.messages { } public static func parse_quickReplies(_ reader: BufferReader) -> QuickReplies? { - var _1: [Api.messages.QuickReply]? + var _1: [Api.QuickReply]? if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.messages.QuickReply.self) + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.QuickReply.self) } var _2: [Api.Message]? if let _ = reader.readInt32() { @@ -1372,54 +1372,6 @@ public extension Api.messages { } } -public extension Api.messages { - enum QuickReply: TypeConstructorDescription { - case quickReply(shortcutId: Int32, shortcut: String, topMessage: Int32, count: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .quickReply(let shortcutId, let shortcut, let topMessage, let count): - if boxed { - buffer.appendInt32(-1810973582) - } - serializeInt32(shortcutId, buffer: buffer, boxed: false) - serializeString(shortcut, buffer: buffer, boxed: false) - serializeInt32(topMessage, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .quickReply(let shortcutId, let shortcut, let topMessage, let count): - return ("quickReply", [("shortcutId", shortcutId as Any), ("shortcut", shortcut as Any), ("topMessage", topMessage as Any), ("count", count as Any)]) - } - } - - public static func parse_quickReply(_ reader: BufferReader) -> QuickReply? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.QuickReply.quickReply(shortcutId: _1!, shortcut: _2!, topMessage: _3!, count: _4!) - } - else { - return nil - } - } - - } -} public extension Api.messages { enum Reactions: TypeConstructorDescription { case reactions(hash: Int64, reactions: [Api.Reaction]) diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index d8338e5bd5..55cab5fd70 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -1261,12 +1261,44 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func updateBusinessAwayMessage(flags: Int32, message: Api.InputBusinessAwayMessage?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1570078811) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {message!.serialize(buffer, true)} + return (FunctionDescription(name: "account.updateBusinessAwayMessage", parameters: [("flags", String(describing: flags)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} +public extension Api.functions.account { + static func updateBusinessGreetingMessage(flags: Int32, message: Api.InputBusinessGreetingMessage?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1724755908) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {message!.serialize(buffer, true)} + return (FunctionDescription(name: "account.updateBusinessGreetingMessage", parameters: [("flags", String(describing: flags)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.account { static func updateBusinessLocation(flags: Int32, geoPoint: Api.InputGeoPoint?, address: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1040005974) + buffer.appendInt32(-1637149926) serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {geoPoint!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {geoPoint!.serialize(buffer, true)} if Int(flags) & Int(1 << 0) != 0 {serializeString(address!, buffer: buffer, boxed: false)} return (FunctionDescription(name: "account.updateBusinessLocation", parameters: [("flags", String(describing: flags)), ("geoPoint", String(describing: geoPoint)), ("address", String(describing: address))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in let reader = BufferReader(buffer) @@ -4645,6 +4677,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func deleteQuickReplyShortcut(shortcutId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1019234112) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.deleteQuickReplyShortcut", parameters: [("shortcutId", String(describing: shortcutId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func deleteRevokedExportedChatInvites(peer: Api.InputPeer, adminId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -4921,9 +4968,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-709978674) + buffer.appendInt32(-721186296) serializeInt32(flags, buffer: buffer, boxed: false) fromPeer.serialize(buffer, true) buffer.appendInt32(481674261) @@ -4940,7 +4987,7 @@ public extension Api.functions.messages { if Int(flags) & Int(1 << 9) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 10) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} - if Int(flags) & Int(1 << 17) != 0 {serializeString(quickReplyShortcut!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} return (FunctionDescription(name: "messages.forwardMessages", parameters: [("flags", String(describing: flags)), ("fromPeer", String(describing: fromPeer)), ("id", String(describing: id)), ("randomId", String(describing: randomId)), ("toPeer", String(describing: toPeer)), ("topMsgId", String(describing: topMsgId)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? @@ -7186,9 +7233,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendInlineBotResult(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, randomId: Int64, queryId: Int64, id: String, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendInlineBotResult(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, randomId: Int64, queryId: Int64, id: String, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-407001673) + buffer.appendInt32(1052698730) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {replyTo!.serialize(buffer, true)} @@ -7197,7 +7244,7 @@ public extension Api.functions.messages { serializeString(id, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 10) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} - if Int(flags) & Int(1 << 17) != 0 {serializeString(quickReplyShortcut!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} return (FunctionDescription(name: "messages.sendInlineBotResult", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("randomId", String(describing: randomId)), ("queryId", String(describing: queryId)), ("id", String(describing: id)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? @@ -7209,9 +7256,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendMedia(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, media: Api.InputMedia, message: String, randomId: Int64, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendMedia(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, media: Api.InputMedia, message: String, randomId: Int64, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-10487971) + buffer.appendInt32(2077646913) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {replyTo!.serialize(buffer, true)} @@ -7226,7 +7273,7 @@ public extension Api.functions.messages { }} if Int(flags) & Int(1 << 10) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} - if Int(flags) & Int(1 << 17) != 0 {serializeString(quickReplyShortcut!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} return (FunctionDescription(name: "messages.sendMedia", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("media", String(describing: media)), ("message", String(describing: message)), ("randomId", String(describing: randomId)), ("replyMarkup", String(describing: replyMarkup)), ("entities", String(describing: entities)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? @@ -7238,9 +7285,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendMessage(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, message: String, randomId: Int64, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendMessage(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, message: String, randomId: Int64, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1750387040) + buffer.appendInt32(-537394132) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {replyTo!.serialize(buffer, true)} @@ -7254,7 +7301,7 @@ public extension Api.functions.messages { }} if Int(flags) & Int(1 << 10) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} - if Int(flags) & Int(1 << 17) != 0 {serializeString(quickReplyShortcut!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} return (FunctionDescription(name: "messages.sendMessage", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("message", String(describing: message)), ("randomId", String(describing: randomId)), ("replyMarkup", String(describing: replyMarkup)), ("entities", String(describing: entities)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? @@ -7266,9 +7313,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendMultiMedia(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, multiMedia: [Api.InputSingleMedia], scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendMultiMedia(flags: Int32, peer: Api.InputPeer, replyTo: Api.InputReplyTo?, multiMedia: [Api.InputSingleMedia], scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-2027543192) + buffer.appendInt32(211175177) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {replyTo!.serialize(buffer, true)} @@ -7279,7 +7326,7 @@ public extension Api.functions.messages { } if Int(flags) & Int(1 << 10) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} - if Int(flags) & Int(1 << 17) != 0 {serializeString(quickReplyShortcut!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} return (FunctionDescription(name: "messages.sendMultiMedia", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("replyTo", String(describing: replyTo)), ("multiMedia", String(describing: multiMedia)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index 80bc038f38..16de0844ae 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -262,6 +262,116 @@ public extension Api { } } +public extension Api { + enum InputBusinessAwayMessage: TypeConstructorDescription { + case inputBusinessAwayMessage(flags: Int32, shortcutId: Int32, schedule: Api.BusinessAwayMessageSchedule, users: [Api.InputUser]?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputBusinessAwayMessage(let flags, let shortcutId, let schedule, let users): + if boxed { + buffer.appendInt32(-831530424) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + schedule.serialize(buffer, true) + if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users!.count)) + for item in users! { + item.serialize(buffer, true) + }} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputBusinessAwayMessage(let flags, let shortcutId, let schedule, let users): + return ("inputBusinessAwayMessage", [("flags", flags as Any), ("shortcutId", shortcutId as Any), ("schedule", schedule as Any), ("users", users as Any)]) + } + } + + public static func parse_inputBusinessAwayMessage(_ reader: BufferReader) -> InputBusinessAwayMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.BusinessAwayMessageSchedule? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessageSchedule + } + var _4: [Api.InputUser]? + if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputBusinessAwayMessage.inputBusinessAwayMessage(flags: _1!, shortcutId: _2!, schedule: _3!, users: _4) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputBusinessGreetingMessage: TypeConstructorDescription { + case inputBusinessGreetingMessage(flags: Int32, shortcutId: Int32, users: [Api.InputUser]?, noActivityDays: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputBusinessGreetingMessage(let flags, let shortcutId, let users, let noActivityDays): + if boxed { + buffer.appendInt32(2102015497) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(shortcutId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users!.count)) + for item in users! { + item.serialize(buffer, true) + }} + serializeInt32(noActivityDays, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputBusinessGreetingMessage(let flags, let shortcutId, let users, let noActivityDays): + return ("inputBusinessGreetingMessage", [("flags", flags as Any), ("shortcutId", shortcutId as Any), ("users", users as Any), ("noActivityDays", noActivityDays as Any)]) + } + } + + public static func parse_inputBusinessGreetingMessage(_ reader: BufferReader) -> InputBusinessGreetingMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.InputUser]? + if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) + } } + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 4) == 0) || _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputBusinessGreetingMessage.inputBusinessGreetingMessage(flags: _1!, shortcutId: _2!, users: _3, noActivityDays: _4!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputChannel: TypeConstructorDescription { case inputChannel(channelId: Int64, accessHash: Int64) diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 0d1df5f992..79c3c4658a 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -62,6 +62,7 @@ enum AccountStateGlobalNotificationSettingsSubject { enum AccountStateMutationOperation { case AddMessages([StoreMessage], AddMessagesLocation) case AddScheduledMessages([StoreMessage]) + case AddQuickReplyMessages([StoreMessage]) case DeleteMessagesWithGlobalIds([Int32]) case DeleteMessages([MessageId]) case EditMessage(MessageId, StoreMessage) @@ -332,6 +333,10 @@ struct AccountMutableState { self.addOperation(.AddScheduledMessages(messages)) } + mutating func addQuickReplyMessages(_ messages: [StoreMessage]) { + self.addOperation(.AddQuickReplyMessages(messages)) + } + mutating func addDisplayAlert(_ text: String, isDropAuth: Bool) { self.displayAlerts.append((text: text, isDropAuth: isDropAuth)) } @@ -709,6 +714,19 @@ struct AccountMutableState { } } } + case let .AddQuickReplyMessages(messages): + for message in messages { + if case let .Id(id) = message.id { + self.storedMessages.insert(id) + inner: for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + self.referencedReplyMessageIds.add(sourceId: id, targetId: attribute.messageId) + } else if let attribute = attribute as? ReplyStoryAttribute { + self.referencedStoryIds.insert(attribute.storyId) + } + } + } + } case let .UpdateState(state): self.state = state case let .UpdateChannelState(peerId, pts): diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 84789814ab..c70bf2b60f 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -208,6 +208,7 @@ private var declaredEncodables: Void = { declareEncodable(WebpagePreviewMessageAttribute.self, f: { WebpagePreviewMessageAttribute(decoder: $0) }) declareEncodable(DerivedDataMessageAttribute.self, f: { DerivedDataMessageAttribute(decoder: $0) }) declareEncodable(TelegramApplicationIcons.self, f: { TelegramApplicationIcons(decoder: $0) }) + declareEncodable(OutgoingQuickReplyMessageAttribute.self, f: { OutgoingQuickReplyMessageAttribute(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 7829c5dcd7..5ecd19cfc1 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -597,8 +597,13 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes extension StoreMessage { convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { switch apiMessage { - case let .message(flags, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, _): + case let .message(flags, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId): let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId + + var namespace = namespace + if quickReplyShortcutId != nil { + namespace = Namespaces.Message.QuickReplyCloud + } let peerId: PeerId var authorId: PeerId? @@ -663,7 +668,7 @@ extension StoreMessage { threadId = Int64(threadIdValue.id) } } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: namespace, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) } if let replyHeader = replyHeader { attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) @@ -729,6 +734,10 @@ extension StoreMessage { if peerId == accountPeerId, let savedPeerId = savedPeerId { threadId = savedPeerId.peerId.toInt64() } + + if let quickReplyShortcutId { + threadId = Int64(quickReplyShortcutId) + } let messageText = message var medias: [Media] = [] @@ -791,7 +800,7 @@ extension StoreMessage { attributes.append(InlineBotMessageAttribute(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(viaBotId)), title: nil)) } - if namespace != Namespaces.Message.ScheduledCloud { + if namespace != Namespaces.Message.ScheduledCloud && namespace != Namespaces.Message.QuickReplyCloud { if let views = views { attributes.append(ViewCountMessageAttribute(count: Int(views))) } diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 4c23fe9516..2b70d10d63 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -138,6 +138,15 @@ public enum EnqueueMessage { } } + public func withUpdatedThreadId(_ threadId: Int64?) -> EnqueueMessage { + switch self { + case let .message(text, attributes, inlineStickers, mediaReference, _, replyToMessageId, replyToStoryId, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): + return .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets) + case let .forward(source, _, grouping, attributes, correlationId): + return .forward(source: source, threadId: threadId, grouping: grouping, attributes: attributes, correlationId: correlationId) + } + } + public var groupingKey: Int64? { if case let .message(_, _, _, _, _, _, _, localGroupingKey, _, _) = self { return localGroupingKey @@ -225,6 +234,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt return true case _ as OutgoingScheduleInfoMessageAttribute: return true + case _ as OutgoingQuickReplyMessageAttribute: + return true case _ as EmbeddedMediaStickersMessageAttribute: return true case _ as EmojiSearchQueryMessageAttribute: @@ -254,6 +265,8 @@ private func filterMessageAttributesForForwardedMessage(_ attributes: [MessageAt return true case _ as OutgoingScheduleInfoMessageAttribute: return true + case _ as OutgoingQuickReplyMessageAttribute: + return true case _ as ForwardOptionsMessageAttribute: return true case _ as SendAsMessageAttribute: @@ -673,6 +686,9 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, messageNamespace = Namespaces.Message.ScheduledLocal effectiveTimestamp = attribute.scheduleTime } + } else if attribute is OutgoingQuickReplyMessageAttribute { + messageNamespace = Namespaces.Message.QuickReplyLocal + effectiveTimestamp = 0 } else if let attribute = attribute as? SendAsMessageAttribute { if let peer = transaction.getPeer(attribute.peerId) { sendAsPeer = peer @@ -698,11 +714,14 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, if messageNamespace != Namespaces.Message.ScheduledLocal { attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) } + if messageNamespace != Namespaces.Message.QuickReplyLocal { + attributes.removeAll(where: { $0 is OutgoingQuickReplyMessageAttribute }) + } if let peer = peer as? TelegramChannel { switch peer.info { case let .broadcast(info): - if messageNamespace != Namespaces.Message.ScheduledLocal { + if messageNamespace != Namespaces.Message.ScheduledLocal && messageNamespace != Namespaces.Message.QuickReplyLocal { attributes.append(ViewCountMessageAttribute(count: 1)) } if info.flags.contains(.messagesShouldHaveSignatures) { @@ -911,6 +930,9 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, messageNamespace = Namespaces.Message.ScheduledLocal effectiveTimestamp = attribute.scheduleTime } + } else if attribute is OutgoingQuickReplyMessageAttribute { + messageNamespace = Namespaces.Message.QuickReplyLocal + effectiveTimestamp = 0 } else if let attribute = attribute as? ReplyMessageAttribute { if let threadMessageId = attribute.threadMessageId { threadId = Int64(threadMessageId.id) @@ -940,6 +962,9 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, if messageNamespace != Namespaces.Message.ScheduledLocal { attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) } + if messageNamespace != Namespaces.Message.QuickReplyLocal { + attributes.removeAll(where: { $0 is OutgoingQuickReplyMessageAttribute }) + } let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: attributes, media: sourceMessage.media, textEntities: entitiesAttribute?.entities, isPinned: false) diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index e568ed9c78..8744792086 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -174,7 +174,13 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, } } - return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime, quickReplyShortcutId: nil)) + var quickReplyShortcutId: Int32? + if messageId.namespace == Namespaces.Message.QuickReplyCloud { + quickReplyShortcutId = Int32(clamping: message.threadId ?? 0) + flags |= Int32(1 << 17) + } + + return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime, quickReplyShortcutId: quickReplyShortcutId)) |> map { result -> Api.Updates? in return result } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index a68c802ace..7dfad231f5 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1658,12 +1658,26 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum, namespace: Namespaces.Message.ScheduledCloud) { updatedState.addScheduledMessages([message]) } + case let .updateQuickReplyMessage(apiMessage): + var peerIsForum = false + if let peerId = apiMessage.peerId { + peerIsForum = updatedState.isPeerForum(peerId: peerId) + } + if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum, namespace: Namespaces.Message.QuickReplyCloud) { + updatedState.addQuickReplyMessages([message]) + } case let .updateDeleteScheduledMessages(peer, messages): var messageIds: [MessageId] = [] for message in messages { messageIds.append(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.ScheduledCloud, id: message)) } updatedState.deleteMessages(messageIds) + case let .updateDeleteQuickReplyMessages(_, messages): + var messageIds: [MessageId] = [] + for message in messages { + messageIds.append(MessageId(peerId: accountPeerId, namespace: Namespaces.Message.QuickReplyCloud, id: message)) + } + updatedState.deleteMessages(messageIds) case let .updateTheme(theme): updatedState.updateTheme(TelegramTheme(apiTheme: theme)) case let .updateMessageID(id, randomId): @@ -3250,6 +3264,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddMessages: OptimizeAddMessagesState? var currentAddScheduledMessages: OptimizeAddMessagesState? + var currentAddQuickReplyMessages: OptimizeAddMessagesState? for operation in operations { switch operation { case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper: @@ -3259,6 +3274,9 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) if let currentAddScheduledMessages = currentAddScheduledMessages, !currentAddScheduledMessages.messages.isEmpty { result.append(.AddScheduledMessages(currentAddScheduledMessages.messages)) } + if let currentAddQuickReplyMessages = currentAddQuickReplyMessages, !currentAddQuickReplyMessages.messages.isEmpty { + result.append(.AddQuickReplyMessages(currentAddQuickReplyMessages.messages)) + } currentAddMessages = nil result.append(operation) case let .UpdateState(state): @@ -3284,6 +3302,12 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) } else { currentAddScheduledMessages = OptimizeAddMessagesState(messages: messages, location: .Random) } + case let .AddQuickReplyMessages(messages): + if let currentAddQuickReplyMessages = currentAddQuickReplyMessages { + currentAddQuickReplyMessages.messages.append(contentsOf: messages) + } else { + currentAddQuickReplyMessages = OptimizeAddMessagesState(messages: messages, location: .Random) + } } } if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { @@ -3294,6 +3318,10 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) result.append(.AddScheduledMessages(currentAddScheduledMessages.messages)) } + if let currentAddQuickReplyMessages = currentAddQuickReplyMessages, !currentAddQuickReplyMessages.messages.isEmpty { + result.append(.AddQuickReplyMessages(currentAddQuickReplyMessages.messages)) + } + if let updatedState = updatedState { result.append(.UpdateState(updatedState)) } @@ -3745,6 +3773,16 @@ func replayFinalState( let _ = transaction.addMessages(messages, location: .Random) } } + case let .AddQuickReplyMessages(messages): + for message in messages { + if case let .Id(id) = message.id, let _ = transaction.getMessage(id) { + transaction.updateMessage(id) { _ -> PostboxUpdateMessage in + return .update(message) + } + } else { + let _ = transaction.addMessages(messages, location: .Random) + } + } case let .DeleteMessagesWithGlobalIds(ids): var resourceIds: [MediaResourceId] = [] transaction.deleteMessagesWithGlobalIds(ids, forEachMedia: { media in diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 50e1d1d148..330d2ab0b3 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -61,16 +61,30 @@ private func pollMessages(entries: [MessageHistoryEntry]) -> (Set, [M return (messageIds, messages) } -private func fetchWebpage(account: Account, messageId: MessageId) -> Signal { +private func fetchWebpage(account: Account, messageId: MessageId, threadId: Int64?) -> Signal { let accountPeerId = account.peerId return account.postbox.loadedPeerWithId(messageId.peerId) |> take(1) |> mapToSignal { peer in if let inputPeer = apiInputPeer(peer) { - let isScheduledMessage = Namespaces.Message.allScheduled.contains(messageId.namespace) + let targetMessageNamespace: MessageId.Namespace + if Namespaces.Message.allScheduled.contains(messageId.namespace) { + targetMessageNamespace = Namespaces.Message.ScheduledCloud + } else if Namespaces.Message.allQuickReply.contains(messageId.namespace) { + targetMessageNamespace = Namespaces.Message.QuickReplyCloud + } else { + targetMessageNamespace = Namespaces.Message.Cloud + } + let messages: Signal - if isScheduledMessage { + if Namespaces.Message.allScheduled.contains(messageId.namespace) { messages = account.network.request(Api.functions.messages.getScheduledMessages(peer: inputPeer, id: [messageId.id])) + } else if Namespaces.Message.allQuickReply.contains(messageId.namespace) { + if let threadId { + messages = account.network.request(Api.functions.messages.getQuickReplyMessages(flags: 1 << 0, shortcutId: Int32(clamping: threadId), id: [messageId.id], hash: 0)) + } else { + messages = .never() + } } else { switch inputPeer { case let .inputPeerChannel(channelId, accessHash): @@ -109,7 +123,7 @@ private func fetchWebpage(account: Account, messageId: MessageId) -> Signal() - private var updatedUnsupportedMediaMessageIdsAndTimestamps: [MessageId: Int32] = [:] + private var updatedUnsupportedMediaMessageIdsAndTimestamps: [MessageAndThreadId: Int32] = [:] private var refreshSecretChatMediaMessageIdsAndTimestamps: [MessageId: Int32] = [:] private var refreshStoriesForMessageIdsAndTimestamps: [MessageId: Int32] = [:] private var nextUpdatedUnsupportedMediaDisposableId: Int32 = 0 @@ -367,7 +381,7 @@ public final class AccountViewTracker { } } - private func updatePendingWebpages(viewId: Int32, messageIds: Set, localWebpages: [MessageId: (MediaId, String)]) { + private func updatePendingWebpages(viewId: Int32, threadId: Int64?, messageIds: Set, localWebpages: [MessageId: (MediaId, String)]) { self.queue.async { var addedMessageIds: [MessageId] = [] var removedMessageIds: [MessageId] = [] @@ -440,7 +454,7 @@ public final class AccountViewTracker { } }) } else if messageId.namespace == Namespaces.Message.Cloud { - self.webpageDisposables[messageId] = fetchWebpage(account: account, messageId: messageId).start(completed: { [weak self] in + self.webpageDisposables[messageId] = fetchWebpage(account: account, messageId: messageId, threadId: threadId).start(completed: { [weak self] in if let strongSelf = self { strongSelf.queue.async { strongSelf.webpageDisposables.removeValue(forKey: messageId) @@ -1009,9 +1023,9 @@ public final class AccountViewTracker { } } - public func updateUnsupportedMediaForMessageIds(messageIds: Set) { + public func updateUnsupportedMediaForMessageIds(messageIds: Set) { self.queue.async { - var addedMessageIds: [MessageId] = [] + var addedMessageIds: [MessageAndThreadId] = [] let timestamp = Int32(CFAbsoluteTimeGetCurrent()) for messageId in messageIds { let messageTimestamp = self.updatedUnsupportedMediaMessageIdsAndTimestamps[messageId] @@ -1021,14 +1035,14 @@ public final class AccountViewTracker { } } if !addedMessageIds.isEmpty { - for (peerId, messageIds) in messagesIdsGroupedByPeerId(Set(addedMessageIds)) { + for (peerIdAndThreadId, messageIds) in messagesIdsGroupedByPeerId(Set(addedMessageIds)) { let disposableId = self.nextUpdatedUnsupportedMediaDisposableId self.nextUpdatedUnsupportedMediaDisposableId += 1 if let account = self.account { let accountPeerId = account.peerId let signal = account.postbox.transaction { transaction -> Peer? in - if let peer = transaction.getPeer(peerId) { + if let peer = transaction.getPeer(peerIdAndThreadId.peerId) { return peer } else { return nil @@ -1043,9 +1057,15 @@ public final class AccountViewTracker { if let inputPeer = apiInputPeer(peer) { fetchSignal = account.network.request(Api.functions.messages.getScheduledMessages(peer: inputPeer, id: messageIds.map { $0.id })) } - } else if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup { + } else if let messageId = messageIds.first, messageId.namespace == Namespaces.Message.QuickReplyCloud { + if let threadId = peerIdAndThreadId.threadId { + fetchSignal = account.network.request(Api.functions.messages.getQuickReplyMessages(flags: 1 << 0, shortcutId: Int32(clamping: threadId), id: messageIds.map { $0.id }, hash: 0)) + } else { + fetchSignal = .never() + } + } else if peerIdAndThreadId.peerId.namespace == Namespaces.Peer.CloudUser || peerIdAndThreadId.peerId.namespace == Namespaces.Peer.CloudGroup { fetchSignal = account.network.request(Api.functions.messages.getMessages(id: messageIds.map { Api.InputMessage.inputMessageID(id: $0.id) })) - } else if peerId.namespace == Namespaces.Peer.CloudChannel { + } else if peerIdAndThreadId.peerId.namespace == Namespaces.Peer.CloudChannel { if let inputChannel = apiInputChannel(peer) { fetchSignal = account.network.request(Api.functions.channels.getMessages(channel: inputChannel, id: messageIds.map { Api.InputMessage.inputMessageID(id: $0.id) })) } @@ -1815,7 +1835,7 @@ public final class AccountViewTracker { if let strongSelf = self { strongSelf.queue.async { let (messageIds, localWebpages) = pendingWebpages(entries: next.0.entries) - strongSelf.updatePendingWebpages(viewId: viewId, messageIds: messageIds, localWebpages: localWebpages) + strongSelf.updatePendingWebpages(viewId: viewId, threadId: chatLocation.threadId, messageIds: messageIds, localWebpages: localWebpages) let (pollMessageIds, pollMessageDict) = pollMessages(entries: next.0.entries) strongSelf.updatePolls(viewId: viewId, messageIds: pollMessageIds, messages: pollMessageDict) if case let .peer(peerId, _) = chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel { @@ -1828,7 +1848,7 @@ public final class AccountViewTracker { }, disposed: { [weak self] viewId in if let strongSelf = self { strongSelf.queue.async { - strongSelf.updatePendingWebpages(viewId: viewId, messageIds: [], localWebpages: [:]) + strongSelf.updatePendingWebpages(viewId: viewId, threadId: chatLocation.threadId, messageIds: [], localWebpages: [:]) strongSelf.updatePolls(viewId: viewId, messageIds: [], messages: [:]) switch chatLocation { case let .peer(peerId, _): @@ -1945,14 +1965,14 @@ public final class AccountViewTracker { if let strongSelf = self { strongSelf.queue.async { let (messageIds, localWebpages) = pendingWebpages(entries: next.0.entries) - strongSelf.updatePendingWebpages(viewId: viewId, messageIds: messageIds, localWebpages: localWebpages) + strongSelf.updatePendingWebpages(viewId: viewId, threadId: chatLocation.threadId, messageIds: messageIds, localWebpages: localWebpages) strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0, location: chatLocation) } } }, disposed: { [weak self] viewId in if let strongSelf = self { strongSelf.queue.async { - strongSelf.updatePendingWebpages(viewId: viewId, messageIds: [], localWebpages: [:]) + strongSelf.updatePendingWebpages(viewId: viewId, threadId: chatLocation.threadId, messageIds: [], localWebpages: [:]) strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: nil) } } @@ -1962,6 +1982,36 @@ public final class AccountViewTracker { } } + public func quickReplyMessagesViewForLocation(quickReplyId: Int32, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + guard let account = self.account else { + return .never() + } + let chatLocation: ChatLocationInput = .peer(peerId: account.peerId, threadId: Int64(quickReplyId)) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Namespaces.Message.allQuickReply), orderStatistics: [], additionalData: additionalData) + return withState(signal, { [weak self] () -> Int32 in + if let strongSelf = self { + return OSAtomicIncrement32(&strongSelf.nextViewId) + } else { + return -1 + } + }, next: { [weak self] next, viewId in + if let strongSelf = self { + strongSelf.queue.async { + let (messageIds, localWebpages) = pendingWebpages(entries: next.0.entries) + strongSelf.updatePendingWebpages(viewId: viewId, threadId: Int64(quickReplyId), messageIds: messageIds, localWebpages: localWebpages) + strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0, location: chatLocation) + } + } + }, disposed: { [weak self] viewId in + if let strongSelf = self { + strongSelf.queue.async { + strongSelf.updatePendingWebpages(viewId: viewId, threadId: Int64(quickReplyId), messageIds: [], localWebpages: [:]) + strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: nil) + } + } + }) + } + public func aroundMessageOfInterestHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange? = nil, count: Int, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> @@ -1994,7 +2044,7 @@ public final class AccountViewTracker { topTaggedMessageIdNamespaces: [], tag: tag, appendMessagesFromTheSameGroup: false, - namespaces: .not(Namespaces.Message.allScheduled), + namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread @@ -2020,7 +2070,7 @@ public final class AccountViewTracker { topTaggedMessageIdNamespaces: [], tag: tag, appendMessagesFromTheSameGroup: false, - namespaces: .not(Namespaces.Message.allScheduled), + namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread @@ -2028,10 +2078,10 @@ public final class AccountViewTracker { } } - return account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + return account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) } } else { - signal = account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + signal = account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, customUnreadMessageId: nil, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) } return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, fixedCombinedReadStates: nil, addHoleIfNeeded: true) } else { @@ -2041,7 +2091,7 @@ public final class AccountViewTracker { public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange? = nil, count: Int, ignoreRelatedChats: Bool, messageId: MessageId, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { - let signal = account.postbox.aroundIdMessageHistoryViewForLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, ignoreRelatedChats: ignoreRelatedChats, messageId: messageId, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + let signal = account.postbox.aroundIdMessageHistoryViewForLocation(chatLocation, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, ignoreRelatedChats: ignoreRelatedChats, messageId: messageId, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, fixedCombinedReadStates: nil, addHoleIfNeeded: false) } else { return .never() @@ -2059,7 +2109,7 @@ public final class AccountViewTracker { case let .message(index): inputAnchor = .index(index) } - let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, clipHoles: clipHoles, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, count: count, clipHoles: clipHoles, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData), useRootInterfaceStateForThread: useRootInterfaceStateForThread) return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, fixedCombinedReadStates: fixedCombinedReadStates, addHoleIfNeeded: false) } else { return .never() diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 79999c1058..0a2a0b1fb4 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -129,7 +129,9 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes let updatedId: MessageId if let messageId = messageId { var namespace: MessageId.Namespace = Namespaces.Message.Cloud - if let updatedTimestamp = updatedTimestamp { + if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + namespace = Namespaces.Message.QuickReplyCloud + } else if let updatedTimestamp = updatedTimestamp { if message.scheduleTime != nil && message.scheduleTime == updatedTimestamp { namespace = Namespaces.Message.ScheduledCloud } @@ -141,21 +143,17 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes updatedId = currentMessage.id } - for attribute in currentMessage.attributes { - if let attribute = attribute as? OutgoingMessageInfoAttribute { - bubbleUpEmojiOrStickersets = attribute.bubbleUpEmojiOrStickersets - } - } - let media: [Media] var attributes: [MessageAttribute] let text: String let forwardInfo: StoreMessageForwardInfo? + let threadId: Int64? if let apiMessage = apiMessage, let apiMessagePeerId = apiMessage.peerId, let updatedMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: transaction.getPeer(apiMessagePeerId)?.isForum ?? false) { media = updatedMessage.media attributes = updatedMessage.attributes text = updatedMessage.text forwardInfo = updatedMessage.forwardInfo + threadId = updatedMessage.threadId } else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities, ttlPeriod) = result { let (mediaValue, _, nonPremium, hasSpoiler, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) if let mediaValue = mediaValue { @@ -197,16 +195,36 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes } } } + if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + for i in 0 ..< updatedAttributes.count { + if updatedAttributes[i] is OutgoingQuickReplyMessageAttribute { + updatedAttributes.remove(at: i) + break + } + } + } attributes = updatedAttributes text = currentMessage.text forwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + threadId = currentMessage.threadId } else { media = currentMessage.media attributes = currentMessage.attributes text = currentMessage.text forwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + threadId = currentMessage.threadId + } + + for attribute in currentMessage.attributes { + if let attribute = attribute as? OutgoingMessageInfoAttribute { + bubbleUpEmojiOrStickersets = attribute.bubbleUpEmojiOrStickersets + } else if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + if let threadId { + _internal_applySentQuickReplyMessage(transaction: transaction, shortcut: attribute.shortcut, quickReplyId: Int32(clamping: threadId)) + } + } } if let channelPts = channelPts { @@ -255,7 +273,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes let (tags, globalTags) = tagsForStoreMessage(incoming: currentMessage.flags.contains(.Incoming), attributes: attributes, media: media, textEntities: entitiesAttribute?.entities, isPinned: currentMessage.tags.contains(.pinned)) - if currentMessage.id.peerId.namespace == Namespaces.Peer.CloudChannel, !currentMessage.flags.contains(.Incoming), !Namespaces.Message.allScheduled.contains(currentMessage.id.namespace) { + if currentMessage.id.peerId.namespace == Namespaces.Peer.CloudChannel, !currentMessage.flags.contains(.Incoming), !Namespaces.Message.allNonRegular.contains(currentMessage.id.namespace) { let peerId = currentMessage.id.peerId if let peer = transaction.getPeer(peerId) { if let peer = peer as? TelegramChannel { @@ -344,7 +362,9 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage let updatedRawMessageIds = result.updatedRawMessageIds var namespace = Namespaces.Message.Cloud - if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { + if Namespaces.Message.allQuickReply.contains(messages[0].id.namespace) { + namespace = Namespaces.Message.QuickReplyCloud + } else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { namespace = Namespaces.Message.ScheduledCloud } @@ -411,6 +431,16 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] + if let (message, _, updatedMessage) = mapping.first { + for attribute in message.attributes { + if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + if let threadId = updatedMessage.threadId { + _internal_applySentQuickReplyMessage(transaction: transaction, shortcut: attribute.shortcut, quickReplyId: Int32(clamping: threadId)) + } + } + } + } + for (message, _, updatedMessage) in mapping { transaction.updateMessage(message.id, update: { currentMessage in let updatedId: MessageId diff --git a/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift b/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift index 58b9bd40ed..acf9a30261 100644 --- a/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift +++ b/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift @@ -1,7 +1,7 @@ import Postbox -func cloudChatAddRemoveMessagesOperation(transaction: Transaction, peerId: PeerId, messageIds: [MessageId], type: CloudChatRemoveMessagesType) { - transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatRemoveMessagesOperation(messageIds: messageIds, type: type)) +func cloudChatAddRemoveMessagesOperation(transaction: Transaction, peerId: PeerId, threadId: Int64?, messageIds: [MessageId], type: CloudChatRemoveMessagesType) { + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatRemoveMessagesOperation(messageIds: messageIds, threadId: threadId, type: type)) } func cloudChatAddRemoveChatOperation(transaction: Transaction, peerId: PeerId, reportChatSpam: Bool, deleteGloballyIfPossible: Bool) { @@ -15,7 +15,26 @@ func cloudChatAddClearHistoryOperation(transaction: Transaction, peerId: PeerId, messageIds.append(message.id) return true } - cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, messageIds: messageIds, type: .forLocalPeer) + cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, threadId: threadId, messageIds: messageIds, type: .forLocalPeer) + } else if type == .quickReplyMessages { + var messageIds: [MessageId] = [] + transaction.withAllMessages(peerId: peerId, namespace: Namespaces.Message.QuickReplyCloud) { message -> Bool in + messageIds.append(message.id) + return true + } + cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, threadId: threadId, messageIds: messageIds, type: .forLocalPeer) + + let topMessageId: MessageId? + if let explicitTopMessageId = explicitTopMessageId { + topMessageId = explicitTopMessageId + } else { + topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.QuickReplyCloud) + } + if let topMessageId = topMessageId { + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: topMessageId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type)) + } else if case .forEveryone = type { + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: .max), threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type)) + } } else { let topMessageId: MessageId? if let explicitTopMessageId = explicitTopMessageId { diff --git a/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift index ddaea2d2c3..4290ad04f4 100644 --- a/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift +++ b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift @@ -28,78 +28,60 @@ private final class HistoryStateValidationContext { private enum HistoryState { case channel(PeerId, ChannelState) - //case group(PeerGroupId, TelegramPeerGroupState) case scheduledMessages(PeerId) + case quickReplyMessages(PeerId, Int32) var hasInvalidationIndex: Bool { switch self { - case let .channel(_, state): - return state.invalidatedPts != nil - /*case let .group(_, state): - return state.invalidatedStateIndex != nil*/ - case .scheduledMessages: - return false + case let .channel(_, state): + return state.invalidatedPts != nil + case .scheduledMessages: + return false + case .quickReplyMessages: + return false } } func isMessageValid(_ message: Message) -> Bool { switch self { - case let .channel(_, state): - if let invalidatedPts = state.invalidatedPts { - var messagePts: Int32? - inner: for attribute in message.attributes { - if let attribute = attribute as? ChannelMessageStateVersionAttribute { - messagePts = attribute.pts - break inner - } + case let .channel(_, state): + if let invalidatedPts = state.invalidatedPts { + var messagePts: Int32? + inner: for attribute in message.attributes { + if let attribute = attribute as? ChannelMessageStateVersionAttribute { + messagePts = attribute.pts + break inner } - var requiresValidation = false - if let messagePts = messagePts { - if messagePts < invalidatedPts { - requiresValidation = true - } - } else { - requiresValidation = true - } - - return !requiresValidation - } else { - return true } - /*case let .group(_, state): - if let invalidatedStateIndex = state.invalidatedStateIndex { - var messageStateIndex: Int32? - inner: for attribute in message.attributes { - if let attribute = attribute as? PeerGroupMessageStateVersionAttribute { - messageStateIndex = attribute.stateIndex - break inner - } - } - var requiresValidation = false - if let messageStateIndex = messageStateIndex { - if messageStateIndex < invalidatedStateIndex { - requiresValidation = true - } - } else { + + var requiresValidation = false + if let messagePts = messagePts { + if messagePts < invalidatedPts { requiresValidation = true } - return !requiresValidation } else { - return true - }*/ - case .scheduledMessages: - return false + requiresValidation = true + } + + return !requiresValidation + } else { + return true + } + case .scheduledMessages: + return false + case .quickReplyMessages: + return false } } func matchesPeerId(_ peerId: PeerId) -> Bool { switch self { - case let .channel(statePeerId, _): - return statePeerId == peerId - /*case .group: - return true*/ - case let .scheduledMessages(statePeerId): - return statePeerId == peerId + case let .channel(statePeerId, _): + return statePeerId == peerId + case let .scheduledMessages(statePeerId): + return statePeerId == peerId + case let .quickReplyMessages(statePeerId, _): + return statePeerId == peerId } } } @@ -411,6 +393,35 @@ final class HistoryViewStateValidationContexts { })) } } + } else if view.namespaces.contains(Namespaces.Message.QuickReplyCloud) { + if let _ = self.contexts[id] { + } else if let location = location, case let .peer(peerId, threadId) = location { + guard let threadId else { + return + } + + let timestamp = self.network.context.globalTime() + if let previousTimestamp = self.previousPeerValidationTimestamps[peerId], timestamp < previousTimestamp + 60 { + } else { + self.previousPeerValidationTimestamps[peerId] = timestamp + + let context = HistoryStateValidationContext() + self.contexts[id] = context + + let disposable = MetaDisposable() + let batch = HistoryStateValidationBatch(disposable: disposable) + context.batch = batch + + let messages: [Message] = view.entries.map { $0.message }.filter { $0.id.namespace == Namespaces.Message.QuickReplyCloud } + + disposable.set((validateQuickReplyMessagesBatch(postbox: self.postbox, network: self.network, accountPeerId: peerId, tag: nil, messages: messages, historyState: .quickReplyMessages(peerId, Int32(clamping: threadId))) + |> deliverOn(self.queue)).start(completed: { [weak self] in + if let strongSelf = self, let context = strongSelf.contexts[id] { + context.batch = nil + } + })) + } + } } } } @@ -690,6 +701,53 @@ private func validateScheduledMessagesBatch(postbox: Postbox, network: Network, } |> switchToLatest } +private func validateQuickReplyMessagesBatch(postbox: Postbox, network: Network, accountPeerId: PeerId, tag: MessageTags?, messages: [Message], historyState: HistoryState) -> Signal { + return postbox.transaction { transaction -> Signal in + var signal: Signal + switch historyState { + case let .quickReplyMessages(peerId, shortcutId): + if let peer = transaction.getPeer(peerId) { + let hash = hashForScheduledMessages(messages) + signal = network.request(Api.functions.messages.getQuickReplyMessages(flags: 0, shortcutId: shortcutId, id: nil, hash: hash)) + |> map { result -> ValidatedMessages in + let messages: [Api.Message] + let chats: [Api.Chat] + let users: [Api.User] + + switch result { + case let .messages(messages: apiMessages, chats: apiChats, users: apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + case let .messagesSlice(_, _, _, _, messages: apiMessages, chats: apiChats, users: apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + case let .channelMessages(_, _, _, _, apiMessages, apiTopics, apiChats, apiUsers): + messages = apiMessages + let _ = apiTopics + chats = apiChats + users = apiUsers + case .messagesNotModified: + return .notModified + } + return .messages(peer, messages, chats, users, nil) + } + } else { + signal = .complete() + } + default: + signal = .complete() + } + var previous: [MessageId: Message] = [:] + for message in messages { + previous[message.id] = message + } + return validateBatch(postbox: postbox, network: network, transaction: transaction, accountPeerId: accountPeerId, tag: tag, historyState: historyState, signal: signal, previous: previous, messageNamespace: Namespaces.Message.QuickReplyCloud) + } + |> switchToLatest +} + private func validateBatch(postbox: Postbox, network: Network, transaction: Transaction, accountPeerId: PeerId, tag: MessageTags?, historyState: HistoryState, signal: Signal, previous: [MessageId: Message], messageNamespace: MessageId.Namespace) -> Signal { return signal |> map(Optional.init) diff --git a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift index 1e4572414b..18743c8450 100644 --- a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift @@ -127,10 +127,14 @@ func managedCloudChatRemoveMessagesOperations(postbox: Postbox, network: Network private func removeMessages(postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatRemoveMessagesOperation) -> Signal { var isScheduled = false + var isQuickReply = false for id in operation.messageIds { if id.namespace == Namespaces.Message.ScheduledCloud { isScheduled = true break + } else if id.namespace == Namespaces.Message.QuickReplyCloud { + isQuickReply = true + break } } @@ -160,6 +164,32 @@ private func removeMessages(postbox: Postbox, network: Network, stateManager: Ac } else { return .complete() } + } else if isQuickReply { + if let threadId = operation.threadId { + var signal: Signal = .complete() + for s in stride(from: 0, to: operation.messageIds.count, by: 100) { + let ids = Array(operation.messageIds[s ..< min(s + 100, operation.messageIds.count)]) + let partSignal = network.request(Api.functions.messages.deleteQuickReplyMessages(shortcutId: Int32(clamping: threadId), id: ids.map(\.id))) + |> map { result -> Api.Updates? in + return result + } + |> `catch` { _ in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + stateManager.addUpdates(updates) + } + return .complete() + } + + signal = signal + |> then(partSignal) + } + return signal + } else { + return .complete() + } } else if peer.id.namespace == Namespaces.Peer.CloudChannel { if let inputChannel = apiInputChannel(peer) { var signal: Signal = .complete() @@ -329,7 +359,7 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId?.id ?? Int32.max - 1, justClear: false, minTimestamp: nil, maxTimestamp: nil, type: operation.deleteGloballyIfPossible ? .forEveryone : .forLocalPeer) |> then(reportSignal) |> then(postbox.transaction { transaction -> Void in - _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, threadId: nil, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, threadId: nil, namespaces: .not(Namespaces.Message.allNonRegular)) }) } else { return .complete() @@ -386,6 +416,27 @@ private func requestClearHistory(postbox: Postbox, network: Network, stateManage private func _internal_clearHistory(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatClearHistoryOperation) -> Signal { if peer.id.namespace == Namespaces.Peer.CloudGroup || peer.id.namespace == Namespaces.Peer.CloudUser { + if case .quickReplyMessages = operation.type { + guard let threadId = operation.threadId else { + return .complete() + } + + let signal = network.request(Api.functions.messages.deleteQuickReplyShortcut(shortcutId: Int32(clamping: threadId))) + |> map { result -> Api.Bool? in + return result + } + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + return .fail(true) + } + return (signal |> restart) + |> `catch` { _ -> Signal in + return .complete() + } + } + if let inputPeer = apiInputPeer(peer) { if peer.id == stateManager.accountPeerId, let threadId = operation.threadId { guard let inputSubPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) else { @@ -407,7 +458,7 @@ private func _internal_clearHistory(transaction: Transaction, postbox: Postbox, return result } |> `catch` { _ -> Signal in - return .fail(true) + return .single(nil) } |> mapToSignal { result -> Signal in if let result = result { diff --git a/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift b/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift index f253215b50..ace5e77447 100644 --- a/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift +++ b/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift @@ -21,7 +21,7 @@ private func md5Hash(_ data: Data) -> Md5Hash { return Md5Hash(data: hashData) } -private func md5StringHash(_ string: String) -> UInt64 { +func md5StringHash(_ string: String) -> UInt64 { guard let data = string.data(using: .utf8) else { return 0 } diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index 85d6551e74..bed6a901b1 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -793,6 +793,7 @@ public final class PendingMessageManager { var replyToStoryId: StoryId? var scheduleTime: Int32? var sendAsPeerId: PeerId? + var quickReply: OutgoingQuickReplyMessageAttribute? var flags: Int32 = 0 @@ -821,6 +822,8 @@ public final class PendingMessageManager { hideCaptions = attribute.hideCaptions } else if let attribute = attribute as? SendAsMessageAttribute { sendAsPeerId = attribute.peerId + } else if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + quickReply = attribute } } @@ -871,6 +874,16 @@ public final class PendingMessageManager { topMsgId = Int32(clamping: threadId) } + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = messages[0].0.threadId, !"".isEmpty { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } + let forwardPeerIds = Set(forwardIds.map { $0.0.peerId }) if forwardPeerIds.count != 1 { assertionFailure() @@ -878,7 +891,7 @@ public final class PendingMessageManager { } else if let inputSourcePeerId = forwardPeerIds.first, let inputSourcePeer = transaction.getPeer(inputSourcePeerId).flatMap(apiInputPeer) { let dependencyTag = PendingMessageRequestDependencyTag(messageId: messages[0].0.id) - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut), tag: dependencyTag) } else { assertionFailure() sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "Invalid forward source")) @@ -993,7 +1006,17 @@ public final class PendingMessageManager { } } - sendMessageRequest = network.request(Api.functions.messages.sendMultiMedia(flags: flags, peer: inputPeer, replyTo: replyTo, multiMedia: singleMedias, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil)) + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = messages[0].0.threadId, !"".isEmpty { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } + + sendMessageRequest = network.request(Api.functions.messages.sendMultiMedia(flags: flags, peer: inputPeer, replyTo: replyTo, multiMedia: singleMedias, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut)) } return sendMessageRequest @@ -1168,6 +1191,7 @@ public final class PendingMessageManager { var scheduleTime: Int32? var sendAsPeerId: PeerId? var bubbleUpEmojiOrStickersets = false + var quickReply: OutgoingQuickReplyMessageAttribute? var flags: Int32 = 0 @@ -1202,6 +1226,8 @@ public final class PendingMessageManager { scheduleTime = attribute.scheduleTime } else if let attribute = attribute as? SendAsMessageAttribute { sendAsPeerId = attribute.peerId + } else if let attribute = attribute as? OutgoingQuickReplyMessageAttribute { + quickReply = attribute } } @@ -1291,7 +1317,17 @@ public final class PendingMessageManager { } } - sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: message.text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil), info: .acknowledgement, tag: dependencyTag) + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = message.threadId, !"".isEmpty { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } + + sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: message.text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut), info: .acknowledgement, tag: dependencyTag) case let .media(inputMedia, text): if bubbleUpEmojiOrStickersets { flags |= Int32(1 << 15) @@ -1355,8 +1391,18 @@ public final class PendingMessageManager { flags |= 1 << 16 } } + + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = message.threadId, !"".isEmpty { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } - sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut), tag: dependencyTag) |> map(NetworkRequestResult.result) case let .forward(sourceInfo): var topMsgId: Int32? @@ -1365,8 +1411,18 @@ public final class PendingMessageManager { topMsgId = Int32(clamping: threadId) } + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = message.threadId, !"".isEmpty { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } + if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) { - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut), tag: dependencyTag) |> map(NetworkRequestResult.result) } else { sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal")) @@ -1429,7 +1485,17 @@ public final class PendingMessageManager { } } - sendMessageRequest = network.request(Api.functions.messages.sendInlineBotResult(flags: flags, peer: inputPeer, replyTo: replyTo, randomId: uniqueId, queryId: chatContextResult.queryId, id: chatContextResult.id, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil)) + var quickReplyShortcut: Api.InputQuickReplyShortcut? + if let quickReply { + if let threadId = message.threadId, !"".isEmpty { + quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId)) + } else { + quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut) + } + flags |= 1 << 17 + } + + sendMessageRequest = network.request(Api.functions.messages.sendInlineBotResult(flags: flags, peer: inputPeer, replyTo: replyTo, randomId: uniqueId, queryId: chatContextResult.queryId, id: chatContextResult.id, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut)) |> map(NetworkRequestResult.result) case .messageScreenshot: let replyTo: Api.InputReplyTo @@ -1557,7 +1623,16 @@ public final class PendingMessageManager { private func applySentMessage(postbox: Postbox, stateManager: AccountStateManager, message: Message, content: PendingMessageUploadedContentAndReuploadInfo, result: Api.Updates) -> Signal { var apiMessage: Api.Message? for resultMessage in result.messages { - if let id = resultMessage.id(namespace: Namespaces.Message.allScheduled.contains(message.id.namespace) ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + let targetNamespace: MessageId.Namespace + if Namespaces.Message.allScheduled.contains(message.id.namespace) { + targetNamespace = Namespaces.Message.ScheduledCloud + } else if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + targetNamespace = Namespaces.Message.QuickReplyCloud + } else { + targetNamespace = Namespaces.Message.Cloud + } + + if let id = resultMessage.id(namespace: targetNamespace) { if id.peerId == message.id.peerId { apiMessage = resultMessage break @@ -1567,7 +1642,9 @@ public final class PendingMessageManager { let silent = message.muted var namespace = Namespaces.Message.Cloud - if let apiMessage = apiMessage, let id = apiMessage.id(namespace: message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + if message.id.namespace == Namespaces.Message.QuickReplyLocal { + namespace = Namespaces.Message.QuickReplyCloud + } else if let apiMessage = apiMessage, let id = apiMessage.id(namespace: message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { namespace = id.namespace if let attribute = message.attributes.first(where: { $0 is OutgoingMessageInfoAttribute }) as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId { @@ -1594,7 +1671,9 @@ public final class PendingMessageManager { private func applySentGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates) -> Signal { var silent = false var namespace = Namespaces.Message.Cloud - if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { + if let message = messages.first, message.id.namespace == Namespaces.Message.QuickReplyLocal { + namespace = Namespaces.Message.QuickReplyCloud + } else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { namespace = Namespaces.Message.ScheduledCloud if message.muted { silent = true @@ -1605,7 +1684,7 @@ public final class PendingMessageManager { for i in 0 ..< messages.count { let message = messages[i] let apiMessage = result.messages[i] - if let id = apiMessage.id(namespace: message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + if let id = apiMessage.id(namespace: namespace) { if let attribute = message.attributes.first(where: { $0 is OutgoingMessageInfoAttribute }) as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId { self.correlationIdToSentMessageId.with { value in value.mapping[correlationId] = id diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index a75e264534..a7fcc88093 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -271,6 +271,8 @@ extension Api.Update { return message case let .updateNewScheduledMessage(message): return message + case let .updateQuickReplyMessage(message): + return message default: return nil } @@ -334,6 +336,8 @@ extension Api.Update { return [peer.peerId] case let .updateNewScheduledMessage(message): return apiMessagePeerIds(message) + case let .updateQuickReplyMessage(message): + return apiMessagePeerIds(message) default: return [] } @@ -349,6 +353,8 @@ extension Api.Update { return apiMessageAssociatedMessageIds(message) case let .updateNewScheduledMessage(message): return apiMessageAssociatedMessageIds(message) + case let .updateQuickReplyMessage(message): + return apiMessageAssociatedMessageIds(message) default: break } diff --git a/submodules/TelegramCore/Sources/SyncCore/QuickReplyMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/QuickReplyMessageAttribute.swift new file mode 100644 index 0000000000..9df4ade399 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/QuickReplyMessageAttribute.swift @@ -0,0 +1,23 @@ +import Foundation +import Postbox +import TelegramApi + +public final class OutgoingQuickReplyMessageAttribute: Equatable, MessageAttribute { + public let shortcut: String + + public init(shortcut: String) { + self.shortcut = shortcut + } + + required public init(decoder: PostboxDecoder) { + self.shortcut = decoder.decodeStringForKey("s", orElse: "") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.shortcut, forKey: "s") + } + + public static func ==(lhs: OutgoingQuickReplyMessageAttribute, rhs: OutgoingQuickReplyMessageAttribute) -> Bool { + return true + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 402f4fa81d..49837a1318 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -429,8 +429,8 @@ extension TelegramBusinessLocation.Coordinates { extension TelegramBusinessLocation { convenience init(apiLocation: Api.BusinessLocation) { switch apiLocation { - case let .businessLocation(geoPoint, address): - self.init(address: address, coordinates: Coordinates(apiGeoPoint: geoPoint)) + case let .businessLocation(_, geoPoint, address): + self.init(address: address, coordinates: geoPoint.flatMap { Coordinates(apiGeoPoint: $0) }) } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift index 40d830bf1b..41e23ccdcf 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift @@ -24,21 +24,29 @@ public extension CloudChatRemoveMessagesType { public final class CloudChatRemoveMessagesOperation: PostboxCoding { public let messageIds: [MessageId] + public let threadId: Int64? public let type: CloudChatRemoveMessagesType - public init(messageIds: [MessageId], type: CloudChatRemoveMessagesType) { + public init(messageIds: [MessageId], threadId: Int64?, type: CloudChatRemoveMessagesType) { self.messageIds = messageIds + self.threadId = threadId self.type = type } public init(decoder: PostboxDecoder) { self.messageIds = MessageId.decodeArrayFromBuffer(decoder.decodeBytesForKeyNoCopy("i")!) + self.threadId = decoder.decodeOptionalInt64ForKey("threadId") self.type = CloudChatRemoveMessagesType(rawValue: decoder.decodeInt32ForKey("t", orElse: 0))! } public func encode(_ encoder: PostboxEncoder) { let buffer = WriteBuffer() MessageId.encodeArrayToBuffer(self.messageIds, buffer: buffer) + if let threadId = self.threadId { + encoder.encodeInt64(threadId, forKey: "threadId") + } else { + encoder.encodeNil(forKey: "threadId") + } encoder.encodeBytes(buffer, forKey: "i") encoder.encodeInt32(self.type.rawValue, forKey: "t") } @@ -88,6 +96,7 @@ public enum CloudChatClearHistoryType: Int32 { case forLocalPeer case forEveryone case scheduledMessages + case quickReplyMessages } public enum InteractiveHistoryClearingType: Int32 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift index 66f409e0fd..57a8473844 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift @@ -7,7 +7,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(peer, _, _, _, _, _): + case let .message(peer, _, _, _, _, _, _): return peer } } @@ -15,7 +15,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(_, author, _, _, _, _): + case let .message(_, author, _, _, _, _, _): return author } } @@ -24,7 +24,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(_, _, id, _, _, _): + case let .message(_, _, id, _, _, _, _): return id } } @@ -33,7 +33,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(_, _, _, timestamp, _, _): + case let .message(_, _, _, timestamp, _, _, _): return timestamp } } @@ -42,7 +42,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(_, _, _, _, incoming, _): + case let .message(_, _, _, _, incoming, _, _): return incoming } } @@ -51,11 +51,20 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { switch content { case .none: return nil - case let .message(_, _, _, _, _, secret): + case let .message(_, _, _, _, _, secret, _): return secret } } + public var threadId: Int64? { + switch content { + case .none: + return nil + case let .message(_, _, _, _, _, _, threadId): + return threadId + } + } + public init(_ message: Message) { if message.id.namespace != Namespaces.Message.Local, let peer = message.peers[message.id.peerId], let inputPeer = PeerReference(peer) { let author: PeerReference? @@ -64,13 +73,13 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { } else { author = nil } - self.content = .message(peer: inputPeer, author: author, id: message.id, timestamp: message.timestamp, incoming: message.flags.contains(.Incoming), secret: message.containsSecretMedia) + self.content = .message(peer: inputPeer, author: author, id: message.id, timestamp: message.timestamp, incoming: message.flags.contains(.Incoming), secret: message.containsSecretMedia, threadId: message.threadId) } else { self.content = .none } } - public init(peer: Peer, author: Peer?, id: MessageId, timestamp: Int32, incoming: Bool, secret: Bool) { + public init(peer: Peer, author: Peer?, id: MessageId, timestamp: Int32, incoming: Bool, secret: Bool, threadId: Int64?) { if let inputPeer = PeerReference(peer) { let a: PeerReference? if let peer = author { @@ -78,7 +87,7 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { } else { a = nil } - self.content = .message(peer: inputPeer, author: a, id: id, timestamp: timestamp, incoming: incoming, secret: secret) + self.content = .message(peer: inputPeer, author: a, id: id, timestamp: timestamp, incoming: incoming, secret: secret, threadId: threadId) } else { self.content = .none } @@ -95,14 +104,14 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { public enum MessageReferenceContent: PostboxCoding, Hashable, Equatable { case none - case message(peer: PeerReference, author: PeerReference?, id: MessageId, timestamp: Int32, incoming: Bool, secret: Bool) + case message(peer: PeerReference, author: PeerReference?, id: MessageId, timestamp: Int32, incoming: Bool, secret: Bool, threadId: Int64?) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("_r", orElse: 0) { case 0: self = .none case 1: - self = .message(peer: decoder.decodeObjectForKey("p", decoder: { PeerReference(decoder: $0) }) as! PeerReference, author: decoder.decodeObjectForKey("author") as? PeerReference, id: MessageId(peerId: PeerId(decoder.decodeInt64ForKey("i.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("i.n", orElse: 0), id: decoder.decodeInt32ForKey("i.i", orElse: 0)), timestamp: 0, incoming: false, secret: false) + self = .message(peer: decoder.decodeObjectForKey("p", decoder: { PeerReference(decoder: $0) }) as! PeerReference, author: decoder.decodeObjectForKey("author") as? PeerReference, id: MessageId(peerId: PeerId(decoder.decodeInt64ForKey("i.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("i.n", orElse: 0), id: decoder.decodeInt32ForKey("i.i", orElse: 0)), timestamp: 0, incoming: false, secret: false, threadId: decoder.decodeOptionalInt64ForKey("tid")) default: assertionFailure() self = .none @@ -113,7 +122,7 @@ public enum MessageReferenceContent: PostboxCoding, Hashable, Equatable { switch self { case .none: encoder.encodeInt32(0, forKey: "_r") - case let .message(peer, author, id, _, _, _): + case let .message(peer, author, id, _, _, _, threadId): encoder.encodeInt32(1, forKey: "_r") encoder.encodeObject(peer, forKey: "p") if let author = author { @@ -124,6 +133,11 @@ public enum MessageReferenceContent: PostboxCoding, Hashable, Equatable { encoder.encodeInt64(id.peerId.toInt64(), forKey: "i.p") encoder.encodeInt32(id.namespace, forKey: "i.n") encoder.encodeInt32(id.id, forKey: "i.i") + if let threadId { + encoder.encodeInt64(threadId, forKey: "tid") + } else { + encoder.encodeNil(forKey: "tid") + } } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index a483fd4329..70d9a83496 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -8,8 +8,12 @@ public struct Namespaces { public static let SecretIncoming: Int32 = 2 public static let ScheduledCloud: Int32 = 3 public static let ScheduledLocal: Int32 = 4 + public static let QuickReplyCloud: Int32 = 5 + public static let QuickReplyLocal: Int32 = 6 public static let allScheduled: Set = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal]) + public static let allQuickReply: Set = Set([Namespaces.Message.QuickReplyCloud, Namespaces.Message.QuickReplyLocal]) + public static let allNonRegular: Set = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal, Namespaces.Message.QuickReplyCloud, Namespaces.Message.QuickReplyLocal]) } public struct Media { @@ -280,7 +284,7 @@ private enum PreferencesKeyValues: Int32 { case audioTranscriptionTrialState = 33 case didCacheSavedMessageTagsPrefix = 34 case displaySavedChatsAsTopics = 35 - case shortcutMessages = 36 + case shortcutMessages = 37 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index db2a419409..c2986ca224 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -138,8 +138,12 @@ public extension TelegramEngine { var inputAddress: String? if let businessLocation { flags |= 1 << 0 - inputGeoPoint = businessLocation.coordinates?.apiInputGeoPoint ?? .inputGeoPointEmpty inputAddress = businessLocation.address + + inputGeoPoint = businessLocation.coordinates?.apiInputGeoPoint + if inputGeoPoint != nil { + flags |= 1 << 1 + } } let remoteApply: Signal = self.account.network.request(Api.functions.account.updateBusinessLocation(flags: flags, geoPoint: inputGeoPoint, address: inputAddress)) @@ -158,12 +162,28 @@ public extension TelegramEngine { |> then(remoteApply) } - public func shortcutMessages() -> Signal { - return _internal_shortcutMessages(account: self.account) + public func shortcutMessageList() -> Signal { + return _internal_shortcutMessageList(account: self.account) } - public func updateShortcutMessages(state: QuickReplyMessageShortcutsState) { - let _ = _internal_updateShortcutMessages(account: self.account, state: state).startStandalone() + public func keepShortcutMessageListUpdated() -> Signal { + return _internal_keepShortcutMessagesUpdated(account: self.account) + } + + public func editMessageShortcut(id: Int32, shortcut: String) { + let _ = _internal_editMessageShortcut(account: self.account, id: id, shortcut: shortcut).startStandalone() + } + + public func deleteMessageShortcuts(ids: [Int32]) { + let _ = _internal_deleteMessageShortcuts(account: self.account, ids: ids).startStandalone() + } + + public func reorderMessageShortcuts(ids: [Int32], completion: @escaping () -> Void) { + let _ = _internal_reorderMessageShortcuts(account: self.account, ids: ids, localCompletion: completion).startStandalone() + } + + public func sendMessageShortcut(peerId: EnginePeer.Id, id: Int32) { + let _ = _internal_sendMessageShortcut(account: self.account, peerId: peerId, id: id).startStandalone() } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift index 12e0fa696a..6fb62b8142 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift @@ -12,35 +12,51 @@ func _internal_deleteMessagesInteractively(account: Account, messageIds: [Messag } func deleteMessagesInteractively(transaction: Transaction, stateManager: AccountStateManager?, postbox: Postbox, messageIds initialMessageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false, removeIfPossiblyDelivered: Bool) { - var messageIds: [MessageId] = [] + var messageIds: [MessageAndThreadId] = [] if deleteAllInGroup { + var tempIds: [MessageId] = initialMessageIds for id in initialMessageIds { if let group = transaction.getMessageGroup(id) ?? transaction.getMessageForwardedGroup(id) { for message in group { - if !messageIds.contains(message.id) { - messageIds.append(message.id) + if !tempIds.contains(message.id) { + tempIds.append(message.id) } } } else { - messageIds.append(id) + tempIds.append(id) + } + } + + messageIds = tempIds.map { id in + if id.namespace == Namespaces.Message.QuickReplyCloud { + if let message = transaction.getMessage(id) { + return MessageAndThreadId(messageId: id, threadId: message.threadId) + } else { + return MessageAndThreadId(messageId: id, threadId: nil) + } + } else { + return MessageAndThreadId(messageId: id, threadId: nil) } } } else { - messageIds = initialMessageIds - } - - var messageIdsByPeerId: [PeerId: [MessageId]] = [:] - for id in messageIds { - if messageIdsByPeerId[id.peerId] == nil { - messageIdsByPeerId[id.peerId] = [id] - } else { - messageIdsByPeerId[id.peerId]!.append(id) + messageIds = initialMessageIds.map { id in + if id.namespace == Namespaces.Message.QuickReplyCloud { + if let message = transaction.getMessage(id) { + return MessageAndThreadId(messageId: id, threadId: message.threadId) + } else { + return MessageAndThreadId(messageId: id, threadId: nil) + } + } else { + return MessageAndThreadId(messageId: id, threadId: nil) + } } } var uniqueIds: [Int64: PeerId] = [:] - for (peerId, peerMessageIds) in messageIdsByPeerId { + for (peerAndThreadId, peerMessageIds) in messagesIdsGroupedByPeerId(messageIds) { + let peerId = peerAndThreadId.peerId + let threadId = peerAndThreadId.threadId for id in peerMessageIds { if let message = transaction.getMessage(id) { for attribute in message.attributes { @@ -53,13 +69,13 @@ func deleteMessagesInteractively(transaction: Transaction, stateManager: Account if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudUser { let remoteMessageIds = peerMessageIds.filter { id in - if id.namespace == Namespaces.Message.Local || id.namespace == Namespaces.Message.ScheduledLocal { + if id.namespace == Namespaces.Message.Local || id.namespace == Namespaces.Message.ScheduledLocal || id.namespace == Namespaces.Message.QuickReplyLocal { return false } return true } if !remoteMessageIds.isEmpty { - cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, messageIds: remoteMessageIds, type: CloudChatRemoveMessagesType(type)) + cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, threadId: threadId, messageIds: remoteMessageIds, type: CloudChatRemoveMessagesType(type)) } } else if peerId.namespace == Namespaces.Peer.SecretChat { if let state = transaction.getPeerChatState(peerId) as? SecretChatState { @@ -87,9 +103,9 @@ func deleteMessagesInteractively(transaction: Transaction, stateManager: Account } } } - _internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: messageIds) + _internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: messageIds.map(\.messageId)) - stateManager?.notifyDeletedMessages(messageIds: messageIds) + stateManager?.notifyDeletedMessages(messageIds: messageIds.map(\.messageId)) if !uniqueIds.isEmpty && removeIfPossiblyDelivered { stateManager?.removePossiblyDeliveredMessages(uniqueIds: uniqueIds) @@ -102,7 +118,7 @@ func _internal_clearHistoryInRangeInteractively(postbox: Postbox, peerId: PeerId cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, threadId: threadId, explicitTopMessageId: nil, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: CloudChatClearHistoryType(type)) if type == .scheduledMessages { } else { - _internal_clearHistoryInRange(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistoryInRange(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: .not(Namespaces.Message.allNonRegular)) } } else if peerId.namespace == Namespaces.Peer.SecretChat { /*_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .all) @@ -141,7 +157,7 @@ func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, threa topIndex = topMessage.index } - _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, threadId: threadId, namespaces: .not(Namespaces.Message.allNonRegular)) if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, let migrationReference = cachedData.migrationReference { cloudChatAddClearHistoryOperation(transaction: transaction, peerId: migrationReference.maxMessageId.peerId, threadId: threadId, explicitTopMessageId: MessageId(peerId: migrationReference.maxMessageId.peerId, namespace: migrationReference.maxMessageId.namespace, id: migrationReference.maxMessageId.id + 1), minTimestamp: nil, maxTimestamp: nil, type: CloudChatClearHistoryType(type)) _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: migrationReference.maxMessageId.peerId, threadId: threadId, namespaces: .all) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index 34dde0d272..e8d6cbab5e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -5,89 +5,12 @@ import TelegramApi import MtProtoKit public final class QuickReplyMessageShortcut: Codable, Equatable { - private final class CodableMessage: Codable { - let message: Message - - init(message: Message) { - self.message = message - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: StringCodingKey.self) - - var media: [Media] = [] - if let mediaData = try container.decodeIfPresent(Data.self, forKey: "media") { - if let value = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as? Media { - media.append(value) - } - } - - var attributes: [MessageAttribute] = [] - if let attributesData = try container.decodeIfPresent([Data].self, forKey: "attributes") { - for attribute in attributesData { - if let value = PostboxDecoder(buffer: MemoryBuffer(data: attribute)).decodeRootObject() as? MessageAttribute { - attributes.append(value) - } - } - } - - self.message = Message( - stableId: 0, - stableVersion: 0, - id: MessageId(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0)), namespace: 0, id: 0), - globallyUniqueId: nil, - groupingKey: nil, - groupInfo: nil, - threadId: nil, - timestamp: 0, - flags: [], - tags: [], - globalTags: [], - localTags: [], - customTags: [], - forwardInfo: nil, - author: nil, - text: try container.decode(String.self, forKey: "text"), - attributes: attributes, - media: media, - peers: SimpleDictionary(), - associatedMessages: SimpleDictionary(), - associatedMessageIds: [], - associatedMedia: [:], - associatedThreadInfo: nil, - associatedStories: [:] - ) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: StringCodingKey.self) - - if let media = self.message.media.first { - let mediaEncoder = PostboxEncoder() - mediaEncoder.encodeRootObject(media) - try container.encode(mediaEncoder.makeData(), forKey: "media") - } - - var attributesData: [Data] = [] - for attribute in self.message.attributes { - let attributeEncoder = PostboxEncoder() - attributeEncoder.encodeRootObject(attribute) - attributesData.append(attributeEncoder.makeData()) - } - try container.encode(attributesData, forKey: "attributes") - - try container.encode(self.message.text, forKey: "text") - } - } - public let id: Int32 public let shortcut: String - public let messages: [EngineMessage] - public init(id: Int32, shortcut: String, messages: [EngineMessage]) { + public init(id: Int32, shortcut: String) { self.id = id self.shortcut = shortcut - self.messages = messages } public static func ==(lhs: QuickReplyMessageShortcut, rhs: QuickReplyMessageShortcut) -> Bool { @@ -97,9 +20,6 @@ public final class QuickReplyMessageShortcut: Codable, Equatable { if lhs.shortcut != rhs.shortcut { return false } - if lhs.messages != rhs.messages { - return false - } return true } @@ -108,7 +28,6 @@ public final class QuickReplyMessageShortcut: Codable, Equatable { self.id = try container.decode(Int32.self, forKey: "id") self.shortcut = try container.decode(String.self, forKey: "shortcut") - self.messages = try container.decode([CodableMessage].self, forKey: "messages").map { EngineMessage($0.message) } } public func encode(to encoder: Encoder) throws { @@ -116,42 +35,330 @@ public final class QuickReplyMessageShortcut: Codable, Equatable { try container.encode(self.id, forKey: "id") try container.encode(self.shortcut, forKey: "shortcut") - try container.encode(self.messages.map { CodableMessage(message: $0._asMessage()) }, forKey: "messages") } } -public final class QuickReplyMessageShortcutsState: Codable, Equatable { - public let shortcuts: [QuickReplyMessageShortcut] +struct QuickReplyMessageShortcutsState: Codable, Equatable { + var shortcuts: [QuickReplyMessageShortcut] - public init(shortcuts: [QuickReplyMessageShortcut]) { + init(shortcuts: [QuickReplyMessageShortcut]) { self.shortcuts = shortcuts } +} + +public final class ShortcutMessageList: Equatable { + public final class Item: Equatable { + public let id: Int32 + public let shortcut: String + public let topMessage: EngineMessage + public let totalCount: Int + + public init(id: Int32, shortcut: String, topMessage: EngineMessage, totalCount: Int) { + self.id = id + self.shortcut = shortcut + self.topMessage = topMessage + self.totalCount = totalCount + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if lhs.id != rhs.id { + return false + } + if lhs.shortcut != rhs.shortcut { + return false + } + if lhs.topMessage != rhs.topMessage { + return false + } + if lhs.totalCount != rhs.totalCount { + return false + } + return true + } + } - public static func ==(lhs: QuickReplyMessageShortcutsState, rhs: QuickReplyMessageShortcutsState) -> Bool { - if lhs.shortcuts != rhs.shortcuts { + public let items: [Item] + public let isLoading: Bool + + public init(items: [Item], isLoading: Bool) { + self.items = items + self.isLoading = isLoading + } + + public static func ==(lhs: ShortcutMessageList, rhs: ShortcutMessageList) -> Bool { + if lhs === rhs { + return true + } + if lhs.items != rhs.items { + return false + } + if lhs.isLoading != rhs.isLoading { return false } return true } } -func _internal_shortcutMessages(account: Account) -> Signal { +func _internal_quickReplyMessageShortcutsState(account: Account) -> Signal { let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.shortcutMessages()])) return account.postbox.combinedView(keys: [viewKey]) - |> map { views -> QuickReplyMessageShortcutsState in + |> map { views -> QuickReplyMessageShortcutsState? in guard let view = views.views[viewKey] as? PreferencesView else { - return QuickReplyMessageShortcutsState(shortcuts: []) + return nil } guard let value = view.values[PreferencesKeys.shortcutMessages()]?.get(QuickReplyMessageShortcutsState.self) else { - return QuickReplyMessageShortcutsState(shortcuts: []) + return nil } return value } } -func _internal_updateShortcutMessages(account: Account, state: QuickReplyMessageShortcutsState) -> Signal { - return account.postbox.transaction { transaction -> Void in +func _internal_keepShortcutMessagesUpdated(account: Account) -> Signal { + let updateSignal = _internal_shortcutMessageList(account: account) + |> take(1) + |> mapToSignal { list -> Signal in + var acc: UInt64 = 0 + for item in list.items { + combineInt64Hash(&acc, with: UInt64(item.id)) + combineInt64Hash(&acc, with: md5StringHash(item.shortcut)) + combineInt64Hash(&acc, with: UInt64(item.topMessage.id.id)) + + var editTimestamp: Int32 = 0 + inner: for attribute in item.topMessage.attributes { + if let attribute = attribute as? EditedMessageAttribute { + editTimestamp = attribute.date + break inner + } + } + combineInt64Hash(&acc, with: UInt64(editTimestamp)) + } + + return account.network.request(Api.functions.messages.getQuickReplies(hash: finalizeInt64Hash(acc))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + + return account.postbox.transaction { transaction in + var state = transaction.getPreferencesEntry(key: PreferencesKeys.shortcutMessages())?.get(QuickReplyMessageShortcutsState.self) ?? QuickReplyMessageShortcutsState(shortcuts: []) + switch result { + case let .quickReplies(quickReplies, messages, chats, users): + let previousShortcuts = state.shortcuts + state.shortcuts.removeAll() + + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + + var storeMessages: [StoreMessage] = [] + + for message in messages { + if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) { + storeMessages.append(message) + } + } + let _ = transaction.addMessages(storeMessages, location: .Random) + var topMessageIds: [Int32: Int32] = [:] + + for quickReply in quickReplies { + switch quickReply { + case let .quickReply(shortcutId, shortcut, topMessage, _): + state.shortcuts.append(QuickReplyMessageShortcut( + id: shortcutId, + shortcut: shortcut + )) + topMessageIds[shortcutId] = topMessage + } + } + + if previousShortcuts != state.shortcuts { + for shortcut in previousShortcuts { + if let topMessageId = topMessageIds[shortcut.id] { + //TODO:remove earlier + let _ = topMessageId + } else { + let existingCloudMessages = transaction.getMessagesWithThreadId(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyCloud, threadId: Int64(shortcut.id), from: MessageIndex.lowerBound(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyCloud), includeFrom: false, to: MessageIndex.upperBound(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyCloud), limit: 1000) + let existingLocalMessages = transaction.getMessagesWithThreadId(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyLocal, threadId: Int64(shortcut.id), from: MessageIndex.lowerBound(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyLocal), includeFrom: false, to: MessageIndex.upperBound(peerId: account.peerId, namespace: Namespaces.Message.QuickReplyLocal), limit: 1000) + + transaction.deleteMessages(existingCloudMessages.map(\.id), forEachMedia: nil) + transaction.deleteMessages(existingLocalMessages.map(\.id), forEachMedia: nil) + } + } + } + case .quickRepliesNotModified: + break + } + + transaction.setPreferencesEntry(key: PreferencesKeys.shortcutMessages(), value: PreferencesEntry(state)) + } + |> ignoreValues + } + } + + return updateSignal +} + +func _internal_shortcutMessageList(account: Account) -> Signal { + return _internal_quickReplyMessageShortcutsState(account: account) + |> distinctUntilChanged + |> mapToSignal { state -> Signal in + guard let state else { + return .single(ShortcutMessageList(items: [], isLoading: true)) + } + + var keys: [PostboxViewKey] = [] + var historyViewKeys: [Int32: PostboxViewKey] = [:] + var summaryKeys: [Int32: PostboxViewKey] = [:] + for shortcut in state.shortcuts { + let historyViewKey: PostboxViewKey = .historyView(PostboxViewKey.HistoryView( + peerId: account.peerId, + threadId: Int64(shortcut.id), + clipHoles: false, + trackHoles: false, + anchor: .lowerBound, + appendMessagesFromTheSameGroup: false, + namespaces: .just(Set([Namespaces.Message.QuickReplyCloud])), + count: 10 + )) + historyViewKeys[shortcut.id] = historyViewKey + keys.append(historyViewKey) + + let summaryKey: PostboxViewKey = .historyTagSummaryView(tag: [], peerId: account.peerId, threadId: Int64(shortcut.id), namespace: Namespaces.Message.ScheduledCloud, customTag: nil) + summaryKeys[shortcut.id] = summaryKey + keys.append(summaryKey) + } + return account.postbox.combinedView( + keys: keys + ) + |> map { views -> ShortcutMessageList in + var items: [ShortcutMessageList.Item] = [] + for shortcut in state.shortcuts { + guard let historyViewKey = historyViewKeys[shortcut.id], let historyView = views.views[historyViewKey] as? MessageHistoryView else { + continue + } + + var totalCount = 1 + if let summaryKey = summaryKeys[shortcut.id], let summaryView = views.views[summaryKey] as? MessageHistoryTagSummaryView { + if let count = summaryView.count { + totalCount = max(1, Int(count)) + } + } + + if let entry = historyView.entries.first { + items.append(ShortcutMessageList.Item(id: shortcut.id, shortcut: shortcut.shortcut, topMessage: EngineMessage(entry.message), totalCount: totalCount)) + } + } + return ShortcutMessageList(items: items, isLoading: false) + } + |> distinctUntilChanged + } +} + +func _internal_editMessageShortcut(account: Account, id: Int32, shortcut: String) -> Signal { + let remoteApply = account.network.request(Api.functions.messages.editQuickReplyShortcut(shortcutId: id, shortcut: shortcut)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + + return account.postbox.transaction { transaction in + var state = transaction.getPreferencesEntry(key: PreferencesKeys.shortcutMessages())?.get(QuickReplyMessageShortcutsState.self) ?? QuickReplyMessageShortcutsState(shortcuts: []) + if let index = state.shortcuts.firstIndex(where: { $0.id == id }) { + state.shortcuts[index] = QuickReplyMessageShortcut(id: id, shortcut: shortcut) + } transaction.setPreferencesEntry(key: PreferencesKeys.shortcutMessages(), value: PreferencesEntry(state)) } |> ignoreValues + |> then(remoteApply) +} + +func _internal_deleteMessageShortcuts(account: Account, ids: [Int32]) -> Signal { + return account.postbox.transaction { transaction in + var state = transaction.getPreferencesEntry(key: PreferencesKeys.shortcutMessages())?.get(QuickReplyMessageShortcutsState.self) ?? QuickReplyMessageShortcutsState(shortcuts: []) + + for id in ids { + if let index = state.shortcuts.firstIndex(where: { $0.id == id }) { + state.shortcuts.remove(at: index) + } + } + transaction.setPreferencesEntry(key: PreferencesKeys.shortcutMessages(), value: PreferencesEntry(state)) + + for id in ids { + cloudChatAddClearHistoryOperation(transaction: transaction, peerId: account.peerId, threadId: Int64(id), explicitTopMessageId: nil, minTimestamp: nil, maxTimestamp: nil, type: .quickReplyMessages) + } + } + |> ignoreValues +} + +func _internal_reorderMessageShortcuts(account: Account, ids: [Int32], localCompletion: @escaping () -> Void) -> Signal { + let remoteApply = account.network.request(Api.functions.messages.reorderQuickReplies(order: ids)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + + return account.postbox.transaction { transaction in + var state = transaction.getPreferencesEntry(key: PreferencesKeys.shortcutMessages())?.get(QuickReplyMessageShortcutsState.self) ?? QuickReplyMessageShortcutsState(shortcuts: []) + + let previousShortcuts = state.shortcuts + state.shortcuts.removeAll() + for id in ids { + if let index = previousShortcuts.firstIndex(where: { $0.id == id }) { + state.shortcuts.append(previousShortcuts[index]) + } + } + for shortcut in previousShortcuts { + if !state.shortcuts.contains(where: { $0.id == shortcut.id }) { + state.shortcuts.append(shortcut) + } + } + + transaction.setPreferencesEntry(key: PreferencesKeys.shortcutMessages(), value: PreferencesEntry(state)) + } + |> ignoreValues + |> afterCompleted { + localCompletion() + } + |> then(remoteApply) +} + +func _internal_sendMessageShortcut(account: Account, peerId: PeerId, id: Int32) -> Signal { + return account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> mapToSignal { peer -> Signal in + guard let peer, let inputPeer = apiInputPeer(peer) else { + return .complete() + } + return account.network.request(Api.functions.messages.sendQuickReplyMessages(peer: inputPeer, shortcutId: id)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + if let result { + account.stateManager.addUpdates(result) + } + return .complete() + } + } +} + +func _internal_applySentQuickReplyMessage(transaction: Transaction, shortcut: String, quickReplyId: Int32) { + var state = transaction.getPreferencesEntry(key: PreferencesKeys.shortcutMessages())?.get(QuickReplyMessageShortcutsState.self) ?? QuickReplyMessageShortcutsState(shortcuts: []) + + if !state.shortcuts.contains(where: { $0.id == quickReplyId }) { + state.shortcuts.insert(QuickReplyMessageShortcut(id: quickReplyId, shortcut: shortcut), at: 0) + transaction.setPreferencesEntry(key: PreferencesKeys.shortcutMessages(), value: PreferencesEntry(state)) + } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift index 0695e05c0c..4c57ab1f13 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift @@ -843,7 +843,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa count: 40, clipHoles: true, anchor: inputAnchor, - namespaces: .not(Namespaces.Message.allScheduled) + namespaces: .not(Namespaces.Message.allNonRegular) ) if !testView.isLoading || transaction.getMessageHistoryThreadInfo(peerId: threadMessageId.peerId, threadId: Int64(threadMessageId.id)) != nil { let initialAnchor: ChatReplyThreadMessage.Anchor diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index 409791f28d..d64c77acc5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -584,12 +584,18 @@ func _internal_downloadMessage(accountPeerId: PeerId, postbox: Postbox, network: } func fetchRemoteMessage(accountPeerId: PeerId, postbox: Postbox, source: FetchMessageHistoryHoleSource, message: MessageReference) -> Signal { - guard case let .message(peer, _, id, _, _, _) = message.content else { + guard case let .message(peer, _, id, _, _, _, threadId) = message.content else { return .single(nil) } let signal: Signal if id.namespace == Namespaces.Message.ScheduledCloud { signal = source.request(Api.functions.messages.getScheduledMessages(peer: peer.inputPeer, id: [id.id])) + } else if id.namespace == Namespaces.Message.QuickReplyCloud { + if let threadId { + signal = source.request(Api.functions.messages.getQuickReplyMessages(flags: 1 << 0, shortcutId: Int32(clamping: threadId), id: [id.id], hash: 0)) + } else { + signal = .never() + } } else if id.peerId.namespace == Namespaces.Peer.CloudChannel { if let channel = peer.inputChannel { signal = source.request(Api.functions.channels.getMessages(channel: channel, id: [Api.InputMessage.inputMessageID(id: id.id)])) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift index 5f39622ea4..f6f258c154 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift @@ -192,7 +192,7 @@ public final class SparseMessageList { let location: ChatLocationInput = .peer(peerId: self.peerId, threadId: self.threadId) - self.topItemsDisposable.set((self.account.postbox.aroundMessageHistoryViewForLocation(location, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: count, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: .tag(self.messageTag), appendMessagesFromTheSameGroup: false, namespaces: .not(Set(Namespaces.Message.allScheduled)), orderStatistics: []) + self.topItemsDisposable.set((self.account.postbox.aroundMessageHistoryViewForLocation(location, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: count, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: .tag(self.messageTag), appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) |> deliverOn(self.queue)).start(next: { [weak self] view, updateType, _ in guard let strongSelf = self else { return diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 0d1e3ea5d7..5f625b1e91 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1082,7 +1082,7 @@ public extension TelegramEngine { transaction.setMessageHistoryThreadInfo(peerId: id, threadId: threadId, info: nil) - _internal_clearHistory(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: id, threadId: threadId, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: id, threadId: threadId, namespaces: .not(Namespaces.Message.allNonRegular)) } |> ignoreValues } @@ -1094,7 +1094,7 @@ public extension TelegramEngine { transaction.setMessageHistoryThreadInfo(peerId: id, threadId: threadId, info: nil) - _internal_clearHistory(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: id, threadId: threadId, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: id, threadId: threadId, namespaces: .not(Namespaces.Message.allNonRegular)) } } |> ignoreValues @@ -1373,7 +1373,7 @@ public extension TelegramEngine { return .single(false) } - return self.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: id, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 44, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: []) + return self.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: id, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 44, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) |> map { view -> Bool in for entry in view.0.entries { if entry.message.flags.contains(.Incoming) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index eced71f7af..0cd5c6b4a1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -216,7 +216,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let _ = accountUser switch fullUser { - case let .userFull(_, _, _, _, _, _, _, _, userFullNotifySettings, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .userFull(_, _, _, _, _, _, _, _, userFullNotifySettings, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: userFullNotifySettings)]) } @@ -228,7 +228,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): + case let .userFull(userFullFlags, _, _, userFullAbout, userFullSettings, personalPhoto, profilePhoto, fallbackPhoto, _, userFullBotInfo, userFullPinnedMsgId, userFullCommonChatsCount, _, userFullTtlPeriod, userFullThemeEmoticon, _, _, _, userPremiumGiftOptions, userWallpaper, stories, businessWorkHours, businessLocation, _, _): let _ = stories let botInfo = userFullBotInfo.flatMap(BotInfo.init(apiBotInfo:)) let isBlocked = (userFullFlags & (1 << 0)) != 0 diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 22173d47cb..53f630c40d 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -181,6 +181,36 @@ func messagesIdsGroupedByPeerId(_ ids: ReferencedReplyMessageIds) -> [PeerId: Re return dict } +func messagesIdsGroupedByPeerId(_ ids: Set) -> [PeerAndThreadId: [MessageId]] { + var dict: [PeerAndThreadId: [MessageId]] = [:] + + for id in ids { + let peerAndThreadId = PeerAndThreadId(peerId: id.messageId.peerId, threadId: id.threadId) + if dict[peerAndThreadId] == nil { + dict[peerAndThreadId] = [id.messageId] + } else { + dict[peerAndThreadId]!.append(id.messageId) + } + } + + return dict +} + +func messagesIdsGroupedByPeerId(_ ids: [MessageAndThreadId]) -> [PeerAndThreadId: [MessageId]] { + var dict: [PeerAndThreadId: [MessageId]] = [:] + + for id in ids { + let peerAndThreadId = PeerAndThreadId(peerId: id.messageId.peerId, threadId: id.threadId) + if dict[peerAndThreadId] == nil { + dict[peerAndThreadId] = [id.messageId] + } else { + dict[peerAndThreadId]!.append(id.messageId) + } + } + + return dict +} + func locallyRenderedMessage(message: StoreMessage, peers: [PeerId: Peer], associatedThreadInfo: Message.AssociatedThreadInfo? = nil) -> Message? { guard case let .Id(id) = message.id else { return nil diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 29434ba731..d8859b915f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -434,10 +434,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { override public func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { super.setupItem(item, synchronousLoad: synchronousLoad) - if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal { + if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal || item.message.id.namespace == Namespaces.Message.QuickReplyLocal { self.wasPending = true } - if self.wasPending && (item.message.id.namespace != Namespaces.Message.Local && item.message.id.namespace != Namespaces.Message.ScheduledLocal) { + if self.wasPending && (item.message.id.namespace != Namespaces.Message.Local && item.message.id.namespace != Namespaces.Message.ScheduledLocal && item.message.id.namespace != Namespaces.Message.QuickReplyLocal) { self.didChangeFromPendingToSent = true } @@ -857,7 +857,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var needsShareButton = false if case .pinnedMessages = item.associatedData.subject { needsShareButton = true - } else if isFailed || Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + } else if isFailed || Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { needsShareButton = false } else if item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { for attribute in item.content.firstMessage.attributes { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 6c49dba131..16c144e196 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -315,7 +315,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ needReactions = false } - if !isAction && !hasSeparateCommentsButton && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) && !hideAllAdditionalInfo { + if !isAction && !hasSeparateCommentsButton && !Namespaces.Message.allNonRegular.contains(firstMessage.id.namespace) && !hideAllAdditionalInfo { if hasCommentButton(item: item) { result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .footer, neighborSpacing: .default))) } @@ -1485,7 +1485,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } else if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id { needsShareButton = false allowFullWidth = true - } else if isFailed || Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + } else if isFailed || Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { needsShareButton = false } else if item.message.id.peerId == item.context.account.peerId { if let _ = sourceReference { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index b4e50791ab..c1ba69fe70 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -99,7 +99,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco self.interactiveVideoNode.shouldOpen = { [weak self] in if let strongSelf = self { - if let item = strongSelf.item, (item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal) { + if let item = strongSelf.item, (item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal || item.message.id.namespace == Namespaces.Message.QuickReplyLocal) { return false } return !strongSelf.animatingHeight @@ -341,7 +341,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco var needsShareButton = false if case .pinnedMessages = item.associatedData.subject { needsShareButton = true - } else if isFailed || Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + } else if isFailed || Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { needsShareButton = false } else if item.message.id.peerId == item.context.account.peerId { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 588bf59b55..e10b3d40c7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -464,7 +464,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { break } } - if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal { + if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal || item.message.id.namespace == Namespaces.Message.QuickReplyLocal { notConsumed = true } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index e1f4f5b65f..297e1a4988 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -524,7 +524,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat return } var messageId: MessageId? - if let messageReference = messageReference, case let .message(_, _, id, _, _, _) = messageReference.content { + if let messageReference = messageReference, case let .message(_, _, id, _, _, _, _) = messageReference.content { messageId = id } strongSelf.controllerInteraction?.openPeerContextMenu(peer, messageId, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture) @@ -751,7 +751,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat @objc private func tapGesture(_ recognizer: ListViewTapGestureRecognizer) { if case .ended = recognizer.state { - if self.peerId.namespace == Namespaces.Peer.Empty, case let .message(_, _, id, _, _, _) = self.messageReference?.content { + if self.peerId.namespace == Namespaces.Peer.Empty, case let .message(_, _, id, _, _, _, _) = self.messageReference?.content { self.controllerInteraction?.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, self.avatarNode.frame) } else if let peer = self.peer { if let adMessageId = self.adMessageId { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 28f52c83e9..940ca9feb7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -494,7 +494,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { let shouldHaveRadioNode = optionResult == nil let isSelectable: Bool - if shouldHaveRadioNode, case .poll(multipleAnswers: true) = poll.kind, !Namespaces.Message.allScheduled.contains(message.id.namespace) { + if shouldHaveRadioNode, case .poll(multipleAnswers: true) = poll.kind, !Namespaces.Message.allNonRegular.contains(message.id.namespace) { isSelectable = true } else { isSelectable = false @@ -905,7 +905,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } if !hasSelection { - if !Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + if !Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { item.controllerInteraction.requestOpenMessagePollResults(item.message.id, pollId) } } else if !selectedOpaqueIdentifiers.isEmpty { @@ -1211,7 +1211,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { boundingSize.width = max(boundingSize.width, min(270.0, constrainedSize.width)) var canVote = false - if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allScheduled.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !isClosed { + if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allNonRegular.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !isClosed { var hasVoted = false if let voters = poll.results.voters { for voter in voters { @@ -1580,7 +1580,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { self.buttonNode.isHidden = false } - if Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + if Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { self.buttonNode.isUserInteractionEnabled = false } else { self.buttonNode.isUserInteractionEnabled = true @@ -1643,7 +1643,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if optionNode.frame.contains(point), case .tap = gesture { if optionNode.isUserInteractionEnabled { return ChatMessageBubbleContentTapAction(content: .ignore) - } else if let result = optionNode.currentResult, let item = self.item, !Namespaces.Message.allScheduled.contains(item.message.id.namespace), let poll = self.poll, let option = optionNode.option, !isBotChat { + } else if let result = optionNode.currentResult, let item = self.item, !Namespaces.Message.allNonRegular.contains(item.message.id.namespace), let poll = self.poll, let option = optionNode.option, !isBotChat { switch poll.publicity { case .anonymous: let string: String diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 35dc348dac..3c6f2d43f0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -499,7 +499,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { var needsShareButton = false if case .pinnedMessages = item.associatedData.subject { needsShareButton = true - } else if isFailed || Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + } else if isFailed || Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { needsShareButton = false } else if item.message.id.peerId == item.context.account.peerId { for attribute in item.content.firstMessage.attributes { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift index 0323734314..6ee2fe4f8f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift @@ -265,12 +265,12 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode for range in self.cachedWeekMinuteSet.rangeView { if range.lowerBound > currentWeekMinute { let openInMinutes = range.lowerBound - currentWeekMinute - let _ = openInMinutes - /*if openInMinutes < 60 { - openStatusText = "Opens in \(openInMinutes) minutes" + //TODO:localize + if openInMinutes < 60 { + currentDayStatusText = "Opens in \(openInMinutes) minutes" } else if openInMinutes < 6 * 60 { - openStatusText = "Opens in \(openInMinutes / 60) hours" - } else*/ do { + currentDayStatusText = "Opens in \(openInMinutes / 60) hours" + } else { let openDate = currentDate.addingTimeInterval(Double(openInMinutes * 60)) let openTimestamp = Int32(openDate.timeIntervalSince1970) + Int32(currentCalendar.timeZone.secondsFromGMT() - TimeZone.current.secondsFromGMT()) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift index b238231a04..8e5298fe9b 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift @@ -6,173 +6,168 @@ import TelegramCore import AccountContext final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtocol { + private final class PendingMessageContext { + let disposable = MetaDisposable() + var message: Message? + + init() { + } + } + private final class Impl { let queue: Queue let context: AccountContext - private var messages: [Message] = [] - private var nextMessageId: Int32 = 1000 - let messagesPromise = Promise<[Message]>([]) + private var shortcut: String + private var shortcutId: Int32? - private var nextGroupingId: UInt32 = 0 - private var groupingKeyToGroupId: [Int64: UInt32] = [:] + private(set) var mergedHistoryView: MessageHistoryView? + private var sourceHistoryView: MessageHistoryView? - init(queue: Queue, context: AccountContext, messages: [EngineMessage]) { + private var pendingMessages: [PendingMessageContext] = [] + private var historyViewDisposable: Disposable? + let historyViewStream = ValuePipe<(MessageHistoryView, ViewUpdateType)>() + private var nextUpdateIsHoleFill: Bool = false + + init(queue: Queue, context: AccountContext, shortcut: String, shortcutId: Int32?) { self.queue = queue self.context = context - self.messages = messages.map { $0._asMessage() } - self.notifyMessagesUpdated() + self.shortcut = shortcut + self.shortcutId = shortcutId - if let maxMessageId = messages.map(\.id).max() { - self.nextMessageId = maxMessageId.id + 1 - } - if let maxGroupingId = messages.compactMap(\.groupInfo?.stableId).max() { - self.nextGroupingId = maxGroupingId + 1 - } + self.updateHistoryViewRequest(reload: false) } deinit { + for context in self.pendingMessages { + context.disposable.dispose() + } + self.historyViewDisposable?.dispose() } - private func notifyMessagesUpdated() { - self.messages.sort(by: { $0.index > $1.index }) - self.messagesPromise.set(.single(self.messages)) + private func updateHistoryViewRequest(reload: Bool) { + if let shortcutId = self.shortcutId { + if self.historyViewDisposable == nil || reload { + self.historyViewDisposable?.dispose() + + self.historyViewDisposable = (self.context.account.viewTracker.quickReplyMessagesViewForLocation(quickReplyId: shortcutId) + |> deliverOn(self.queue)).start(next: { [weak self] view, update, _ in + guard let self else { + return + } + if update == .FillHole { + self.nextUpdateIsHoleFill = true + self.updateHistoryViewRequest(reload: true) + return + } + + let nextUpdateIsHoleFill = self.nextUpdateIsHoleFill + self.nextUpdateIsHoleFill = false + + self.sourceHistoryView = view + self.updateHistoryView(updateType: nextUpdateIsHoleFill ? .FillHole : .Generic) + }) + } + } else { + if self.sourceHistoryView == nil { + let sourceHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: [], holeEarlier: false, holeLater: false, isLoading: false) + self.sourceHistoryView = sourceHistoryView + self.updateHistoryView(updateType: .Initial) + } + } + } + + private func updateHistoryView(updateType: ViewUpdateType) { + var entries = self.sourceHistoryView?.entries ?? [] + for pendingMessage in self.pendingMessages { + if let message = pendingMessage.message { + if !entries.contains(where: { $0.message.stableId == message.stableId }) { + entries.append(MessageHistoryEntry( + message: message, + isRead: true, + location: nil, + monthLocation: nil, + attributes: MutableMessageHistoryEntryAttributes( + authorIsContact: false + ) + )) + } + } + } + entries.sort(by: { $0.message.index < $1.message.index }) + + let mergedHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: entries, holeEarlier: false, holeLater: false, isLoading: false) + self.mergedHistoryView = mergedHistoryView + + self.historyViewStream.putNext((mergedHistoryView, updateType)) } func enqueueMessages(messages: [EnqueueMessage]) { - for message in messages { - switch message { - case let .message(text, attributes, _, mediaReference, _, _, _, localGroupingKey, correlationId, _): - let _ = attributes - let _ = mediaReference - let _ = correlationId - - let messageId = self.nextMessageId - self.nextMessageId += 1 - - var attributes: [MessageAttribute] = [] - attributes.append(OutgoingMessageInfoAttribute( - uniqueId: Int64.random(in: Int64.min ... Int64.max), - flags: [], - acknowledged: true, - correlationId: correlationId, - bubbleUpEmojiOrStickersets: [] - )) - - var media: [Media] = [] - if let mediaReference { - media.append(mediaReference.media) - } - - let mappedMessage = Message( - stableId: UInt32(messageId), - stableVersion: 0, - id: MessageId( - peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0)), - namespace: Namespaces.Message.Local, - id: Int32(messageId) - ), - globallyUniqueId: nil, - groupingKey: localGroupingKey, - groupInfo: localGroupingKey.flatMap { value in - if let current = self.groupingKeyToGroupId[value] { - return MessageGroupInfo(stableId: current) - } else { - let groupId = self.nextGroupingId - self.nextGroupingId += 1 - self.groupingKeyToGroupId[value] = groupId - return MessageGroupInfo(stableId: groupId) - } - }, - threadId: nil, - timestamp: messageId, - flags: [], - tags: [], - globalTags: [], - localTags: [], - customTags: [], - forwardInfo: nil, - author: nil, - text: text, - attributes: attributes, - media: media, - peers: SimpleDictionary(), - associatedMessages: SimpleDictionary(), - associatedMessageIds: [], - associatedMedia: [:], - associatedThreadInfo: nil, - associatedStories: [:] - ) - self.messages.append(mappedMessage) - case .forward: - break + let threadId = self.shortcutId.flatMap(Int64.init) + let _ = (TelegramCore.enqueueMessages(account: self.context.account, peerId: self.context.account.peerId, messages: messages.map { message in + return message.withUpdatedThreadId(threadId).withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingQuickReplyMessageAttribute }) + attributes.append(OutgoingQuickReplyMessageAttribute(shortcut: self.shortcut)) + return attributes } - } - self.notifyMessagesUpdated() + }) + |> deliverOn(self.queue)).startStandalone(next: { [weak self] result in + guard let self else { + return + } + if self.shortcutId != nil { + return + } + for id in result { + if let id { + let pendingMessage = PendingMessageContext() + self.pendingMessages.append(pendingMessage) + pendingMessage.disposable.set(( + self.context.account.postbox.messageView(id) + |> deliverOn(self.queue) + ).startStrict(next: { [weak self, weak pendingMessage] messageView in + guard let self else { + return + } + guard let pendingMessage else { + return + } + pendingMessage.message = messageView.message + if let message = pendingMessage.message, message.id.namespace == Namespaces.Message.QuickReplyCloud, let threadId = message.threadId { + self.shortcutId = Int32(clamping: threadId) + self.updateHistoryViewRequest(reload: true) + } else { + self.updateHistoryView(updateType: .Generic) + } + })) + } + } + }) } func deleteMessages(ids: [EngineMessage.Id]) { - self.messages = self.messages.filter({ !ids.contains($0.id) }) - self.notifyMessagesUpdated() + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: ids, type: .forEveryone).startStandalone() } func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) { - guard let index = self.messages.firstIndex(where: { $0.id == id }) else { - return + } + + func quickReplyUpdateShortcut(value: String) { + if let shortcutId = self.shortcutId { + self.context.engine.accountData.editMessageShortcut(id: shortcutId, shortcut: value) } - let originalMessage = self.messages[index] - - var mappedMedia = originalMessage.media - switch media { - case .keep: - break - case let .update(value): - mappedMedia = [value.media] - } - - var mappedAtrributes = originalMessage.attributes - mappedAtrributes.removeAll(where: { $0 is TextEntitiesMessageAttribute }) - if let entities { - mappedAtrributes.append(entities) - } - - let mappedMessage = Message( - stableId: originalMessage.stableId, - stableVersion: originalMessage.stableVersion + 1, - id: originalMessage.id, - globallyUniqueId: originalMessage.globallyUniqueId, - groupingKey: originalMessage.groupingKey, - groupInfo: originalMessage.groupInfo, - threadId: originalMessage.threadId, - timestamp: originalMessage.timestamp, - flags: originalMessage.flags, - tags: originalMessage.tags, - globalTags: originalMessage.globalTags, - localTags: originalMessage.localTags, - customTags: originalMessage.customTags, - forwardInfo: originalMessage.forwardInfo, - author: originalMessage.author, - text: text, - attributes: mappedAtrributes, - media: mappedMedia, - peers: originalMessage.peers, - associatedMessages: originalMessage.associatedMessages, - associatedMessageIds: originalMessage.associatedMessageIds, - associatedMedia: originalMessage.associatedMedia, - associatedThreadInfo: originalMessage.associatedThreadInfo, - associatedStories: originalMessage.associatedStories - ) - - self.messages[index] = mappedMessage - self.notifyMessagesUpdated() } } var kind: ChatCustomContentsKind - var messages: Signal<[Message], NoError> { + var historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError> { return self.impl.signalWith({ impl, subscriber in - return impl.messagesPromise.get().start(next: subscriber.putNext) + if let mergedHistoryView = impl.mergedHistoryView { + subscriber.putNext((mergedHistoryView, .Initial)) + } + return impl.historyViewStream.signal().start(next: subscriber.putNext) }) } @@ -183,13 +178,23 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco private let queue: Queue private let impl: QueueLocalObject - init(context: AccountContext, messages: [EngineMessage], kind: ChatCustomContentsKind) { + init(context: AccountContext, kind: ChatCustomContentsKind, shortcutId: Int32?) { self.kind = kind + let initialShortcut: String + switch kind { + case .awayMessageInput: + initialShortcut = "_away" + case .greetingMessageInput: + initialShortcut = "_greeting" + case let .quickReplyMessageInput(shortcut): + initialShortcut = shortcut + } + let queue = Queue() self.queue = queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, context: context, messages: messages) + return Impl(queue: queue, context: context, shortcut: initialShortcut, shortcutId: shortcutId) }) } @@ -213,5 +218,8 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco func quickReplyUpdateShortcut(value: String) { self.kind = .quickReplyMessageInput(shortcut: value) + self.impl.with { impl in + impl.quickReplyUpdateShortcut(value: value) + } } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift index 27e2488031..409efe246f 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -352,8 +352,8 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { } let contents = AutomaticBusinessMessageSetupChatContents( context: component.context, - messages: self.messages, - kind: component.mode == .away ? .awayMessageInput : .greetingMessageInput + kind: component.mode == .away ? .awayMessageInput : .greetingMessageInput, + shortcutId: nil ) let chatController = component.context.sharedContext.makeChatController( context: component.context, @@ -365,17 +365,6 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { chatController.navigationPresentation = .modal self.environment?.controller()?.push(chatController) self.messagesDisposable?.dispose() - self.messagesDisposable = (contents.messages - |> deliverOnMainQueue).startStrict(next: { [weak self] messages in - guard let self else { - return - } - let messages = messages.map(EngineMessage.init) - if self.messages != messages { - self.messages = messages - self.state?.updated(transition: .immediate) - } - }) } private func openCustomScheduleDateSetup(isStartTime: Bool, isDate: Bool) { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BottomPanelComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BottomPanelComponent.swift new file mode 100644 index 0000000000..2dd0f3bae7 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BottomPanelComponent.swift @@ -0,0 +1,117 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import ComponentDisplayAdapters + +final class BottomPanelComponent: Component { + let theme: PresentationTheme + let content: AnyComponentWithIdentity + let insets: UIEdgeInsets + + init( + theme: PresentationTheme, + content: AnyComponentWithIdentity, + insets: UIEdgeInsets + ) { + self.theme = theme + self.content = content + self.insets = insets + } + + static func ==(lhs: BottomPanelComponent, rhs: BottomPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.content != rhs.content { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + + final class View: UIView { + private let separatorLayer: SimpleLayer + private let backgroundView: BlurredBackgroundView + private var content = ComponentView() + + private var component: BottomPanelComponent? + private weak var componentState: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.layer.addSublayer(self.separatorLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + self.component = component + self.componentState = state + + let themeUpdated = previousComponent?.theme !== component.theme + + var contentHeight: CGFloat = 0.0 + + contentHeight += component.insets.top + + var contentTransition = transition + if let previousComponent, previousComponent.content.id != component.content.id { + contentTransition = contentTransition.withAnimation(.none) + self.content.view?.removeFromSuperview() + self.content = ComponentView() + } + + let contentSize = self.content.update( + transition: contentTransition, + component: component.content.component, + environment: {}, + containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: availableSize.height - component.insets.top - component.insets.bottom) + ) + let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - contentSize.width) * 0.5), y: contentHeight), size: contentSize) + if let contentView = self.content.view { + if contentView.superview == nil { + self.addSubview(contentView) + } + contentTransition.setFrame(view: contentView, frame: contentFrame) + } + contentHeight += contentSize.height + + contentHeight += component.insets.bottom + + let size = CGSize(width: availableSize.width, height: contentHeight) + + if themeUpdated { + self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor + } + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition) + + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + 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/QuickReplyEmptyStateComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift index d2572d0be4..a145ec7be2 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift @@ -7,6 +7,7 @@ import AppBundle import ButtonComponent import MultilineTextComponent import BalancedTextComponent +import LottieComponent final class QuickReplyEmptyStateComponent: Component { let theme: PresentationTheme @@ -63,72 +64,6 @@ final class QuickReplyEmptyStateComponent: Component { let _ = previousComponent - let iconTitleSpacing: CGFloat = 10.0 - let titleTextSpacing: CGFloat = 8.0 - - let iconSize = self.icon.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "📝", font: Font.semibold(90.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)), - horizontalAlignment: .center - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - - //TODO:localize - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "No Quick Replies", font: Font.semibold(17.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)), - horizontalAlignment: .center - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0) - ) - - let textSize = self.text.update( - transition: .immediate, - component: AnyComponent(BalancedTextComponent( - text: .plain(NSAttributedString(string: "Set up shortcuts with rich text and media to respond to messages faster.", font: Font.regular(15.0), textColor: component.theme.rootController.navigationBar.secondaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 20 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0) - ) - - let centralContentsHeight: CGFloat = iconSize.height + iconTitleSpacing + titleSize.height + titleTextSpacing - var centralContentsY: CGFloat = component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - centralContentsHeight) * 0.5) - - let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: centralContentsY), size: iconSize) - if let iconView = self.icon.view { - if iconView.superview == nil { - self.addSubview(iconView) - } - transition.setFrame(view: iconView, frame: iconFrame) - } - centralContentsY += iconSize.height + iconTitleSpacing - - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: centralContentsY), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - self.addSubview(titleView) - } - titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) - transition.setPosition(view: titleView, position: titleFrame.center) - } - centralContentsY += titleSize.height + titleTextSpacing - - let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: centralContentsY), size: textSize) - if let textView = self.text.view { - if textView.superview == nil { - self.addSubview(textView) - } - textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) - transition.setPosition(view: textView, position: textFrame.center) - } - let buttonSize = self.button.update( transition: transition, component: AnyComponent(ButtonComponent( @@ -159,7 +94,7 @@ final class QuickReplyEmptyStateComponent: Component { environment: {}, containerSize: CGSize(width: min(availableSize.width - 16.0 * 2.0, 280.0), height: 50.0) ) - let buttonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: availableSize.height - component.insets.bottom - 8.0 - buttonSize.height), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: availableSize.height - component.insets.bottom - 14.0 - buttonSize.height), size: buttonSize) if let buttonView = self.button.view { if buttonView.superview == nil { self.addSubview(buttonView) @@ -167,6 +102,75 @@ final class QuickReplyEmptyStateComponent: Component { transition.setFrame(view: buttonView, frame: buttonFrame) } + let iconTitleSpacing: CGFloat = 13.0 + let titleTextSpacing: CGFloat = 9.0 + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "WriteEmoji"), + loop: false + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 120.0) + ) + + //TODO:localize + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "No Quick Replies", font: Font.semibold(17.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0) + ) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: "Set up shortcuts with rich text and media to respond to messages faster.", font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 20, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0) + ) + + let topInset: CGFloat = component.insets.top + + let centralContentsHeight: CGFloat = iconSize.height + iconTitleSpacing + titleSize.height + titleTextSpacing + var centralContentsY: CGFloat = topInset + floor((buttonFrame.minY - topInset - centralContentsHeight) * 0.426) + + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: centralContentsY), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: iconFrame) + } + centralContentsY += iconSize.height + iconTitleSpacing + + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: centralContentsY), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + transition.setPosition(view: titleView, position: titleFrame.center) + } + centralContentsY += titleSize.height + titleTextSpacing + + let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: centralContentsY), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + transition.setPosition(view: textView, position: textFrame.center) + } + return availableSize } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 58cf348038..010f4c6fca 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -19,6 +19,8 @@ import ItemListUI import ChatListUI import QuickReplyNameAlertController import ChatListHeaderComponent +import PlainButtonComponent +import MultilineTextComponent final class QuickReplySetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -45,30 +47,30 @@ final class QuickReplySetupScreenComponent: Component { private enum ContentEntry: Comparable, Identifiable { enum Id: Hashable { case add - case item(String) + case item(Int32) } var stableId: Id { switch self { case .add: return .add - case let .item(item, _, _, _): - return .item(item.shortcut) + case let .item(item, _, _, _, _): + return .item(item.id) } } case add - case item(item: QuickReplyMessageShortcut, accountPeer: EnginePeer, sortIndex: Int, isEditing: Bool) + case item(item: ShortcutMessageList.Item, accountPeer: EnginePeer, sortIndex: Int, isEditing: Bool, isSelected: Bool) static func <(lhs: ContentEntry, rhs: ContentEntry) -> Bool { switch lhs { case .add: return false - case let .item(lhsItem, _, lhsSortIndex, _): + case let .item(lhsItem, _, lhsSortIndex, _, _): switch rhs { case .add: return false - case let .item(rhsItem, _, rhsSortIndex, _): + case let .item(rhsItem, _, rhsSortIndex, _, _): if lhsSortIndex != rhsSortIndex { return lhsSortIndex < rhsSortIndex } @@ -97,10 +99,10 @@ final class QuickReplySetupScreenComponent: Component { guard let listNode, let parentView = listNode.parentView else { return } - parentView.openQuickReplyChat(shortcut: nil) + parentView.openQuickReplyChat(shortcut: nil, shortcutId: nil) } ) - case let .item(item, accountPeer, _, isEditing): + case let .item(item, accountPeer, _, isEditing, isSelected): let chatListNodeInteraction = ChatListNodeInteraction( context: listNode.context, animationCache: listNode.context.animationCache, @@ -111,13 +113,21 @@ final class QuickReplySetupScreenComponent: Component { guard let listNode, let parentView = listNode.parentView else { return } - parentView.openQuickReplyChat(shortcut: item.shortcut) + parentView.openQuickReplyChat(shortcut: item.shortcut, shortcutId: item.id) }, disabledPeerSelected: { _, _, _ in }, - togglePeerSelected: { _, _ in + togglePeerSelected: { [weak listNode] _, _ in + guard let listNode, let parentView = listNode.parentView else { + return + } + parentView.toggleShortcutSelection(id: item.id) }, - togglePeersSelection: { _, _ in + togglePeersSelection: { [weak listNode] _, _ in + guard let listNode, let parentView = listNode.parentView else { + return + } + parentView.toggleShortcutSelection(id: item.id) }, additionalCategorySelected: { _ in }, @@ -125,7 +135,7 @@ final class QuickReplySetupScreenComponent: Component { guard let listNode, let parentView = listNode.parentView else { return } - parentView.openQuickReplyChat(shortcut: item.shortcut) + parentView.openQuickReplyChat(shortcut: item.shortcut, shortcutId: item.id) }, groupSelected: { _ in }, @@ -143,7 +153,7 @@ final class QuickReplySetupScreenComponent: Component { guard let listNode, let parentView = listNode.parentView else { return } - parentView.openDeleteShortcut(shortcut: item.shortcut) + parentView.openDeleteShortcuts(ids: [item.id]) }, deletePeerThread: { _, _ in }, @@ -193,7 +203,7 @@ final class QuickReplySetupScreenComponent: Component { guard let listNode, let parentView = listNode.parentView else { return } - parentView.openEditShortcut(shortcut: item.shortcut) + parentView.openEditShortcut(id: item.id, currentValue: item.shortcut) } ) @@ -215,7 +225,7 @@ final class QuickReplySetupScreenComponent: Component { filterData: nil, index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: listNode.context.account.peerId, namespace: 0, id: 0), timestamp: 0))), content: .peer(ChatListItemContent.PeerData( - messages: item.messages.first.flatMap({ [$0] }) ?? [], + messages: [item.topMessage], peer: EngineRenderedPeer(peer: accountPeer), threadInfo: nil, combinedReadState: nil, @@ -245,7 +255,7 @@ final class QuickReplySetupScreenComponent: Component { )), editing: isEditing, hasActiveRevealControls: false, - selected: false, + selected: isSelected, header: nil, enableContextActions: true, hiddenOffset: false, @@ -260,6 +270,10 @@ final class QuickReplySetupScreenComponent: Component { let context: AccountContext var presentationData: PresentationData private var currentEntries: [ContentEntry] = [] + private var originalEntries: [ContentEntry] = [] + private var tempOrder: [Int32]? + private var pendingRemoveItems: [Int32]? + private var resetTempOrderOnNextUpdate: Bool = false init(parentView: View, context: AccountContext) { self.parentView = parentView @@ -267,6 +281,87 @@ final class QuickReplySetupScreenComponent: Component { self.presentationData = context.sharedContext.currentPresentationData.with({ $0 }) super.init() + + self.reorderBegan = { [weak self] in + guard let self else { + return + } + self.tempOrder = nil + } + self.reorderCompleted = { [weak self] _ in + guard let self, let tempOrder = self.tempOrder else { + return + } + self.resetTempOrderOnNextUpdate = true + self.context.engine.accountData.reorderMessageShortcuts(ids: tempOrder, completion: {}) + } + self.reorderItem = { [weak self] fromIndex, toIndex, transactionOpaqueState -> Signal in + guard let self else { + return .single(false) + } + guard fromIndex >= 0 && fromIndex < self.currentEntries.count && toIndex >= 0 && toIndex < self.currentEntries.count else { + return .single(false) + } + + let fromEntry = self.currentEntries[fromIndex] + let toEntry = self.currentEntries[toIndex] + + var referenceId: Int32? + var beforeAll = false + switch toEntry { + case let .item(item, _, _, _, _): + referenceId = item.id + case .add: + beforeAll = true + } + + if case let .item(item, _, _, _, _) = fromEntry { + var itemIds = self.currentEntries.compactMap { entry -> Int32? in + switch entry { + case .add: + return nil + case let .item(item, _, _, _, _): + return item.id + } + } + let itemId: Int32? = item.id + + if let itemId { + itemIds = itemIds.filter({ $0 != itemId }) + if let referenceId { + var inserted = false + for i in 0 ..< itemIds.count { + if itemIds[i] == referenceId { + if fromIndex < toIndex { + itemIds.insert(itemId, at: i + 1) + } else { + itemIds.insert(itemId, at: i) + } + inserted = true + break + } + } + if !inserted { + itemIds.append(itemId) + } + } else if beforeAll { + itemIds.insert(itemId, at: 0) + } else { + itemIds.append(itemId) + } + if self.tempOrder != itemIds { + self.tempOrder = itemIds + self.setEntries(entries: self.originalEntries, animated: true) + } + + return .single(true) + } else { + return .single(false) + } + } else { + return .single(false) + } + } } func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) { @@ -275,14 +370,74 @@ final class QuickReplySetupScreenComponent: Component { deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], - options: [.Synchronous, .LowLatency], + options: [.Synchronous, .LowLatency, .PreferSynchronousResourceLoading], additionalScrollDistance: 0.0, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: listViewDuration, curve: listViewCurve), updateOpaqueState: nil ) } + func setPendingRemoveItems(itemIds: [Int32]) { + self.pendingRemoveItems = itemIds + self.setEntries(entries: self.originalEntries, animated: true) + } + func setEntries(entries: [ContentEntry], animated: Bool) { + if self.resetTempOrderOnNextUpdate { + self.resetTempOrderOnNextUpdate = false + self.tempOrder = nil + } + let pendingRemoveItems = self.pendingRemoveItems + self.pendingRemoveItems = nil + + self.originalEntries = entries + + var entries = entries + if let pendingRemoveItems { + entries = entries.filter { entry in + switch entry.stableId { + case .add: + return true + case let .item(id): + return !pendingRemoveItems.contains(id) + } + } + } + + if let tempOrder = self.tempOrder { + let originalList = entries + entries.removeAll() + + if let entry = originalList.first(where: { entry in + if case .add = entry { + return true + } else { + return false + } + }) { + entries.append(entry) + } + + for id in tempOrder { + if let entry = originalList.first(where: { entry in + if case let .item(listId) = entry.stableId, listId == id { + return true + } else { + return false + } + }) { + entries.append(entry) + } + } + for entry in originalList { + if !entries.contains(where: { listEntry in + listEntry.stableId == entry.stableId + }) { + entries.append(entry) + } + } + } + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries) self.currentEntries = entries @@ -316,15 +471,19 @@ final class QuickReplySetupScreenComponent: Component { private let navigationBarView = ComponentView() private var navigationHeight: CGFloat? + private var selectionPanel: ComponentView? + private var isUpdating: Bool = false private var component: QuickReplySetupScreenComponent? private(set) weak var state: EmptyComponentState? private var environment: EnvironmentType? - private var items: [QuickReplyMessageShortcut] = [] - private var itemsDisposable: Disposable? - private var messagesDisposable: Disposable? + private var shortcutMessageList: ShortcutMessageList? + private var shortcutMessageListDisposable: Disposable? + private var keepUpdatedDisposable: Disposable? + + private var selectedIds = Set() private var isEditing: Bool = false private var isSearchDisplayControllerActive: Bool = false @@ -340,44 +499,27 @@ final class QuickReplySetupScreenComponent: Component { } deinit { - self.itemsDisposable?.dispose() - self.messagesDisposable?.dispose() + self.shortcutMessageListDisposable?.dispose() + self.keepUpdatedDisposable?.dispose() } func scrollToTop() { } func attemptNavigation(complete: @escaping () -> Void) -> Bool { - guard let component = self.component else { - return true - } - component.context.engine.accountData.updateShortcutMessages(state: QuickReplyMessageShortcutsState(shortcuts: self.items)) return true } - func openQuickReplyChat(shortcut: String?) { + func openQuickReplyChat(shortcut: String?, shortcutId: Int32?) { guard let component = self.component else { return } if let shortcut { - var mappedMessages: [EngineMessage] = [] - if let messages = self.items.first(where: { $0.shortcut == shortcut })?.messages { - var nextId: Int32 = 1 - for message in messages { - var mappedMessage = message._asMessage() - mappedMessage = mappedMessage.withUpdatedId(id: MessageId(peerId: component.context.account.peerId, namespace: 0, id: nextId)) - mappedMessage = mappedMessage.withUpdatedStableId(stableId: UInt32(nextId)) - mappedMessage = mappedMessage.withUpdatedTimestamp(nextId) - mappedMessages.append(EngineMessage(mappedMessage)) - - nextId += 1 - } - } let contents = AutomaticBusinessMessageSetupChatContents( context: component.context, - messages: mappedMessages, - kind: .quickReplyMessageInput(shortcut: shortcut) + kind: .quickReplyMessageInput(shortcut: shortcut), + shortcutId: shortcutId ) let chatController = component.context.sharedContext.makeChatController( context: component.context, @@ -388,30 +530,6 @@ final class QuickReplySetupScreenComponent: Component { ) chatController.navigationPresentation = .modal self.environment?.controller()?.push(chatController) - self.messagesDisposable?.dispose() - self.messagesDisposable = (contents.messages - |> deliverOnMainQueue).startStrict(next: { [weak self] messages in - guard let self, let component = self.component else { - return - } - let messages = messages.reversed().map(EngineMessage.init) - - if messages.isEmpty { - if let index = self.items.firstIndex(where: { $0.shortcut == shortcut }) { - self.items.remove(at: index) - } - } else { - if let index = self.items.firstIndex(where: { $0.shortcut == shortcut }) { - self.items[index] = QuickReplyMessageShortcut(id: self.items[index].id, shortcut: self.items[index].shortcut, messages: messages) - } else { - self.items.insert(QuickReplyMessageShortcut(id: Int32.random(in: Int32.min ... Int32.max), shortcut: shortcut, messages: messages), at: 0) - } - } - - component.context.engine.accountData.updateShortcutMessages(state: QuickReplyMessageShortcutsState(shortcuts: self.items)) - - self.state?.updated(transition: .immediate) - }) } else { var completion: ((String?) -> Void)? let alertController = quickReplyNameAlertController( @@ -430,7 +548,12 @@ final class QuickReplySetupScreenComponent: Component { return } if let value, !value.isEmpty { - if self.items.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { + guard let shortcutMessageList = self.shortcutMessageList else { + alertController?.dismissAnimated() + return + } + + if shortcutMessageList.items.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { if let contentNode = alertController?.contentNode as? QuickReplyNameAlertContentNode { contentNode.setErrorText(errorText: "Shortcut with that name already exists") } @@ -438,7 +561,7 @@ final class QuickReplySetupScreenComponent: Component { } alertController?.dismissAnimated() - self.openQuickReplyChat(shortcut: value) + self.openQuickReplyChat(shortcut: value, shortcutId: nil) } } self.environment?.controller()?.present(alertController, in: .window(.root)) @@ -447,13 +570,11 @@ final class QuickReplySetupScreenComponent: Component { self.contentListNode?.clearHighlightAnimated(true) } - func openEditShortcut(shortcut: String) { + func openEditShortcut(id: Int32, currentValue: String) { guard let component = self.component else { return } - let currentValue = shortcut - var completion: ((String?) -> Void)? let alertController = quickReplyNameAlertController( context: component.context, @@ -475,26 +596,17 @@ final class QuickReplySetupScreenComponent: Component { alertController?.dismissAnimated() return } - - var shortcuts = self.items - guard let index = shortcuts.firstIndex(where: { $0.shortcut.lowercased() == currentValue }) else { + guard let shortcutMessageList = self.shortcutMessageList else { alertController?.dismissAnimated() return } - if shortcuts.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { + if shortcutMessageList.items.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { if let contentNode = alertController?.contentNode as? QuickReplyNameAlertContentNode { contentNode.setErrorText(errorText: "Shortcut with that name already exists") } } else { - shortcuts[index] = QuickReplyMessageShortcut( - id: shortcuts[index].id, - shortcut: value, - messages: shortcuts[index].messages - ) - self.items = shortcuts - let updatedShortcutMessages = QuickReplyMessageShortcutsState(shortcuts: shortcuts) - component.context.engine.accountData.updateShortcutMessages(state: updatedShortcutMessages) + component.context.engine.accountData.editMessageShortcut(id: id, shortcut: value) alertController?.dismissAnimated() } @@ -503,23 +615,48 @@ final class QuickReplySetupScreenComponent: Component { self.environment?.controller()?.present(alertController, in: .window(.root)) } - func openDeleteShortcut(shortcut: String) { + func toggleShortcutSelection(id: Int32) { + if self.selectedIds.contains(id) { + self.selectedIds.remove(id) + } else { + self.selectedIds.insert(id) + } + self.state?.updated(transition: .spring(duration: 0.4)) + } + + func openDeleteShortcuts(ids: [Int32]) { guard let component = self.component else { return } - var shortcuts = self.items - guard let index = shortcuts.firstIndex(where: { $0.shortcut.lowercased() == shortcut }) else { - return - } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] - shortcuts.remove(at: index) - self.items = shortcuts - - self.state?.updated(transition: .spring(duration: 0.4)) - - let updatedShortcutMessages = QuickReplyMessageShortcutsState(shortcuts: shortcuts) - component.context.engine.accountData.updateShortcutMessages(state: updatedShortcutMessages) + //TODO:localize + items.append(ActionSheetButtonItem(title: ids.count == 1 ? "Delete Shortcut" : "Delete Shortcuts", color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + guard let self, let component = self.component else { + return + } + + for id in ids { + self.selectedIds.remove(id) + } + self.contentListNode?.setPendingRemoveItems(itemIds: ids) + component.context.engine.accountData.deleteMessageShortcuts(ids: ids) + self.state?.updated(transition: .spring(duration: 0.4)) + })) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + 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)) } private func updateNavigationBar( @@ -533,31 +670,43 @@ final class QuickReplySetupScreenComponent: Component { deferScrollApplication: Bool ) -> CGFloat { var rightButtons: [AnyComponentWithIdentity] = [] - if self.isEditing { - rightButtons.append(AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( - content: .text(title: strings.Common_Done, isBold: true), - pressed: { [weak self] _ in - guard let self else { - return + if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty { + if self.isEditing { + rightButtons.append(AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( + content: .text(title: strings.Common_Done, isBold: true), + pressed: { [weak self] _ in + guard let self else { + return + } + self.isEditing = false + self.selectedIds.removeAll() + self.state?.updated(transition: .spring(duration: 0.4)) } - self.isEditing = false - self.state?.updated(transition: .spring(duration: 0.4)) - } - )))) - } else { - rightButtons.append(AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent( - content: .text(title: strings.Common_Edit, isBold: false), - pressed: { [weak self] _ in - guard let self else { - return + )))) + } else { + rightButtons.append(AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent( + content: .text(title: strings.Common_Edit, isBold: false), + pressed: { [weak self] _ in + guard let self else { + return + } + self.isEditing = true + self.state?.updated(transition: .spring(duration: 0.4)) } - self.isEditing = true - self.state?.updated(transition: .spring(duration: 0.4)) - } - )))) + )))) + } } + + let titleText: String + if !self.selectedIds.isEmpty { + //TODO:localize + titleText = "\(self.selectedIds.count) Selected" + } else { + titleText = "Quick Replies" + } + let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content( - title: "Quick Replies", + title: titleText, navigationBackTitle: nil, titleComponent: nil, chatListTitle: nil, @@ -629,9 +778,7 @@ final class QuickReplySetupScreenComponent: Component { private func updateNavigationScrolling(navigationHeight: CGFloat, transition: Transition) { var mainOffset: CGFloat - if self.items.isEmpty { - mainOffset = navigationHeight - } else { + if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty { if let contentListNode = self.contentListNode { switch contentListNode.visibleContentOffset() { case .none: @@ -644,6 +791,8 @@ final class QuickReplySetupScreenComponent: Component { } else { mainOffset = navigationHeight } + } else { + mainOffset = navigationHeight } mainOffset = min(mainOffset, ChatListNavigationBar.searchScrollHeight) @@ -674,18 +823,20 @@ final class QuickReplySetupScreenComponent: Component { if self.component == nil { self.accountPeer = component.initialData.accountPeer - self.items = component.initialData.shortcutMessages.shortcuts + self.shortcutMessageList = component.initialData.shortcutMessageList - self.itemsDisposable = (component.context.engine.accountData.shortcutMessages() - |> deliverOnMainQueue).start(next: { [weak self] shortcutMessages in + self.shortcutMessageListDisposable = (component.context.engine.accountData.shortcutMessageList() + |> deliverOnMainQueue).startStrict(next: { [weak self] shortcutMessageList in guard let self else { return } - self.items = shortcutMessages.shortcuts + self.shortcutMessageList = shortcutMessageList if !self.isUpdating { self.state?.updated(transition: .immediate) } }) + + self.keepUpdatedDisposable = component.context.engine.accountData.keepShortcutMessageListUpdated().startStrict() } let environment = environment[EnvironmentType.self].value @@ -702,7 +853,12 @@ final class QuickReplySetupScreenComponent: Component { self.backgroundColor = environment.theme.list.plainBackgroundColor } - if self.items.isEmpty { + if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty { + if let emptyState = self.emptyState { + self.emptyState = nil + emptyState.view?.removeFromSuperview() + } + } else { let emptyState: ComponentView var emptyStateTransition = transition if let current = self.emptyState { @@ -713,18 +869,18 @@ final class QuickReplySetupScreenComponent: Component { emptyStateTransition = emptyStateTransition.withAnimation(.none) } - let emptyStateFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight), size: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight)) + let emptyStateFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)) let _ = emptyState.update( transition: emptyStateTransition, component: AnyComponent(QuickReplyEmptyStateComponent( theme: environment.theme, strings: environment.strings, - insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), + insets: UIEdgeInsets(top: environment.navigationHeight, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), action: { [weak self] in guard let self else { return } - self.openQuickReplyChat(shortcut: nil) + self.openQuickReplyChat(shortcut: nil, shortcutId: nil) } )), environment: {}, @@ -736,13 +892,9 @@ final class QuickReplySetupScreenComponent: Component { } emptyStateTransition.setFrame(view: emptyStateView, frame: emptyStateFrame) } - } else { - if let emptyState = self.emptyState { - self.emptyState = nil - emptyState.view?.removeFromSuperview() - } } + var listBottomInset = environment.safeInsets.bottom let navigationHeight = self.updateNavigationBar( component: component, theme: environment.theme, @@ -755,6 +907,78 @@ final class QuickReplySetupScreenComponent: Component { ) self.navigationHeight = navigationHeight + if !self.selectedIds.isEmpty { + let selectionPanel: ComponentView + var selectionPanelTransition = transition + if let current = self.selectionPanel { + selectionPanel = current + } else { + selectionPanelTransition = selectionPanelTransition.withAnimation(.none) + selectionPanel = ComponentView() + self.selectionPanel = selectionPanel + } + + let buttonTitle: String + if self.selectedIds.count == 1 { + buttonTitle = "Delete 1 Quick Reply" + } else { + buttonTitle = "Delete \(self.selectedIds.count) Quick Replies" + } + + let selectionPanelSize = selectionPanel.update( + transition: selectionPanelTransition, + component: AnyComponent(BottomPanelComponent( + theme: environment.theme, + content: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: buttonTitle, font: Font.regular(17.0), textColor: environment.theme.list.itemDestructiveColor)) + )), + background: nil, + effectAlignment: .center, + minSize: CGSize(width: availableSize.width - environment.safeInsets.left - environment.safeInsets.right, height: 44.0), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + guard let self else { + return + } + if self.selectedIds.isEmpty { + return + } + self.openDeleteShortcuts(ids: Array(self.selectedIds)) + }, + animateAlpha: true, + animateScale: false, + animateContents: false + ))), + insets: UIEdgeInsets(top: 4.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right) + )), + environment: {}, + containerSize: availableSize + ) + let selectionPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - selectionPanelSize.height), size: selectionPanelSize) + listBottomInset = selectionPanelSize.height + if let selectionPanelView = selectionPanel.view { + var animateIn = false + if selectionPanelView.superview == nil { + animateIn = true + self.addSubview(selectionPanelView) + } + selectionPanelTransition.setFrame(view: selectionPanelView, frame: selectionPanelFrame) + if animateIn { + transition.animatePosition(view: selectionPanelView, from: CGPoint(x: 0.0, y: selectionPanelFrame.height), to: CGPoint(), additive: true) + } + } + } else { + if let selectionPanel = self.selectionPanel { + self.selectionPanel = nil + if let selectionPanelView = selectionPanel.view { + transition.setPosition(view: selectionPanelView, position: CGPoint(x: selectionPanelView.center.x, y: availableSize.height + selectionPanelView.bounds.height * 0.5), completion: { [weak selectionPanelView] _ in + selectionPanelView?.removeFromSuperview() + }) + } + } + } + let contentListNode: ContentListNode if let current = self.contentListNode { contentListNode = current @@ -773,7 +997,9 @@ final class QuickReplySetupScreenComponent: Component { self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: .immediate) } - if let navigationBarComponentView = self.navigationBarView.view { + if let selectionPanelView = self.selectionPanel?.view { + self.insertSubview(contentListNode.view, belowSubview: selectionPanelView) + } else if let navigationBarComponentView = self.navigationBarView.view { self.insertSubview(contentListNode.view, belowSubview: navigationBarComponentView) } else { self.addSubview(contentListNode.view) @@ -781,18 +1007,22 @@ final class QuickReplySetupScreenComponent: Component { } transition.setFrame(view: contentListNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) - contentListNode.update(size: availableSize, insets: UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), transition: transition) + contentListNode.update(size: availableSize, insets: UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: listBottomInset, right: environment.safeInsets.right), transition: transition) var entries: [ContentEntry] = [] - if let accountPeer = self.accountPeer { + if let shortcutMessageList = self.shortcutMessageList, let accountPeer = self.accountPeer { entries.append(.add) - for item in self.items { - entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing)) + for item in shortcutMessageList.items { + entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing, isSelected: self.selectedIds.contains(item.id))) } } contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate) - contentListNode.isHidden = self.items.isEmpty + if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty { + contentListNode.isHidden = false + } else { + contentListNode.isHidden = true + } self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: transition) @@ -817,14 +1047,14 @@ final class QuickReplySetupScreenComponent: Component { public final class QuickReplySetupScreen: ViewControllerComponentContainer { public final class InitialData: QuickReplySetupScreenInitialData { let accountPeer: EnginePeer? - let shortcutMessages: QuickReplyMessageShortcutsState + let shortcutMessageList: ShortcutMessageList init( accountPeer: EnginePeer?, - shortcutMessages: QuickReplyMessageShortcutsState + shortcutMessageList: ShortcutMessageList ) { self.accountPeer = accountPeer - self.shortcutMessages = shortcutMessages + self.shortcutMessageList = shortcutMessageList } } @@ -874,13 +1104,13 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer { context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) ), - context.engine.accountData.shortcutMessages() + context.engine.accountData.shortcutMessageList() |> take(1) ) - |> map { accountPeer, shortcutMessages -> QuickReplySetupScreenInitialData in + |> map { accountPeer, shortcutMessageList -> QuickReplySetupScreenInitialData in return InitialData( accountPeer: accountPeer, - shortcutMessages: shortcutMessages + shortcutMessageList: shortcutMessageList ) } } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/Contents.json new file mode 100644 index 0000000000..5146f4aea0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "quickrepliesdemo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/quickrepliesdemo.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/quickrepliesdemo.pdf new file mode 100644 index 0000000000..7dd7ee40f2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/QuickReplies.imageset/quickrepliesdemo.pdf @@ -0,0 +1,127 @@ +%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 10.000000 7.000000 cm +0.000000 0.000000 0.000000 scn +25.000000 65.066650 m +11.192883 65.066650 0.000000 54.891304 0.000000 42.339378 c +0.000000 29.787453 11.192883 19.612106 25.000000 19.612106 c +27.116848 19.612106 29.172245 19.851284 31.135277 20.301537 c +31.506607 20.386707 31.928230 19.997253 32.632072 19.347115 c +33.483276 18.560856 34.747246 17.393326 36.834217 16.222927 c +39.500584 14.727596 43.194855 14.857323 43.820072 15.119576 c +44.419498 15.371006 43.958118 15.870796 43.145599 16.750961 c +42.583481 17.359879 41.853298 18.150848 41.190041 19.167587 c +39.568069 21.653973 40.238289 24.614052 40.965954 25.146793 c +46.656067 29.312656 50.000000 35.180622 50.000000 42.339378 c +50.000000 54.891304 38.807121 65.066650 25.000000 65.066650 c +h +50.000000 16.666668 m +50.000000 21.333771 50.000000 23.667324 50.908279 25.449921 c +51.707222 27.017937 52.982063 28.292778 54.550079 29.091721 c +56.332676 30.000000 58.666229 30.000000 63.333332 30.000000 c +66.666672 30.000000 l +71.333771 30.000000 73.667328 30.000000 75.449921 29.091721 c +77.017937 28.292778 78.292778 27.017937 79.091721 25.449921 c +80.000000 23.667324 80.000000 21.333771 80.000000 16.666668 c +80.000000 13.333332 l +80.000000 8.666229 80.000000 6.332676 79.091721 4.550079 c +78.292778 2.982063 77.017937 1.707222 75.449921 0.908279 c +73.667328 0.000000 71.333771 0.000000 66.666664 0.000000 c +63.333332 0.000000 l +58.666229 0.000000 56.332676 0.000000 54.550079 0.908279 c +52.982063 1.707222 51.707222 2.982063 50.908279 4.550079 c +50.000000 6.332676 50.000000 8.666229 50.000000 13.333336 c +50.000000 16.666668 l +h +55.723064 6.619629 m +55.723064 5.921387 56.220295 5.381836 57.098389 5.381836 c +57.849529 5.381836 58.283287 5.730957 58.505455 6.492676 c +63.192139 22.668621 l +63.245037 22.848469 63.266197 23.007160 63.266197 23.197590 c +63.266197 23.969891 62.716064 24.498859 61.890869 24.498859 c +61.139732 24.498859 60.705975 24.118000 60.473228 23.303387 c +55.797119 7.180340 l +55.754803 7.021648 55.723064 6.831219 55.723064 6.619629 c +h +62.353432 7.801270 m +62.353432 6.880859 62.977619 6.298992 63.972084 6.298992 c +64.818443 6.298992 65.294518 6.722168 65.590736 7.737793 c +66.394775 10.118164 l +71.906662 10.118164 l +72.710693 7.706055 l +72.996338 6.711590 73.472412 6.298992 74.371658 6.298992 c +75.302658 6.298992 75.969162 6.923176 75.969162 7.801270 c +75.969162 8.118652 75.916260 8.404297 75.768143 8.816895 c +71.578690 20.179199 l +71.123779 21.427570 70.383217 21.977699 69.134842 21.977699 c +67.928787 21.977699 67.177650 21.406410 66.733315 20.168617 c +62.565022 8.816895 l +62.427490 8.425457 62.353432 8.065754 62.353432 7.801270 c +h +71.166092 12.593750 m +67.093018 12.593750 l +69.071365 18.846191 l +69.145424 18.846191 l +71.166092 12.593750 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2818 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 100.000000 80.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 +0000002908 00000 n +0000002931 00000 n +0000003105 00000 n +0000003179 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3238 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/WriteEmoji.tgs b/submodules/TelegramUI/Resources/Animations/WriteEmoji.tgs new file mode 100644 index 0000000000000000000000000000000000000000..47caac05a2fc28b951f68bb336c11a332dea94e2 GIT binary patch literal 64148 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

Ct zGBrr)E(chPrDfAkXCUniP_p-D#Iur_QY*mFD!04;18N^QMnPVS+^|O?RlUGP&=S2q z0uwI5mF5UCQ8uj - if case let .customChatContents(customChatContents) = strongSelf.subject { - sourceMessage = customChatContents.messages - |> take(1) - |> map { messages -> EngineMessage? in - return messages.first(where: { $0.id == editMessage.messageId }).flatMap(EngineMessage.init) - } - } else { - sourceMessage = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId)) - } + sourceMessage = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId)) let _ = (sourceMessage |> deliverOnMainQueue).start(next: { [weak strongSelf] message in @@ -9210,37 +9202,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G media = .keep } - if case let .customChatContents(customChatContents) = strongSelf.subject { - customChatContents.editMessage(id: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview) - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - var state = state - state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) - state = state.updatedEditMessageState(nil) - return state - }) - } else { - let _ = (strongSelf.context.account.postbox.messageAtId(editMessage.messageId) - |> deliverOnMainQueue).startStandalone(next: { [weak self] currentMessage in - if let strongSelf = self { - if let currentMessage = currentMessage { - let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? [] - let currentWebpagePreviewAttribute = currentMessage.webpagePreviewAttribute ?? WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: nil, isManuallyAdded: true, isSafe: false) - - if currentMessage.text != text.string || currentEntities != entities || updatingMedia || webpagePreviewAttribute != currentWebpagePreviewAttribute || disableUrlPreview { - strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview) - } - } + let _ = (strongSelf.context.account.postbox.messageAtId(editMessage.messageId) + |> deliverOnMainQueue).startStandalone(next: { [weak self] currentMessage in + if let strongSelf = self { + if let currentMessage = currentMessage { + let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? [] + let currentWebpagePreviewAttribute = currentMessage.webpagePreviewAttribute ?? WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: nil, isManuallyAdded: true, isSafe: false) - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - var state = state - state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) - state = state.updatedEditMessageState(nil) - return state - }) + if currentMessage.text != text.string || currentEntities != entities || updatingMedia || webpagePreviewAttribute != currentWebpagePreviewAttribute || disableUrlPreview { + strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview) + } } - }) - } + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + var state = state + state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) + state = state.updatedEditMessageState(nil) + return state + }) + } + }) }) }, beginMessageSearch: { [weak self] domain, query in guard let strongSelf = self else { @@ -9479,12 +9460,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - }, sendShortcut: { [weak self] shortcut in + }, sendShortcut: { [weak self] shortcutId in guard let self else { return } + guard let peerId = self.chatLocation.peerId else { + return + } + let _ = self + let _ = shortcutId - self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in + self.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) } + }) + self.context.engine.accountData.sendMessageShortcut(peerId: peerId, id: shortcutId) + + /*self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in guard let self else { return } @@ -9494,7 +9485,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, nil) var messages: [EnqueueMessage] = [] - for message in shortcut.messages { + do { + let message = shortcut.topMessage var attributes: [MessageAttribute] = [] let entities = generateTextEntities(message.text, enabledTypes: .all) if !entities.isEmpty { @@ -9515,7 +9507,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G )) } - self.sendMessages(messages) + self.sendMessages(messages)*/ }, openEditShortcuts: { [weak self] in guard let self else { return diff --git a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift index 59f7290162..a7c7370f72 100644 --- a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift +++ b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift @@ -11,6 +11,7 @@ import QuickReplyNameAlertController extension ChatControllerImpl { func editChat() { + //TODO:localize if case let .customChatContents(customChatContents) = self.subject, case let .quickReplyMessageInput(currentValue) = customChatContents.kind { var completion: ((String?) -> Void)? let alertController = quickReplyNameAlertController( @@ -34,40 +35,25 @@ extension ChatControllerImpl { return } - let _ = (self.context.engine.accountData.shortcutMessages() + let _ = (self.context.engine.accountData.shortcutMessageList() |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] shortcutMessages in + |> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in guard let self else { alertController?.dismissAnimated() return } - var shortcuts = shortcutMessages.shortcuts - guard let index = shortcuts.firstIndex(where: { $0.shortcut.lowercased() == currentValue }) else { - alertController?.dismissAnimated() - return - } - - if shortcuts.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { + if shortcutMessageList.items.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { if let contentNode = alertController?.contentNode as? QuickReplyNameAlertContentNode { contentNode.setErrorText(errorText: "Shortcut with that name already exists") } } else { - shortcuts[index] = QuickReplyMessageShortcut( - id: shortcuts[index].id, - shortcut: value, - messages: shortcuts[index].messages - ) - let updatedShortcutMessages = QuickReplyMessageShortcutsState(shortcuts: shortcuts) - self.context.engine.accountData.updateShortcutMessages(state: updatedShortcutMessages) - - self.chatTitleView?.titleContent = .custom("/\(value)", nil, false) + self.chatTitleView?.titleContent = .custom("\(value)", nil, false) + alertController?.dismissAnimated() if case let .customChatContents(customChatContents) = self.subject { customChatContents.quickReplyUpdateShortcut(value: value) } - - alertController?.dismissAnimated() } }) } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index faa7465e86..50e8551bb6 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -590,15 +590,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } else if case .customChatContents = chatLocation { if case let .customChatContents(customChatContents) = subject { - source = .custom( - messages: customChatContents.messages - |> map { messages in - return (messages, 0, false) - }, - messageId: nil, - quote: nil, - loadMore: nil - ) + source = .customView(historyView: customChatContents.historyView) } else { source = .custom(messages: .single(([], 0, false)), messageId: nil, quote: nil, loadMore: nil) } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift index 1e6576268b..d1445389c0 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift @@ -31,7 +31,7 @@ func chatShareToSavedMessagesAdditionalView(_ chatController: ChatControllerImpl return } - let _ = (chatController.context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: chatController.context.account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 45, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: []) + let _ = (chatController.context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: chatController.context.account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 45, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []) |> map { view, _, _ -> [EngineMessage.Id] in let messageIds = correlationIds.compactMap { correlationId in return chatController.context.engine.messages.synchronouslyLookupCorrelationId(correlationId: correlationId) diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Sources/ChatEmptyNode.swift index 013b467cba..6b98a57487 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatEmptyNode.swift @@ -703,8 +703,22 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { var maxWidth: CGFloat = size.width var centerText = false - if case .customChatContents = interfaceState.subject { + + var insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) + var imageSpacing: CGFloat = 12.0 + var titleSpacing: CGFloat = 4.0 + + if case let .customChatContents(customChatContents) = interfaceState.subject { maxWidth = min(240.0, maxWidth) + + switch customChatContents.kind { + case .greetingMessageInput, .awayMessageInput: + break + case .quickReplyMessageInput: + insets.top = 10.0 + imageSpacing = 5.0 + titleSpacing = 5.0 + } } if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { @@ -713,7 +727,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) - self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Empty Chat/Cloud"), color: serviceColor.primaryText) + var iconName = "Chat/Empty Chat/Cloud" let titleString: String let strings: [String] @@ -735,6 +749,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC "Add messages that are automatically sent when you are off." ] case let .quickReplyMessageInput(shortcut): + iconName = "Chat/Empty Chat/QuickReplies" //TODO:localize centerText = false titleString = "New Quick Reply" @@ -753,6 +768,8 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC ] } + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: serviceColor.primaryText) + self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText) let lines: [NSAttributedString] = strings.map { @@ -781,11 +798,6 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC } } - let insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) - - let imageSpacing: CGFloat = 12.0 - let titleSpacing: CGFloat = 4.0 - var contentWidth: CGFloat = 100.0 var contentHeight: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 3b7ddd21f0..bbd4200418 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -794,35 +794,35 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto //self.debugInfo = true self.messageProcessingManager.process = { [weak context] messageIds in - context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds, clientId: clientId.with { $0 }) + context?.account.viewTracker.updateViewCountForMessageIds(messageIds: Set(messageIds.map(\.messageId)), clientId: clientId.with { $0 }) } self.messageWithReactionsProcessingManager.process = { [weak context] messageIds in - context?.account.viewTracker.updateReactionsForMessageIds(messageIds: messageIds) + context?.account.viewTracker.updateReactionsForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.seenLiveLocationProcessingManager.process = { [weak context] messageIds in - context?.account.viewTracker.updateSeenLiveLocationForMessageIds(messageIds: messageIds) + context?.account.viewTracker.updateSeenLiveLocationForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.unsupportedMessageProcessingManager.process = { [weak context] messageIds in context?.account.viewTracker.updateUnsupportedMediaForMessageIds(messageIds: messageIds) } self.refreshMediaProcessingManager.process = { [weak context] messageIds in - context?.account.viewTracker.refreshSecretMediaMediaForMessageIds(messageIds: messageIds) + context?.account.viewTracker.refreshSecretMediaMediaForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.refreshStoriesProcessingManager.process = { [weak context] messageIds in - context?.account.viewTracker.refreshStoriesForMessageIds(messageIds: messageIds) + context?.account.viewTracker.refreshStoriesForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.translationProcessingManager.process = { [weak self, weak context] messageIds in if let context = context, let toLang = self?.toLang { - let _ = translateMessageIds(context: context, messageIds: Array(messageIds), toLang: toLang).startStandalone() + let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), toLang: toLang).startStandalone() } } self.messageMentionProcessingManager.process = { [weak self, weak context] messageIds in if let strongSelf = self { if strongSelf.canReadHistoryValue { - context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds) + context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } else { - strongSelf.messageIdsScheduledForMarkAsSeen.formUnion(messageIds) + strongSelf.messageIdsScheduledForMarkAsSeen.formUnion(messageIds.map(\.messageId)) } } } @@ -832,9 +832,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto return } if strongSelf.canReadHistoryValue && !strongSelf.suspendReadingReactions && !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { - strongSelf.context.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: messageIds) + strongSelf.context.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } else { - strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.formUnion(messageIds) + strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.formUnion(messageIds.map(\.messageId)) } } @@ -842,7 +842,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto guard let strongSelf = self else { return } - strongSelf.context.account.viewTracker.updatedExtendedMediaForMessageIds(messageIds: messageIds) + strongSelf.context.account.viewTracker.updatedExtendedMediaForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.preloadPages = false @@ -1269,6 +1269,53 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto return (ChatHistoryViewUpdate.HistoryView(view: MessageHistoryView(tag: nil, namespaces: .all, entries: messages.reversed().map { MessageHistoryEntry(message: $0, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)) }, holeEarlier: hasMore, holeLater: false, isLoading: false), type: .Generic(type: version > 0 ? ViewUpdateType.Generic : ViewUpdateType.Initial), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: nil, buttonKeyboardMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil), id: 0), version, nil, nil) } + } else if case let .customView(historyView) = self.source { + historyViewUpdate = combineLatest(queue: .mainQueue(), + self.chatHistoryLocationPromise.get(), + self.ignoreMessagesInTimestampRangePromise.get() + ) + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.0 != rhs.0 { + return false + } + if lhs.1 != rhs.1 { + return false + } + return true + }) + |> mapToSignal { _ in + return historyView + } + |> map { view, update in + let version = currentViewVersion.modify({ value in + if let value = value { + return value + 1 + } else { + return 0 + } + })! + + return ( + ChatHistoryViewUpdate.HistoryView( + view: view, + type: .Generic(type: update), + scrollPosition: nil, + flashIndicators: false, + originalScrollPosition: nil, + initialData: ChatHistoryCombinedInitialData( + initialData: nil, + buttonKeyboardMessage: nil, + cachedData: nil, + cachedDataMessages: nil, + readStateData: nil + ), + id: 0 + ), + version, + nil, + nil + ) + } } else { historyViewUpdate = combineLatest(queue: .mainQueue(), self.chatHistoryLocationPromise.get(), @@ -2415,7 +2462,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var messageIdsWithViewCount: [MessageId] = [] var messageIdsWithLiveLocation: [MessageId] = [] - var messageIdsWithUnsupportedMedia: [MessageId] = [] + var messageIdsWithUnsupportedMedia: [MessageAndThreadId] = [] var messageIdsWithRefreshMedia: [MessageId] = [] var messageIdsWithRefreshStories: [MessageId] = [] var messageIdsWithUnseenPersonalMention: [MessageId] = [] @@ -2516,7 +2563,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } if contentRequiredValidation { - messageIdsWithUnsupportedMedia.append(message.id) + messageIdsWithUnsupportedMedia.append(MessageAndThreadId(messageId: message.id, threadId: message.threadId)) } if mediaRequiredValidation { messageIdsWithRefreshMedia.append(message.id) @@ -2712,41 +2759,41 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } if !messageIdsWithViewCount.isEmpty { - self.messageProcessingManager.add(messageIdsWithViewCount) + self.messageProcessingManager.add(messageIdsWithViewCount.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !messageIdsWithLiveLocation.isEmpty { - self.seenLiveLocationProcessingManager.add(messageIdsWithLiveLocation) + self.seenLiveLocationProcessingManager.add(messageIdsWithLiveLocation.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !messageIdsWithUnsupportedMedia.isEmpty { self.unsupportedMessageProcessingManager.add(messageIdsWithUnsupportedMedia) } if !messageIdsWithRefreshMedia.isEmpty { - self.refreshMediaProcessingManager.add(messageIdsWithRefreshMedia) + self.refreshMediaProcessingManager.add(messageIdsWithRefreshMedia.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !messageIdsWithRefreshStories.isEmpty { - self.refreshStoriesProcessingManager.add(messageIdsWithRefreshStories) + self.refreshStoriesProcessingManager.add(messageIdsWithRefreshStories.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !messageIdsWithUnseenPersonalMention.isEmpty { - self.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention) + self.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !messageIdsWithUnseenReactions.isEmpty { - self.unseenReactionsProcessingManager.add(messageIdsWithUnseenReactions) + self.unseenReactionsProcessingManager.add(messageIdsWithUnseenReactions.map { MessageAndThreadId(messageId: $0, threadId: nil) }) if self.canReadHistoryValue && !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { let _ = self.displayUnseenReactionAnimations(messageIds: messageIdsWithUnseenReactions) } } if !messageIdsWithPossibleReactions.isEmpty { - self.messageWithReactionsProcessingManager.add(messageIdsWithPossibleReactions) + self.messageWithReactionsProcessingManager.add(messageIdsWithPossibleReactions.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !downloadableResourceIds.isEmpty { let _ = markRecentDownloadItemsAsSeen(postbox: self.context.account.postbox, items: downloadableResourceIds).startStandalone() } if !messageIdsWithInactiveExtendedMedia.isEmpty { - self.extendedMediaProcessingManager.update(messageIdsWithInactiveExtendedMedia) + self.extendedMediaProcessingManager.update(Set(messageIdsWithInactiveExtendedMedia.map { MessageAndThreadId(messageId: $0, threadId: nil) })) } if !messageIdsToTranslate.isEmpty { - self.translationProcessingManager.add(messageIdsToTranslate) + self.translationProcessingManager.add(messageIdsToTranslate.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } if !visibleAdOpaqueIds.isEmpty { for opaqueId in visibleAdOpaqueIds { @@ -3650,7 +3697,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let visibleNewIncomingReactionMessageIds = strongSelf.displayUnseenReactionAnimations(messageIds: messageIds) if !visibleNewIncomingReactionMessageIds.isEmpty { - strongSelf.unseenReactionsProcessingManager.add(visibleNewIncomingReactionMessageIds) + strongSelf.unseenReactionsProcessingManager.add(visibleNewIncomingReactionMessageIds.map { MessageAndThreadId(messageId: $0, threadId: nil) }) } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 0d41d323aa..4f03d1f2c4 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -711,7 +711,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - if Namespaces.Message.allScheduled.contains(message.id.namespace) || message.id.peerId.isReplies { + if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.id.peerId.isReplies { canReply = false canPin = false } else if messages[0].flags.intersection([.Failed, .Unsent]).isEmpty { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 002df47b9a..fb7f548a16 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -218,11 +218,11 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee )) }) } - var shortcuts: Signal<[QuickReplyMessageShortcut], NoError> = .single([]) + var shortcuts: Signal<[ShortcutMessageList.Item], NoError> = .single([]) if peer is TelegramUser { - shortcuts = context.engine.accountData.shortcutMessages() - |> map { shortcuts -> [QuickReplyMessageShortcut] in - return shortcuts.shortcuts.filter { item in + shortcuts = context.engine.accountData.shortcutMessageList() + |> map { shortcutMessageList -> [ShortcutMessageList.Item] in + return shortcutMessageList.items.filter { item in return item.shortcut.hasPrefix(normalizedQuery) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift b/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift index a7eaa0f5b2..a29c5cb6f1 100644 --- a/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift +++ b/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift @@ -4,30 +4,30 @@ import Postbox import SwiftSignalKit final class ChatMessageThrottledProcessingManager { - private let queue = Queue() + private let queue = Queue.mainQueue() private let delay: Double private let submitInterval: Double? - var process: ((Set) -> Void)? + var process: ((Set) -> Void)? private var timer: SwiftSignalKit.Timer? - private var processedList: [MessageId] = [] - private var processed: [MessageId: Double] = [:] - private var buffer = Set() + private var processedList: [MessageAndThreadId] = [] + private var processed: [MessageAndThreadId: Double] = [:] + private var buffer = Set() init(delay: Double = 1.0, submitInterval: Double? = nil) { self.delay = delay self.submitInterval = submitInterval } - func setProcess(process: @escaping (Set) -> Void) { + func setProcess(process: @escaping (Set) -> Void) { self.queue.async { self.process = process } } - func add(_ messageIds: [MessageId]) { + func add(_ messageIds: [MessageAndThreadId]) { self.queue.async { let timestamp = CFAbsoluteTimeGetCurrent() @@ -76,13 +76,13 @@ final class ChatMessageThrottledProcessingManager { final class ChatMessageVisibleThrottledProcessingManager { - private let queue = Queue() + private let queue = Queue.mainQueue() private let interval: Double - private var currentIds = Set() + private var currentIds = Set() - var process: ((Set) -> Void)? + var process: ((Set) -> Void)? private let timer: SwiftSignalKit.Timer @@ -107,13 +107,13 @@ final class ChatMessageVisibleThrottledProcessingManager { self.timer.invalidate() } - func setProcess(process: @escaping (Set) -> Void) { + func setProcess(process: @escaping (Set) -> Void) { self.queue.async { self.process = process } } - func update(_ ids: Set) { + func update(_ ids: Set) { self.queue.async { if self.currentIds != ids { self.currentIds = ids diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index e7f2bca16b..6ecb5d7693 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -195,7 +195,7 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { filterData: nil, index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: context.account.peerId, namespace: 0, id: 0), timestamp: 0))), content: .peer(ChatListItemContent.PeerData( - messages: shortcut.messages.first.flatMap({ [$0] }) ?? [], + messages: [shortcut.topMessage], peer: renderedPeer, threadInfo: nil, combinedReadState: nil, @@ -375,7 +375,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { } } case let .shortcut(shortcut): - interfaceInteraction.sendShortcut(shortcut) + interfaceInteraction.sendShortcut(shortcut.id) } }, openEditShortcuts: { [weak self] in guard let self, let interfaceInteraction = self.interfaceInteraction else { diff --git a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift index c6e029ff09..61f6f39189 100644 --- a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift @@ -284,7 +284,10 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { } let titleString: String - if canEditMedia { + if let message, message.id.namespace == Namespaces.Message.QuickReplyCloud { + //TODO:localize + titleString = "Edit Quick Reply" + } else if canEditMedia { titleString = isPhoto ? self.strings.Conversation_EditingPhotoPanelTitle : self.strings.Conversation_EditingCaptionPanelTitle } else { titleString = self.strings.Conversation_EditingMessagePanelTitle diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index f0b44c24c0..6830e72ed3 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -549,8 +549,10 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { let namespaces: MessageIdNamespaces if Namespaces.Message.allScheduled.contains(anchor.id.namespace) { namespaces = .just(Namespaces.Message.allScheduled) + } else if Namespaces.Message.allQuickReply.contains(anchor.id.namespace) { + namespaces = .just(Namespaces.Message.allQuickReply) } else { - namespaces = .not(Namespaces.Message.allScheduled) + namespaces = .not(Namespaces.Message.allNonRegular) } switch anchor { diff --git a/submodules/TelegramUI/Sources/PrefetchManager.swift b/submodules/TelegramUI/Sources/PrefetchManager.swift index c404dc2487..be6dafa68a 100644 --- a/submodules/TelegramUI/Sources/PrefetchManager.swift +++ b/submodules/TelegramUI/Sources/PrefetchManager.swift @@ -174,16 +174,16 @@ private final class PrefetchManagerInnerImpl { if case .full = automaticDownload { if let image = media as? TelegramMediaImage { - context.fetchDisposable.set(messageMediaImageInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), image: image, resource: resource._asResource(), userInitiated: false, priority: priority, storeToDownloadsPeerId: nil).startStrict()) + context.fetchDisposable.set(messageMediaImageInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false, threadId: nil), image: image, resource: resource._asResource(), userInitiated: false, priority: priority, storeToDownloadsPeerId: nil).startStrict()) } else if let _ = media as? TelegramMediaWebFile { //strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).startStrict()) } else if let file = media as? TelegramMediaFile { - let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), file: file, userInitiated: false, priority: priority) + let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false, threadId: nil), file: file, userInitiated: false, priority: priority) context.fetchDisposable.set(fetchSignal.startStrict()) } } else if case .prefetch = automaticDownload, mediaItem.media.peer.id.namespace != Namespaces.Peer.SecretChat { if let file = media as? TelegramMediaFile, let _ = file.size { - context.fetchDisposable.set(preloadVideoResource(postbox: self.account.postbox, userLocation: .peer(mediaItem.media.index.id.peerId), userContentType: MediaResourceUserContentType(file: file), resourceReference: FileMediaReference.message(message: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), media: file).resourceReference(file.resource), duration: 4.0).startStrict()) + context.fetchDisposable.set(preloadVideoResource(postbox: self.account.postbox, userLocation: .peer(mediaItem.media.index.id.peerId), userContentType: MediaResourceUserContentType(file: file), resourceReference: FileMediaReference.message(message: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false, threadId: nil), media: file).resourceReference(file.resource), duration: 4.0).startStrict()) } } } From 50339b3c4c62d9e5f08aa644fb459946fc0f7fd8 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 23 Feb 2024 23:12:28 +0400 Subject: [PATCH 7/7] [WIP] Business --- .../Sources/Node/ChatListItem.swift | 101 +++++++++++++++-- .../Sources/Node/ChatListNode.swift | 10 +- .../ChatPresentationInterfaceState.swift | 2 + .../ChatItemGalleryFooterContentNode.swift | 5 + .../SyncCore/SyncCore_Namespaces.swift | 7 ++ .../TelegramEngineAccountData.swift | 8 ++ .../Messages/QuickReplyMessages.swift | 2 +- .../TelegramEngine/Messages/TimeZones.swift | 106 ++++++++++++++++++ .../Sources/ChatListHeaderComponent.swift | 21 +++- .../Sources/PlainButtonComponent.swift | 7 +- ...aticBusinessMessageListItemComponent.swift | 3 +- .../QuickReplyEmptyStateComponent.swift | 2 +- .../Sources/QuickReplySetupScreen.swift | 10 +- .../Sources/BusinessHoursSetupScreen.swift | 79 +++++++++++-- .../Sources/TimezoneSelectionScreenNode.swift | 52 +++++---- .../ChatControllerNavigateToMessage.swift | 4 + .../Sources/ChatControllerNode.swift | 2 +- .../Sources/ChatHistoryEntriesForView.swift | 4 +- .../Sources/ChatHistoryListNode.swift | 25 ++++- .../ChatInterfaceStateAccessoryPanels.swift | 19 +++- .../ChatInterfaceStateContextMenus.swift | 6 +- .../CommandChatInputContextPanelNode.swift | 3 +- 22 files changed, 411 insertions(+), 67 deletions(-) create mode 100644 submodules/TelegramCore/Sources/TelegramEngine/Messages/TimeZones.swift diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 6d79803d62..6af430ca12 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -94,11 +94,13 @@ public enum ChatListItemContent { public var commandPrefix: String? public var searchQuery: String? public var messageCount: Int? + public var hideSeparator: Bool - public init(commandPrefix: String?, searchQuery: String?, messageCount: Int?) { + public init(commandPrefix: String?, searchQuery: String?, messageCount: Int?, hideSeparator: Bool) { self.commandPrefix = commandPrefix self.searchQuery = searchQuery self.messageCount = messageCount + self.hideSeparator = hideSeparator } } @@ -1166,6 +1168,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var currentItemHeight: CGFloat? let forwardedIconNode: ASImageNode let textNode: TextNodeWithEntities + var trailingTextBadgeNode: TextNode? + var trailingTextBadgeBackground: UIImageView? var dustNode: InvisibleInkDustNode? let inputActivitiesNode: ChatListInputActivitiesNode let dateNode: TextNode @@ -1437,6 +1441,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.textNode = TextNodeWithEntities() self.textNode.textNode.isUserInteractionEnabled = false self.textNode.textNode.displaysAsynchronously = true + self.textNode.textNode.layer.anchorPoint = CGPoint() self.inputActivitiesNode = ChatListInputActivitiesNode() self.inputActivitiesNode.isUserInteractionEnabled = false @@ -1823,6 +1828,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let dateLayout = TextNode.asyncLayout(self.dateNode) let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) + let makeTrailingTextBadgeLayout = TextNode.asyncLayout(self.trailingTextBadgeNode) let titleLayout = TextNode.asyncLayout(self.titleNode) let authorLayout = self.authorNode.asyncLayout() let makeMeasureLayout = TextNode.asyncLayout(self.measureNode) @@ -2073,7 +2079,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil { avatarDiameter = 40.0 - avatarLeftInset = 18.0 + avatarDiameter + avatarLeftInset = 17.0 + avatarDiameter } else { if item.interaction.isInlineMode { avatarLeftInset = 12.0 @@ -2666,7 +2672,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { topIndex = peerData.messages.first?.index } if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { - if let messageCount = customMessageListData.messageCount { + if let messageCount = customMessageListData.messageCount, customMessageListData.commandPrefix == nil { dateText = "\(messageCount)" } else { dateText = " " @@ -2990,9 +2996,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let (authorLayout, authorApply) = authorLayout(item.context, rawContentWidth - badgeSize, item.presentationData.theme, effectiveAuthorTitle, forumThreads) + var textBottomRightCutout: CGFloat = 0.0 + + let trailingTextBadgeInsets = UIEdgeInsets(top: 2.0 - UIScreenPixel, left: 5.0, bottom: 2.0 - UIScreenPixel, right: 5.0) + var trailingTextBadgeLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil, let messageCount = customMessageListData.messageCount, messageCount > 1 { + let trailingText: String + //TODO:localize + trailingText = "+\(messageCount - 1) MORE" + let trailingAttributedText = NSAttributedString(string: trailingText, font: Font.regular(12.0), textColor: theme.messageTextColor) + let (layout, apply) = makeTrailingTextBadgeLayout(TextNodeLayoutArguments(attributedString: trailingAttributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + trailingTextBadgeLayoutAndApply = (layout, apply) + textBottomRightCutout += layout.size.width + 4.0 + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right + } + var textCutout: TextNodeCutout? - if !textLeftCutout.isZero { - textCutout = TextNodeCutout(topLeft: CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: nil) + if !textLeftCutout.isZero || !textBottomRightCutout.isZero { + textCutout = TextNodeCutout(topLeft: textLeftCutout.isZero ? nil : CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: textBottomRightCutout.isZero ? nil : CGSize(width: textBottomRightCutout, height: 10.0)) } var textMaxWidth = rawContentWidth - badgeSize @@ -3552,6 +3572,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let _ = measureApply() let _ = dateApply() + var currentTextSnapshotView: UIView? + if transition.isAnimated, let currentItem, currentItem.editing != item.editing, strongSelf.textNode.textNode.cachedLayout?.linesRects() != textLayout.linesRects() { + if let textSnapshotView = strongSelf.textNode.textNode.view.snapshotContentTree() { + textSnapshotView.layer.anchorPoint = CGPoint() + currentTextSnapshotView = textSnapshotView + strongSelf.textNode.textNode.view.superview?.insertSubview(textSnapshotView, aboveSubview: strongSelf.textNode.textNode.view) + textSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSnapshotView] _ in + textSnapshotView?.removeFromSuperview() + }) + strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } + } + let _ = textApply(TextNodeWithEntities.Arguments( context: item.context, cache: item.interaction.animationCache, @@ -3572,7 +3605,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var dateFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size) - if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil { + if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil, customMessageListData.commandPrefix == nil { dateFrame.origin.x -= 10.0 let dateDisclosureIconView: UIImageView @@ -3818,6 +3851,60 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.authorNode.assignParentNode(parentNode: nil) } + if let currentTextSnapshotView { + transition.updatePosition(layer: currentTextSnapshotView.layer, position: textNodeFrame.origin) + } + + if let trailingTextBadgeLayoutAndApply { + let badgeSize = CGSize(width: trailingTextBadgeLayoutAndApply.0.size.width + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right, height: trailingTextBadgeLayoutAndApply.0.size.height + trailingTextBadgeInsets.top + trailingTextBadgeInsets.bottom - UIScreenPixel) + + var badgeFrame: CGRect + if textLayout.numberOfLines > 1 { + badgeFrame = CGRect(origin: CGPoint(x: textLayout.trailingLineWidth, y: textNodeFrame.height - 3.0 - badgeSize.height), size: badgeSize) + } else { + let firstLineFrame = textLayout.linesRects().first ?? CGRect(origin: CGPoint(), size: textNodeFrame.size) + badgeFrame = CGRect(origin: CGPoint(x: 0.0, y: firstLineFrame.height + 5.0), size: badgeSize) + } + + if badgeFrame.origin.x + badgeFrame.width >= textNodeFrame.width - 2.0 - 10.0 { + badgeFrame.origin.x = textNodeFrame.width - 2.0 - badgeFrame.width + } + + let trailingTextBadgeBackground: UIImageView + if let current = strongSelf.trailingTextBadgeBackground { + trailingTextBadgeBackground = current + } else { + trailingTextBadgeBackground = UIImageView(image: tagBackgroundImage) + strongSelf.trailingTextBadgeBackground = trailingTextBadgeBackground + strongSelf.textNode.textNode.view.addSubview(trailingTextBadgeBackground) + } + trailingTextBadgeBackground.tintColor = theme.pinnedItemBackgroundColor.mixedWith(theme.unreadBadgeInactiveBackgroundColor, alpha: 0.1) + + trailingTextBadgeBackground.frame = badgeFrame + + let trailingTextBadgeFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + trailingTextBadgeInsets.left, y: badgeFrame.minY + trailingTextBadgeInsets.top), size: trailingTextBadgeLayoutAndApply.0.size) + let trailingTextBadgeNode = trailingTextBadgeLayoutAndApply.1() + if strongSelf.trailingTextBadgeNode !== trailingTextBadgeNode { + strongSelf.trailingTextBadgeNode?.removeFromSupernode() + strongSelf.trailingTextBadgeNode = trailingTextBadgeNode + + strongSelf.textNode.textNode.addSubnode(trailingTextBadgeNode) + + trailingTextBadgeNode.layer.anchorPoint = CGPoint() + } + + trailingTextBadgeNode.frame = trailingTextBadgeFrame + } else { + if let trailingTextBadgeNode = strongSelf.trailingTextBadgeNode { + strongSelf.trailingTextBadgeNode = nil + trailingTextBadgeNode.removeFromSupernode() + } + if let trailingTextBadgeBackground = strongSelf.trailingTextBadgeBackground { + strongSelf.trailingTextBadgeBackground = nil + trailingTextBadgeBackground.removeFromSuperview() + } + } + if !itemTags.isEmpty { let itemTagListFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.maxY - 12.0), size: CGSize(width: contentRect.width, height: 20.0)) @@ -4127,7 +4214,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { - if customMessageListData.messageCount != nil { + if customMessageListData.hideSeparator { strongSelf.separatorNode.isHidden = true } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 7306140ff1..82c0cfe1fc 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -433,7 +433,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL }, requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, displayAsTopicList: peerEntry.displayAsTopicList, - tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) + tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, @@ -811,7 +811,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL }, requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, displayAsTopicList: peerEntry.displayAsTopicList, - tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) + tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, @@ -4206,7 +4206,11 @@ func hideChatListContacts(context: AccountContext) { let _ = ApplicationSpecificNotice.setDisplayChatListContacts(accountManager: context.sharedContext.accountManager).startStandalone() } -func chatListItemTags(accountPeerId: EnginePeer.Id, peer: EnginePeer?, isUnread: Bool, isMuted: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> [ChatListItemContent.Tag] { +func chatListItemTags(location: ChatListControllerLocation, accountPeerId: EnginePeer.Id, peer: EnginePeer?, isUnread: Bool, isMuted: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> [ChatListItemContent.Tag] { + if case .chatList = location { + } else { + return [] + } guard let chatListFilters, !chatListFilters.isEmpty else { return [] } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 35ccabb4ea..75406cdcf6 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -1202,6 +1202,8 @@ public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bo } else { return false } + } else if case .customChatContents = state.chatLocation { + return true } else { return false } diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index f1262a1777..62da3c1a45 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -816,6 +816,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false) { self.currentMessage = message + var displayInfo = displayInfo + if Namespaces.Message.allNonRegular.contains(message.id.namespace) { + displayInfo = false + } + var canDelete: Bool var canShare = !message.containsSecretMedia diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 70d9a83496..958ac84482 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -285,6 +285,7 @@ private enum PreferencesKeyValues: Int32 { case didCacheSavedMessageTagsPrefix = 34 case displaySavedChatsAsTopics = 35 case shortcutMessages = 37 + case timezoneList = 38 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -474,6 +475,12 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.shortcutMessages.rawValue) return key } + + public static func timezoneList() -> ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.timezoneList.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 c2986ca224..46cfb507e1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -185,5 +185,13 @@ public extension TelegramEngine { public func sendMessageShortcut(peerId: EnginePeer.Id, id: Int32) { let _ = _internal_sendMessageShortcut(account: self.account, peerId: peerId, id: id).startStandalone() } + + public func cachedTimeZoneList() -> Signal { + return _internal_cachedTimeZoneList(account: self.account) + } + + public func keepCachedTimeZoneListUpdated() -> Signal { + return _internal_keepCachedTimeZoneListUpdated(account: self.account) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index e8d6cbab5e..a6b052fb01 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -229,7 +229,7 @@ func _internal_shortcutMessageList(account: Account) -> Signal Bool { + if lhs === rhs { + return true + } + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.utcOffset != rhs.utcOffset { + return false + } + return true + } + } + + public let items: [Item] + public let hashValue: Int32 + + public init(items: [Item], hashValue: Int32) { + self.items = items + self.hashValue = hashValue + } + + public static func ==(lhs: TimeZoneList, rhs: TimeZoneList) -> Bool { + if lhs === rhs { + return true + } + if lhs.items != rhs.items { + return false + } + if lhs.hashValue != rhs.hashValue { + return false + } + return true + } +} + +func _internal_cachedTimeZoneList(account: Account) -> Signal { + let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.timezoneList()])) + return account.postbox.combinedView(keys: [viewKey]) + |> map { views -> TimeZoneList? in + guard let view = views.views[viewKey] as? PreferencesView else { + return nil + } + guard let value = view.values[PreferencesKeys.timezoneList()]?.get(TimeZoneList.self) else { + return nil + } + return value + } +} + +func _internal_keepCachedTimeZoneListUpdated(account: Account) -> Signal { + let updateSignal = _internal_cachedTimeZoneList(account: account) + |> take(1) + |> mapToSignal { list -> Signal in + return account.network.request(Api.functions.help.getTimezonesList(hash: list?.hashValue ?? 0)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + + return account.postbox.transaction { transaction in + switch result { + case let .timezonesList(timezones, hash): + var items: [TimeZoneList.Item] = [] + for item in timezones { + switch item { + case let .timezone(id, name, utcOffset): + items.append(TimeZoneList.Item(id: id, title: name, utcOffset: utcOffset)) + } + } + transaction.setPreferencesEntry(key: PreferencesKeys.timezoneList(), value: PreferencesEntry(TimeZoneList(items: items, hashValue: hash))) + case .timezonesListNotModified: + break + } + } + |> ignoreValues + } + } + + return updateSignal +} diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index e41442d0ae..ae0f24162d 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -267,7 +267,9 @@ public final class ChatListHeaderComponent: Component { } func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: Transition) -> CGSize { - self.titleView.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + let titleText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + let titleTextUpdated = self.titleView.attributedText != titleText + self.titleView.attributedText = titleText let titleSize = self.titleView.updateLayout(CGSize(width: 100.0, height: 44.0)) self.accessibilityLabel = title @@ -287,7 +289,12 @@ public final class ChatListHeaderComponent: Component { transition.setPosition(view: self.arrowView, position: arrowFrame.center) transition.setBounds(view: self.arrowView, bounds: CGRect(origin: CGPoint(), size: arrowFrame.size)) - transition.setFrame(view: self.titleView, frame: CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)) + let titleFrame = CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize) + if titleTextUpdated { + self.titleView.frame = titleFrame + } else { + transition.setFrame(view: self.titleView, frame: titleFrame) + } return CGSize(width: iconOffset + arrowSize.width + iconSpacing + titleSize.width, height: availableSize.height) } @@ -479,7 +486,9 @@ public final class ChatListHeaderComponent: Component { transition.setPosition(view: self.titleScaleContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) transition.setBounds(view: self.titleScaleContainer, bounds: CGRect(origin: self.titleScaleContainer.bounds.origin, size: size)) - self.titleTextView.attributedText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) + let titleText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) + let titleTextUpdated = self.titleTextView.attributedText != titleText + self.titleTextView.attributedText = titleText let buttonSpacing: CGFloat = 8.0 @@ -616,7 +625,11 @@ public final class ChatListHeaderComponent: Component { let titleTextSize = self.titleTextView.updateLayout(CGSize(width: remainingWidth, height: size.height)) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleTextSize.width) / 2.0) + sideContentWidth, y: floor((size.height - titleTextSize.height) / 2.0)), size: titleTextSize) - transition.setFrame(view: self.titleTextView, frame: titleFrame) + if titleTextUpdated { + self.titleTextView.frame = titleFrame + } else { + transition.setFrame(view: self.titleTextView, frame: titleFrame) + } if let titleComponent = content.titleComponent { var titleContentTransition = transition diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index cb3ee96e76..f91488ec89 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -227,7 +227,12 @@ public final class PlainButtonComponent: Component { } let contentFrame = CGRect(origin: CGPoint(x: component.contentInsets.left + floor((size.width - component.contentInsets.left - component.contentInsets.right - contentSize.width) * 0.5), y: component.contentInsets.top + floor((size.height - component.contentInsets.top - component.contentInsets.bottom - contentSize.height) * 0.5)), size: contentSize) - contentTransition.setPosition(view: contentView, position: CGPoint(x: contentFrame.minX + contentFrame.width * contentView.layer.anchorPoint.x, y: contentFrame.minY + contentFrame.height * contentView.layer.anchorPoint.y)) + let contentPosition = CGPoint(x: contentFrame.minX + contentFrame.width * contentView.layer.anchorPoint.x, y: contentFrame.minY + contentFrame.height * contentView.layer.anchorPoint.y) + if !component.animateContents && (abs(contentView.center.x - contentPosition.x) <= 2.0 && abs(contentView.center.y - contentPosition.y) <= 2.0){ + contentView.center = contentPosition + } else { + contentTransition.setPosition(view: contentView, position: contentPosition) + } if component.animateContents { contentTransition.setBounds(view: contentView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size)) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index 123a1d0087..ad53e21b11 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -238,7 +238,8 @@ final class GreetingMessageListItemComponent: Component { customMessageListData: ChatListItemContent.CustomMessageListData( commandPrefix: nil, searchQuery: nil, - messageCount: component.count + messageCount: component.count, + hideSeparator: true ) )), editing: false, diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift index a145ec7be2..f0a9d10fca 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplyEmptyStateComponent.swift @@ -109,7 +109,7 @@ final class QuickReplyEmptyStateComponent: Component { transition: .immediate, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: "WriteEmoji"), - loop: false + loop: true )), environment: {}, containerSize: CGSize(width: 120.0, height: 120.0) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 010f4c6fca..595483c17d 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -250,7 +250,8 @@ final class QuickReplySetupScreenComponent: Component { customMessageListData: ChatListItemContent.CustomMessageListData( commandPrefix: "/\(item.shortcut)", searchQuery: nil, - messageCount: nil + messageCount: item.totalCount, + hideSeparator: false ) )), editing: isEditing, @@ -744,13 +745,14 @@ final class QuickReplySetupScreenComponent: Component { tabsNodeIsSearch: false, accessoryPanelContainer: nil, accessoryPanelContainerHeight: 0.0, - activateSearch: { [weak self] searchContentNode in + activateSearch: { [weak self] _ in guard let self else { return } - self.isSearchDisplayControllerActive = true - self.state?.updated(transition: .spring(duration: 0.4)) + let _ = self + //self.isSearchDisplayControllerActive = true + //self.state?.updated(transition: .spring(duration: 0.4)) }, openStatusSetup: { _ in }, diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift index fb5ffcfabc..5f8839c110 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift @@ -82,11 +82,14 @@ final class BusinessHoursSetupScreenComponent: Component { } var timezoneId: String - var days: [Day] + private(set) var days: [Day] + private(set) var intersectingDays = Set() init(timezoneId: String, days: [Day]) { self.timezoneId = timezoneId self.days = days + + self.validate() } init(businessHours: TelegramBusinessHours) { @@ -107,6 +110,19 @@ final class BusinessHoursSetupScreenComponent: Component { }) } } + + self.validate() + } + + mutating func validate() { + self.intersectingDays.removeAll() + + + } + + mutating func update(days: [Day]) { + self.days = days + self.validate() } func asBusinessHours() throws -> TelegramBusinessHours { @@ -165,6 +181,10 @@ final class BusinessHoursSetupScreenComponent: Component { private var showHours: Bool = false private var daysState = DaysState(timezoneId: "", days: []) + private var timeZoneList: TimeZoneList? + private var timezonesDisposable: Disposable? + private var keepTimezonesUpdatedDisposable: Disposable? + override init(frame: CGRect) { self.scrollView = ScrollView() self.scrollView.showsVerticalScrollIndicator = true @@ -191,6 +211,8 @@ final class BusinessHoursSetupScreenComponent: Component { } deinit { + self.timezonesDisposable?.dispose() + self.keepTimezonesUpdatedDisposable?.dispose() } func scrollToTop() { @@ -210,6 +232,21 @@ final class BusinessHoursSetupScreenComponent: Component { } catch let error { let _ = error //TODO:localize + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "Business hours are intersecting. Reset?", actions: [ + TextAlertAction(type: .genericAction, title: "Cancel", action: { + }), + TextAlertAction(type: .defaultAction, title: "Reset", action: { [weak self] in + guard let self else { + return + } + let _ = self + complete() + }) + ]), in: .window(.root)) + return false } } else { @@ -271,10 +308,20 @@ final class BusinessHoursSetupScreenComponent: Component { } else { self.showHours = false self.daysState.timezoneId = TimeZone.current.identifier - self.daysState.days = (0 ..< 7).map { _ in + self.daysState.update(days: (0 ..< 7).map { _ in return Day(ranges: []) - } + }) } + + self.timezonesDisposable = (component.context.engine.accountData.cachedTimeZoneList() + |> deliverOnMainQueue).start(next: { [weak self] timeZoneList in + guard let self else { + return + } + self.timeZoneList = timeZoneList + self.state?.updated(transition: .immediate) + }) + self.keepTimezonesUpdatedDisposable = component.context.engine.accountData.keepCachedTimeZoneListUpdated().startStrict() } let environment = environment[EnvironmentType.self].value @@ -508,11 +555,13 @@ final class BusinessHoursSetupScreenComponent: Component { return } if dayIndex < self.daysState.days.count { - if self.daysState.days[dayIndex].ranges == nil { - self.daysState.days[dayIndex].ranges = [] + var days = self.daysState.days + if days[dayIndex].ranges == nil { + days[dayIndex].ranges = [] } else { - self.daysState.days[dayIndex].ranges = nil + days[dayIndex].ranges = nil } + self.daysState.update(days: days) } self.state?.updated(transition: .immediate) })), @@ -529,7 +578,9 @@ final class BusinessHoursSetupScreenComponent: Component { return } if self.daysState.days[dayIndex] != day { - self.daysState.days[dayIndex] = day + var days = self.daysState.days + days[dayIndex] = day + self.daysState.update(days: days) self.state?.updated(transition: .immediate) } } @@ -569,6 +620,18 @@ final class BusinessHoursSetupScreenComponent: Component { daysContentHeight += daysSectionSize.height daysContentHeight += sectionSpacing + let timezoneValueText: String + if let timeZoneList = self.timeZoneList { + if let item = timeZoneList.items.first(where: { $0.id == self.daysState.timezoneId }) { + timezoneValueText = item.title + } else { + timezoneValueText = TimeZone(identifier: self.daysState.timezoneId)?.localizedName(for: .shortStandard, locale: Locale.current) ?? " " + } + } else { + //TODO:localize + timezoneValueText = "Loading..." + } + let timezoneSectionSize = self.timezoneSection.update( transition: transition, component: AnyComponent(ListSectionComponent( @@ -588,7 +651,7 @@ final class BusinessHoursSetupScreenComponent: Component { )), icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: TimeZone(identifier: self.daysState.timezoneId)?.localizedName(for: .shortStandard, locale: Locale.current) ?? self.daysState.timezoneId, + string: timezoneValueText, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemSecondaryTextColor )), diff --git a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift index 8656ab32ea..07cc080138 100644 --- a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift +++ b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift @@ -58,8 +58,7 @@ private func preparedLanguageListSearchContainerTransition(presentationData: Pre } private final class TimezoneListSearchContainerNode: SearchDisplayControllerContentNode { - private let timezoneData: TimezoneData - + private let timeZoneList: TimeZoneList private let dimNode: ASDisplayNode private let listNode: ListView @@ -78,8 +77,8 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont return true } - init(context: AccountContext, timezoneData: TimezoneData, action: @escaping (String) -> Void) { - self.timezoneData = timezoneData + init(context: AccountContext, timeZoneList: TimeZoneList, action: @escaping (String) -> Void) { + self.timeZoneList = timeZoneList let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData @@ -102,16 +101,18 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont self.addSubnode(self.dimNode) self.addSubnode(self.listNode) + let querySplitCharacterSet: CharacterSet = CharacterSet(charactersIn: " /.+") + let foundItems = self.searchQuery.get() - |> mapToSignal { query -> Signal<[TimezoneData.Item]?, NoError> in + |> mapToSignal { query -> Signal<[TimeZoneList.Item]?, NoError> in if let query, !query.isEmpty { let query = query.lowercased() - return .single(timezoneData.items.filter { item in + return .single(timeZoneList.items.filter { item in if item.id.lowercased().hasPrefix(query) { return true } - if item.title.lowercased().split(separator: " ").contains(where: { $0.hasPrefix(query) }) { + if item.title.lowercased().components(separatedBy: querySplitCharacterSet).contains(where: { $0.hasPrefix(query) }) { return true } @@ -132,7 +133,7 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont for item in items { entries.append(TimezoneListEntry( id: item.id, - offset: item.offset, + offset: Int(item.utcOffset), title: item.title )) } @@ -301,7 +302,7 @@ final class TimezoneSelectionScreenNode: ViewControllerTracingNode { private let requestDeactivateSearch: () -> Void private let present: (ViewController, Any?) -> Void private let push: (ViewController) -> Void - private let timezoneData: TimezoneData + private var timeZoneList: TimeZoneList? private var didSetReady = false let _ready = ValuePromise() @@ -331,28 +332,32 @@ final class TimezoneSelectionScreenNode: ViewControllerTracingNode { return presentationData.strings.VoiceOver_ScrollStatus(row, count).string } - let timezoneData = TimezoneData() - self.timezoneData = timezoneData - super.init() self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.addSubnode(self.listNode) let previousEntriesHolder = Atomic<([TimezoneListEntry], PresentationTheme, PresentationStrings)?>(value: nil) - self.listDisposable = (self.presentationDataValue.get() - |> deliverOnMainQueue).start(next: { [weak self] presentationData in + self.listDisposable = (combineLatest(queue: .mainQueue(), + self.presentationDataValue.get(), + context.engine.accountData.cachedTimeZoneList() + ) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, timeZoneList in guard let strongSelf = self else { return } - + + strongSelf.timeZoneList = timeZoneList + var entries: [TimezoneListEntry] = [] - for item in timezoneData.items { - entries.append(TimezoneListEntry( - id: item.id, - offset: item.offset, - title: item.title - )) + if let timeZoneList { + for item in timeZoneList.items { + entries.append(TimezoneListEntry( + id: item.id, + offset: Int(item.utcOffset), + title: item.title + )) + } } entries.sort() @@ -447,8 +452,11 @@ final class TimezoneSelectionScreenNode: ViewControllerTracingNode { guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else { return } + guard let timeZoneList = self.timeZoneList else { + return + } - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: TimezoneListSearchContainerNode(context: self.context, timezoneData: self.timezoneData, action: self.action), inline: true, cancel: { [weak self] in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: TimezoneListSearchContainerNode(context: self.context, timeZoneList: timeZoneList, action: self.action), inline: true, cancel: { [weak self] in self?.requestDeactivateSearch() }) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index ef430bc3d9..092847181d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -104,6 +104,9 @@ extension ChatControllerImpl { if case let .peer(peerId) = self.chatLocation, messageLocation.peerId == peerId, !isPinnedMessages, !isScheduledMessages { forceInCurrentChat = true } + if case .customChatContents = self.chatLocation { + forceInCurrentChat = true + } if isPinnedMessages, let messageId = messageLocation.messageId { let _ = (combineLatest( @@ -205,6 +208,7 @@ extension ChatControllerImpl { if case let .id(_, params) = messageLocation { quote = params.quote.flatMap { quote in (string: quote.string, offset: quote.offset) } } + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition) if delayCompletion { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 50e8551bb6..74e5ff6665 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1883,7 +1883,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { overlayNavigationBar.updateLayout(size: barFrame.size, transition: transition) } - var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left) + var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top + 6.0, right: containerInsets.left) let listScrollIndicatorInsets = UIEdgeInsets(top: containerInsets.bottom + inputPanelsHeight, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left) var childContentInsets: UIEdgeInsets = containerInsets diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 332d725dd7..060de3e3ec 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -546,12 +546,12 @@ func chatHistoryEntriesForView( let message = Message( stableId: UInt32.max - 1001 - UInt32(i), stableVersion: 0, - id: MessageId(peerId: context.account.peerId, namespace: Namespaces.Message.Local, id: 123 - Int32(i)), + id: MessageId(peerId: context.account.peerId, namespace: Namespaces.Message.Local, id: Int32.max - 100 - Int32(i)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, - timestamp: Int32(i), + timestamp: -Int32(i), flags: [.Incoming], tags: [], globalTags: [], diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index bbd4200418..055439b3f5 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1283,10 +1283,15 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } return true }) - |> mapToSignal { _ in + |> mapToSignal { location, _ -> Signal<((MessageHistoryView, ViewUpdateType), ChatHistoryLocationInput?), NoError> in return historyView + |> map { historyView in + return (historyView, location) + } } - |> map { view, update in + |> map { viewAndUpdate, location in + let (view, update) = viewAndUpdate + let version = currentViewVersion.modify({ value in if let value = value { return value + 1 @@ -1295,11 +1300,21 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } })! + var scrollPositionValue: ChatHistoryViewScrollPosition? + if let location { + switch location.content { + case let .Scroll(subject, _, _, scrollPosition, animated, highlight): + scrollPositionValue = .index(subject: subject, position: scrollPosition, directionHint: .Up, animated: animated, highlight: highlight, displayLink: false) + default: + break + } + } + return ( ChatHistoryViewUpdate.HistoryView( view: view, type: .Generic(type: update), - scrollPosition: nil, + scrollPosition: scrollPositionValue, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData( @@ -1309,10 +1324,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto cachedDataMessages: nil, readStateData: nil ), - id: 0 + id: location?.id ?? 0 ), version, - nil, + location, nil ) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index fe7d320454..ac201532fe 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -76,12 +76,21 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS replyPanelNode.interfaceInteraction = interfaceInteraction replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return replyPanelNode - } else if let peerId = chatPresentationInterfaceState.chatLocation.peerId { - let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: peerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) - panelNode.interfaceInteraction = interfaceInteraction - return panelNode } else { - return nil + var chatPeerId: EnginePeer.Id? + if let peerId = chatPresentationInterfaceState.chatLocation.peerId { + chatPeerId = peerId + } else if case .customChatContents = chatPresentationInterfaceState.chatLocation { + chatPeerId = context.account.peerId + } + + if let chatPeerId { + let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: chatPeerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) + panelNode.interfaceInteraction = interfaceInteraction + return panelNode + } else { + return nil + } } } else { return nil diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 4f03d1f2c4..510ae5cca2 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -271,6 +271,10 @@ private func canViewReadStats(message: Message, participantCount: Int?, isMessag } func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, accountPeerId: PeerId) -> Bool { + if case .customChatContents = chatPresentationInterfaceState.chatLocation { + return true + } + guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else { return false } @@ -338,7 +342,7 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS case .replyThread: canReply = true case .customChatContents: - canReply = false + canReply = true } return canReply } diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index 6ecb5d7693..4b6334f13e 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -220,7 +220,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { customMessageListData: ChatListItemContent.CustomMessageListData( commandPrefix: "/\(shortcut.shortcut)", searchQuery: command.searchQuery.flatMap { "/\($0)"}, - messageCount: nil + messageCount: shortcut.totalCount, + hideSeparator: false ) )), editing: false,