From acf905a098fb6b9cb83d5f1f5347795dbd6c5f5f Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 20 Apr 2024 19:37:16 +0400 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 11 +- ...orizationSequenceCodeEntryController.swift | 4 + ...ationSequenceCodeEntryControllerNode.swift | 17 +- .../AuthorizationSequenceController.swift | 2 +- ...tionSequencePhoneEntryControllerNode.swift | 2 +- .../ChatMessageLiveLocationTimerNode.swift | 6 +- .../Sources/LocationLiveListItem.swift | 11 +- .../Sources/SolidRoundedButtonNode.swift | 8 +- .../LocationBroadcastActionSheetItem.swift | 2 +- .../Peers/ChannelAdminEventLogs.swift | 23 +- .../Sources/Utils/MessageUtils.swift | 4 +- .../ChatMessageActionBubbleContentNode.swift | 21 ++ .../Sources/ChatMessageBubbleItemNode.swift | 26 +- .../Sources/ChatMessageItem.swift | 1 + .../Chat/ChatRecentActionsController/BUILD | 1 + .../ChatRecentActionsControllerNode.swift | 131 ++++++- .../ChatRecentActionsHistoryTransition.swift | 132 ++++++- .../AddBirthdayIcon.imageset/Contents.json | 12 + .../addbirthday_30.pdf | 353 ++++++++++++++++++ .../Sources/SharedAccountContext.swift | 44 ++- .../Sources/ChatTextInputAttributes.swift | 1 + .../Sources/GenerateTextEntities.swift | 1 + .../Sources/StringWithAppliedEntities.swift | 5 + .../Sources/TelegramAttributes.swift | 1 + 24 files changed, 772 insertions(+), 47 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/addbirthday_30.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b1e01538bb..a86291a5fe 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12065,8 +12065,8 @@ Sorry for the inconvenience."; "Login.EnterPhrasePlaceholder" = "Enter Phrase from SMS"; "Login.EnterPhraseHint" = "You can copy and paste the phrase here."; -"Login.BackToWord" = "Back to entering word"; -"Login.BackToPhrase" = "Back to entering phrase"; +"Login.BackToWord" = "Back to entering the word"; +"Login.BackToPhrase" = "Back to entering the phrase"; "Login.WrongPhraseError" = "Incorrect, please try again."; "Login.Paste" = "Paste"; @@ -12095,3 +12095,10 @@ Sorry for the inconvenience."; "ChannelProfile.ToastAboutCopied" = "Copied to clipboard."; "Conversation.ContextMenuBanFull" = "Ban"; + +"Channel.AdminLog.MessageManyDeleted" = "%1$@ deleted %2$@ from %3$@"; +"Channel.AdminLog.MessageManyDeletedMore" = "%1$@ deleted %2$@ from %3$@ %4$@"; +"Channel.AdminLog.MessageManyDeleted.Messages_1" = "%@ message"; +"Channel.AdminLog.MessageManyDeleted.Messages_any" = "%@ messages"; +"Channel.AdminLog.MessageManyDeleted.ShowAll" = "show all"; +"Channel.AdminLog.MessageManyDeleted.HideAll" = "hide all"; diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift index 0909886408..63810ccb4d 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift @@ -168,6 +168,10 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { self.controllerNode.animateSuccess() } + public func selectIncorrectPart() { + self.controllerNode.selectIncorrectPart() + } + public func animateError(text: String) { self.hapticFeedback.error() self.controllerNode.animateError(text: text) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift index b2f2228f7c..4624659fe7 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift @@ -405,7 +405,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, appleSignInAllowed: Bool, hasPreviousCode: Bool, previousIsPhrase: Bool) { self.codeType = codeType - self.phoneNumber = number + self.phoneNumber = number.replacingOccurrences(of: " ", with: "\u{00A0}").replacingOccurrences(of: "-", with: "\u{2011}") self.email = email self.hasPreviousCode = hasPreviousCode self.previousIsPhrase = previousIsPhrase @@ -881,6 +881,21 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } } + func selectIncorrectPart() { + switch self.codeType { + case .word: + self.textField.textField.selectAll(nil) + case let .phrase(startsWith): + if let startsWith, let fromPosition = self.textField.textField.position(from: self.textField.textField.beginningOfDocument, offset: startsWith.count + 1) { + self.textField.textField.selectedTextRange = self.textField.textField.textRange(from: fromPosition, to: self.textField.textField.endOfDocument) + } else { + self.textField.textField.selectAll(nil) + } + default: + break + } + } + func animateError(text: String) { let errorOriginY: CGFloat let errorOriginOffset: CGFloat diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 52e028b8ae..9498135420 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -476,7 +476,6 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth guard let strongSelf = self else { return } - controller?.inProgress = false switch result { case let .signUp(data): if let (termsOfService, explicit) = termsOfService, explicit { @@ -543,6 +542,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth switch type { case .word, .phrase: text = strongSelf.presentationData.strings.Login_WrongPhraseError + controller.selectIncorrectPart() default: text = strongSelf.presentationData.strings.Login_WrongCodeError } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift index c6560db3d8..e32f74f78a 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift @@ -684,7 +684,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { tokenString = tokenString.replacingOccurrences(of: "/", with: "_") let urlString = "tg://login?token=\(tokenString)" let _ = (qrCode(string: urlString, color: .black, backgroundColor: .white, icon: .none) - |> deliverOnMainQueue).startStrict(next: { _, generate in + |> deliverOnMainQueue).startStandalone(next: { _, generate in guard let strongSelf = self else { return } diff --git a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift index 8151cb0a2f..c8d8e43912 100644 --- a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift +++ b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift @@ -83,7 +83,11 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode { let intRemaining = Int32(remaining) if intRemaining > 60 * 60 { let hours = Int32(round(remaining / (60.0 * 60.0))) - string = strings.Map_LiveLocationShortHour("\(hours)").string + if hours > 99 { + string = "99+" + } else { + string = strings.Map_LiveLocationShortHour("\(hours)").string + } } else { let minutes = Int32(round(remaining / (60.0))) string = "\(minutes)" diff --git a/submodules/LocationUI/Sources/LocationLiveListItem.swift b/submodules/LocationUI/Sources/LocationLiveListItem.swift index 98faeaacab..fd78d3417f 100644 --- a/submodules/LocationUI/Sources/LocationLiveListItem.swift +++ b/submodules/LocationUI/Sources/LocationLiveListItem.swift @@ -321,7 +321,14 @@ final class LocationLiveListItemNode: ListViewItemNode { } let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if currentTimestamp < item.message.timestamp + liveBroadcastingTimeout { + let remainingTime: Int32 + if liveBroadcastingTimeout == liveLocationIndefinitePeriod { + remainingTime = liveLocationIndefinitePeriod + } else { + remainingTime = max(0, item.message.timestamp + liveBroadcastingTimeout - currentTimestamp) + } + + if remainingTime > 0 { let timerNode: ChatMessageLiveLocationTimerNode if let current = strongSelf.timerNode { timerNode = current @@ -331,7 +338,7 @@ final class LocationLiveListItemNode: ListViewItemNode { strongSelf.timerNode = timerNode } let timerSize = CGSize(width: 28.0, height: 28.0) - timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: Double(item.message.timestamp), timeout: Double(liveBroadcastingTimeout), strings: item.presentationData.strings) + timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: Double(item.message.timestamp), timeout: Int32(liveBroadcastingTimeout) == liveLocationIndefinitePeriod ? -1.0 : Double(liveBroadcastingTimeout), strings: item.presentationData.strings) timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: 14.0), size: timerSize) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index fa80ebebbd..7074cd2454 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -838,8 +838,12 @@ public final class SolidRoundedButtonNode: ASDisplayNode { let diameter: CGFloat = self.buttonHeight - 22.0 let progressFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(buttonOffset + (buttonWidth - diameter) / 2.0), y: floorToScreenPixels((self.buttonHeight - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter)) progressNode.frame = progressFrame - progressNode.image = generateIndefiniteActivityIndicatorImage(color: self.theme.foregroundColor, diameter: diameter, lineWidth: 3.0) - + + if !self.isEnabled, let disabledForegroundColor = self.theme.disabledForegroundColor { + progressNode.image = generateIndefiniteActivityIndicatorImage(color: disabledForegroundColor, diameter: diameter, lineWidth: 3.0) + } else { + progressNode.image = generateIndefiniteActivityIndicatorImage(color: self.theme.foregroundColor, diameter: diameter, lineWidth: 3.0) + } self.addSubnode(progressNode) } diff --git a/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift b/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift index ab090a4db7..30bdac8506 100644 --- a/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift +++ b/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift @@ -107,7 +107,7 @@ public class LocationBroadcastActionSheetItemNode: ActionSheetItemNode { self.avatarNode.setPeer(context: item.context, theme: (item.context.sharedContext.currentPresentationData.with { $0 }).theme, peer: EnginePeer(item.peer)) - self.timerNode.update(backgroundColor: self.theme.controlAccentColor.withAlphaComponent(0.4), foregroundColor: self.theme.controlAccentColor, textColor: self.theme.controlAccentColor, beginTimestamp: item.beginTimestamp, timeout: item.timeout, strings: item.strings) + self.timerNode.update(backgroundColor: self.theme.controlAccentColor.withAlphaComponent(0.4), foregroundColor: self.theme.controlAccentColor, textColor: self.theme.controlAccentColor, beginTimestamp: item.beginTimestamp, timeout: Int32(item.timeout) == liveLocationIndefinitePeriod ? -1.0 : item.timeout, strings: item.strings) } public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 4efffb59c8..d113a071bc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -186,7 +186,28 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net if let threadId = message.threadId, let threadInfo = transaction.getMessageHistoryThreadInfo(peerId: message.id.peerId, threadId: threadId) { associatedThreadInfo = postbox.seedConfiguration.decodeMessageThreadInfo(threadInfo.data) } - return locallyRenderedMessage(message: message, peers: peers, associatedThreadInfo: associatedThreadInfo) + var associatedMessages: SimpleDictionary = SimpleDictionary() + if let replyAttribute = message.attributes.first(where: { $0 is ReplyMessageAttribute }) as? ReplyMessageAttribute { + var foundDeletedReplyMessage = false + for event in apiEvents { + switch event { + case let .channelAdminLogEvent(_, _, _, apiAction): + switch apiAction { + case let .channelAdminLogEventActionDeleteMessage(apiMessage): + if let messageId = apiMessage.id(namespace: Namespaces.Message.Cloud), messageId == replyAttribute.messageId, let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let replyMessage = locallyRenderedMessage(message: message, peers: peers, associatedThreadInfo: associatedThreadInfo) { + associatedMessages[replyMessage.id] = replyMessage + foundDeletedReplyMessage = true + } + default: + break + } + } + } + if !foundDeletedReplyMessage, let replyMessage = transaction.getMessage(replyAttribute.messageId) { + associatedMessages[replyMessage.id] = replyMessage + } + } + return locallyRenderedMessage(message: message, peers: peers, associatedThreadInfo: associatedThreadInfo, associatedMessages: associatedMessages) } for event in apiEvents { diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index ce7d685b42..e234e47eff 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -211,7 +211,7 @@ func messagesIdsGroupedByPeerId(_ ids: [MessageAndThreadId]) -> [PeerAndThreadId return dict } -func locallyRenderedMessage(message: StoreMessage, peers: [PeerId: Peer], associatedThreadInfo: Message.AssociatedThreadInfo? = nil) -> Message? { +func locallyRenderedMessage(message: StoreMessage, peers: [PeerId: Peer], associatedThreadInfo: Message.AssociatedThreadInfo? = nil, associatedMessages: SimpleDictionary = SimpleDictionary()) -> Message? { guard case let .Id(id) = message.id else { return nil } @@ -264,7 +264,7 @@ func locallyRenderedMessage(message: StoreMessage, peers: [PeerId: Peer], associ let second = UInt32(hashValue & 0xffffffff) let stableId = first &+ second - return Message(stableId: stableId, stableVersion: 0, id: id, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: message.threadId, timestamp: message.timestamp, flags: MessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, customTags: [], forwardInfo: forwardInfo, author: author, text: message.text, attributes: message.attributes, media: message.media, peers: messagePeers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: associatedThreadInfo, associatedStories: [:]) + return Message(stableId: stableId, stableVersion: 0, id: id, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: message.threadId, timestamp: message.timestamp, flags: MessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, customTags: [], forwardInfo: forwardInfo, author: author, text: message.text, attributes: message.attributes, media: message.media, peers: messagePeers, associatedMessages: associatedMessages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: associatedThreadInfo, associatedStories: [:]) } func locallyRenderedMessage(message: StoreMessage, peers: AccumulatedPeers, associatedThreadInfo: Message.AssociatedThreadInfo? = nil) -> Message? { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift index c8ca0fefd5..a0767c21fb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift @@ -27,6 +27,8 @@ private func attributedServiceMessageString(theme: ChatPresentationThemeData, st } public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { + public var expandHighlightingNode: LinkHighlightingNode? + public let labelNode: TextNodeWithEntities private var dustNode: InvisibleInkDustNode? public var backgroundNode: WallpaperBubbleBackgroundNode? @@ -364,6 +366,25 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) + + if var rect = strongSelf.labelNode.textNode.cachedLayout?.allAttributeRects(name: TelegramTextAttributes.Button).first?.1 { + rect = rect.insetBy(dx: -2.0, dy: 2.0).offsetBy(dx: 0.0, dy: 1.0 - UIScreenPixel) + let highlightNode: LinkHighlightingNode + if let current = strongSelf.expandHighlightingNode { + highlightNode = current + } else { + highlightNode = LinkHighlightingNode(color: UIColor(rgb: 0x000000, alpha: 0.1)) + highlightNode.outerRadius = 7.5 + strongSelf.insertSubnode(highlightNode, belowSubnode: strongSelf.labelNode.textNode) + strongSelf.expandHighlightingNode = highlightNode + } + highlightNode.frame = strongSelf.labelNode.textNode.frame + highlightNode.updateRects([rect]) + } else { + strongSelf.expandHighlightingNode?.removeFromSupernode() + strongSelf.expandHighlightingNode = nil + } + if let (offset, image) = backgroundMaskImage { if item.context.sharedContext.energyUsageSettings.fullTranslucency { if strongSelf.backgroundNode == nil { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index ea196dfb69..36f9827a9a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -304,6 +304,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ case let .eventLogPreviousLink(previousMessage): result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false + case .eventLogGroupedMessages: + break } } @@ -2483,7 +2485,25 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var maxContentWidth: CGFloat = headerSize.width var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))? - if let replyMarkup = replyMarkup, !item.presentationData.isPreview { + if let additionalContent = item.additionalContent, case let .eventLogGroupedMessages(messages, hasButton) = additionalContent, hasButton { + let (minWidth, buttonsLayout) = actionButtonsLayout( + item.context, + item.presentationData.theme, + item.presentationData.chatBubbleCorners, + item.presentationData.strings, + item.controllerInteraction.presentationContext.backgroundNode, + ReplyMarkupMessageAttribute( + rows: [ + ReplyMarkupRow( + buttons: [ReplyMarkupButton(title: "Show \(messages.count - 1) More Messages", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: MemoryBuffer(data: Data())))] + ) + ], + flags: [], + placeholder: nil + ), item.message, maximumNodeWidth) + maxContentWidth = max(maxContentWidth, minWidth) + actionButtonsFinalize = buttonsLayout + } else if let replyMarkup = replyMarkup, !item.presentationData.isPreview { let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, item.controllerInteraction.presentationContext.backgroundNode, replyMarkup, item.message, maximumNodeWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout @@ -2966,6 +2986,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return } + if let additionalContent = item.additionalContent, case .eventLogGroupedMessages = additionalContent { + applyInfo.setInvertOffsetDirection() + } + let themeUpdated = strongSelf.appliedItem?.presentationData.theme.theme !== item.presentationData.theme.theme let previousContextFrame = strongSelf.mainContainerNode.frame strongSelf.mainContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift index ac098658ff..2a856c7905 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift @@ -83,6 +83,7 @@ public enum ChatMessageItemAdditionalContent { case eventLogPreviousMessage(Message) case eventLogPreviousDescription(Message) case eventLogPreviousLink(Message) + case eventLogGroupedMessages([Message], Bool) } public enum ChatMessageMerge: Int32 { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD index 3fee2bfabd..e826f371a2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatControllerInteraction", "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + "//submodules/TelegramUI/Components/Chat/ChatMessageItem", "//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl", "//submodules/TelegramUI/Components/Chat/ChatNavigationButton", "//submodules/TelegramUI/Components/Chat/ChatLoadingNode", diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 1db3236edd..741ea3e9fd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -76,6 +76,13 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private let navigationActionDisposable = MetaDisposable() + private var expandedDeletedMessages = Set() { + didSet { + self.expandedDeletedMessagesPromise.set(self.expandedDeletedMessages) + } + } + private let expandedDeletedMessagesPromise = ValuePromise>(Set()) + private var isLoading: Bool = false { didSet { if self.isLoading != oldValue { @@ -88,6 +95,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private let eventLogContext: ChannelAdminEventLogContext private var enqueuedTransitions: [(ChatRecentActionsHistoryTransition, Bool)] = [] + private var searchResultsState: (String, [MessageIndex])? private var historyDisposable: Disposable? private let resolvePeerByNameDisposable = MetaDisposable() @@ -162,6 +170,16 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { return false } for entry in state.entries { + if entry.entry.headerStableId == message.stableId { + if case let .deleteMessage(message) = entry.entry.event.action { + if strongSelf.expandedDeletedMessages.contains(message.id) { + strongSelf.expandedDeletedMessages.remove(message.id) + } else { + strongSelf.expandedDeletedMessages.insert(message.id) + } + return true + } + } if entry.entry.stableId == message.stableId { switch entry.entry.event.action { case let .changeStickerPack(_, new): @@ -277,12 +295,28 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, updateMessageReaction: { _, _, _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in - }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in + }, navigateToMessage: { [weak self] fromId, toId, params in + guard let self else { + return + } + + context.sharedContext.navigateToChat(accountId: self.context.account.id, peerId: toId.peerId, messageId: toId) + }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { [weak self] peerId, threadId, _ in if let context = self?.context, let navigationController = self?.getNavigationController() { let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, keepStack: .always).startStandalone() } - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url in + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false + }, requestMessageActionCallback: { [weak self] messageId, _, _, _ in + guard let self else { + return + } + if self.expandedDeletedMessages.contains(messageId) { + self.expandedDeletedMessages.remove(messageId) + } else { + self.expandedDeletedMessages.insert(messageId) + } + }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url in self?.openUrl(url.url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { @@ -616,15 +650,36 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } let previousView = Atomic<[ChatRecentActionsEntry]?>(value: nil) + let previousExpandedDeletedMessages = Atomic>(value: Set()) let chatThemes = self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager) - let historyViewTransition = combineLatest(historyViewUpdate, self.chatPresentationDataPromise.get(), chatThemes) - |> mapToQueue { update, chatPresentationData, chatThemes -> Signal in - let processedView = chatRecentActionsEntries(entries: update.0, presentationData: chatPresentationData) + let historyViewTransition = combineLatest( + historyViewUpdate, + self.chatPresentationDataPromise.get(), + chatThemes, + self.expandedDeletedMessagesPromise.get() + ) + |> mapToQueue { [weak self] update, chatPresentationData, chatThemes, expandedDeletedMessages -> Signal in + let processedView = chatRecentActionsEntries(entries: update.0, presentationData: chatPresentationData, expandedDeletedMessages: expandedDeletedMessages) let previous = previousView.swap(processedView) + let previousExpandedDeletedMessages = previousExpandedDeletedMessages.swap(expandedDeletedMessages) - return .single(chatRecentActionsHistoryPreparedTransition(from: previous ?? [], to: processedView, type: update.2, canLoadEarlier: update.1, displayingResults: update.3, context: context, peer: peer, controllerInteraction: controllerInteraction, chatThemes: chatThemes)) + var updateType = update.2 + if previousExpandedDeletedMessages.count != expandedDeletedMessages.count { + updateType = .generic + } + + var searchResultsState: (String, [MessageIndex])? + if update.3, let query = self?.filter.query { + searchResultsState = (query, processedView.compactMap { entry in + return entry.entry.event.action.messageId.flatMap { MessageIndex(id: $0, timestamp: entry.entry.event.date) } + }) + } else { + searchResultsState = nil + } + + return .single(chatRecentActionsHistoryPreparedTransition(from: previous ?? [], to: processedView, type: updateType, canLoadEarlier: update.1, displayingResults: update.3, context: context, peer: peer, controllerInteraction: controllerInteraction, chatThemes: chatThemes, searchResultsState: searchResultsState)) } let appliedTransition = historyViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in @@ -698,15 +753,20 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.backgroundNode.updateLayout(size: self.backgroundNode.bounds.size, displayMode: .aspectFill, transition: transition) let intrinsicPanelHeight: CGFloat = 47.0 - let panelHeight = intrinsicPanelHeight + cleanInsets.bottom - transition.updateFrame(node: self.panelBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) + var panelHeight = intrinsicPanelHeight + cleanInsets.bottom + var panelOffset: CGFloat = panelHeight + if insets.bottom > cleanInsets.bottom { + panelHeight = intrinsicPanelHeight + panelOffset = insets.bottom + panelHeight + } + transition.updateFrame(node: self.panelBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelOffset), size: CGSize(width: layout.size.width, height: panelHeight))) self.panelBackgroundNode.update(size: self.panelBackgroundNode.bounds.size, transition: transition) - transition.updateFrame(node: self.panelSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.panelSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelOffset), size: CGSize(width: layout.size.width, height: UIScreenPixel))) let infoButtonSize = CGSize(width: 56.0, height: intrinsicPanelHeight) - transition.updateFrame(node: self.panelButtonNode, frame: CGRect(origin: CGPoint(x: insets.left + infoButtonSize.width, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width - insets.left - insets.right - infoButtonSize.width * 2.0, height: intrinsicPanelHeight))) + transition.updateFrame(node: self.panelButtonNode, frame: CGRect(origin: CGPoint(x: insets.left + infoButtonSize.width, y: layout.size.height - panelOffset), size: CGSize(width: layout.size.width - insets.left - insets.right - infoButtonSize.width * 2.0, height: intrinsicPanelHeight))) - transition.updateFrame(node: self.panelInfoButtonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - insets.right - infoButtonSize.width, y: layout.size.height - panelHeight), size: infoButtonSize)) + transition.updateFrame(node: self.panelInfoButtonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - insets.right - infoButtonSize.width, y: layout.size.height - panelOffset), size: infoButtonSize)) self.visibleAreaInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: panelHeight, right: 0.0) @@ -714,14 +774,14 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { transition.updatePosition(node: self.listNode, position: CGRect(origin: CGPoint(), size: layout.size).center) transition.updateFrame(node: self.loadingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - loadingNode.updateLayout(size: layout.size, insets: insets, transition: transition) + self.loadingNode.updateLayout(size: layout.size, insets: insets, transition: transition) let emptyFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight - panelHeight)) transition.updateFrame(node: self.emptyNode, frame: emptyFrame) self.emptyNode.update(rect: emptyFrame, within: layout.size) self.emptyNode.updateLayout(presentationData: self.chatPresentationData, backgroundNode: self.backgroundNode, size: emptyFrame.size, transition: transition) - let contentBottomInset: CGFloat = panelHeight + 4.0 + let contentBottomInset: CGFloat = panelOffset + 4.0 let listInsets = UIEdgeInsets(top: contentBottomInset, left: layout.safeInsets.right, bottom: insets.top, right: layout.safeInsets.left) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) @@ -763,6 +823,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { let displayingResults = transition.displayingResults let isEmpty = transition.isEmpty let displayEmptyNode = isEmpty && displayingResults + + self.searchResultsState = transition.searchResultsState + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: ChatRecentActionsListOpaqueState(entries: transition.filteredEntries, canLoadEarlier: transition.canLoadEarlier), completion: { [weak self] _ in if let strongSelf = self { if displayEmptyNode != strongSelf.listNode.isHidden { @@ -805,6 +868,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { isEmpty = true } strongSelf.isEmptyUpdated(isEmpty) + + strongSelf.updateItemNodesSearchTextHighlightStates() } }) } else { @@ -834,6 +899,29 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { func updateSearchQuery(_ query: String) { self.filter = self.filter.withQuery(query.isEmpty ? nil : query) self.eventLogContext.setFilter(self.filter) + + self.updateItemNodesSearchTextHighlightStates() + } + + func updateItemNodesSearchTextHighlightStates() { + var searchString: String? + var resultsMessageIndices: [MessageIndex]? = nil + if let (query, indices) = self.searchResultsState { + searchString = query + resultsMessageIndices = indices + } + if searchString != self.controllerInteraction?.searchTextHighightState?.0 || resultsMessageIndices != self.controllerInteraction?.searchTextHighightState?.1 { + var searchTextHighightState: (String, [MessageIndex])? + if let searchString = searchString, let resultsMessageIndices = resultsMessageIndices { + searchTextHighightState = (searchString, resultsMessageIndices) + } + self.controllerInteraction?.searchTextHighightState = searchTextHighightState + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateSearchTextHighlightState() + } + } + } } func updateFilter(events: AdminLogEventsFlags, adminPeerIds: [PeerId]?) { @@ -1294,3 +1382,20 @@ final class ChatMessageContextLocationContentSource: ContextLocationContentSourc return ContextControllerLocationViewInfo(location: self.location, contentAreaInScreenSpace: UIScreen.main.bounds) } } + +extension AdminLogEventAction { + var messageId: MessageId? { + switch self { + case let .editMessage(_, new): + return new.id + case let .deleteMessage(message): + return message.id + case let .pollStopped(message): + return message.id + case let .sendMessage(message): + return message.id + default: + return nil + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index 37a06dddc2..be8cb83834 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -8,6 +8,7 @@ import MergeLists import AccountContext import ChatControllerInteraction import ChatHistoryEntry +import ChatMessageItem import ChatMessageItemImpl import TextFormat @@ -95,6 +96,8 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let id: ChatRecentActionsEntryId let presentationData: ChatPresentationData let entry: ChannelAdminEventLogEntry + let subEntries: [ChannelAdminEventLogEntry] + let isExpanded: Bool? static func ==(lhs: ChatRecentActionsEntry, rhs: ChatRecentActionsEntry) -> Bool { if lhs.id != rhs.id { @@ -106,6 +109,12 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if lhs.entry != rhs.entry { return false } + if lhs.subEntries != rhs.subEntries { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } return true } @@ -524,13 +533,48 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] - - appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageDeleted(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in - if index == 0, let author = author { - return [.TextMention(peerId: author.id)] + + let authorName = author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" + + if !self.subEntries.isEmpty { + var peers: [EnginePeer] = [] + var existingPeerIds = Set() + for entry in self.subEntries { + if case let .deleteMessage(message) = entry.event.action, let author = message.author { + guard !existingPeerIds.contains(author.id) else { + continue + } + peers.append(EnginePeer(author)) + existingPeerIds.insert(author.id) + } } - return [] - }, to: &text, entities: &entities) + let peerNames = peers.map { $0.compactDisplayTitle }.joined(separator: ", ") + let messagesString = self.presentationData.strings.Channel_AdminLog_MessageManyDeleted_Messages(Int32(self.subEntries.count)) + + let fullText: PresentationStrings.FormattedString + if let isExpanded = self.isExpanded { + let moreText = (isExpanded ? self.presentationData.strings.Channel_AdminLog_MessageManyDeleted_HideAll : self.presentationData.strings.Channel_AdminLog_MessageManyDeleted_ShowAll).replacingOccurrences(of: " ", with: "\u{00A0}") + fullText = self.presentationData.strings.Channel_AdminLog_MessageManyDeletedMore(authorName, messagesString, peerNames, moreText) + } else { + fullText = self.presentationData.strings.Channel_AdminLog_MessageManyDeleted(authorName, messagesString, peerNames) + } + + appendAttributedText(text: fullText, generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } else if index == 3 { + return [.Custom(type: ApplicationSpecificEntityType.Button)] + } + return [] + }, to: &text, entities: &entities) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageDeleted(authorName), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) @@ -567,8 +611,24 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if let peer = self.entry.peers[message.id.peerId] { peers[peer.id] = peer } - let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: message.id.id), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: message.threadId, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: message.associatedMessages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: message.associatedThreadInfo, associatedStories: [:]) - return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + + var additionalContent: ChatMessageItemAdditionalContent? + if !self.subEntries.isEmpty { + var messages: [Message] = [] + for entry in self.subEntries { + if case let .deleteMessage(message) = entry.event.action { + messages.append(message) + } + } + var hasButton = false + if let isExpanded = self.isExpanded, !isExpanded { + hasButton = true + } + additionalContent = .eventLogGroupedMessages(messages, hasButton) + } + + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: message.id.id), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: message.threadId, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: message.associatedMessages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: message.associatedThreadInfo, associatedStories: [:]) + return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: additionalContent) } case .participantJoin, .participantLeave: var peers = SimpleDictionary() @@ -2184,16 +2244,53 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } -func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentationData: ChatPresentationData) -> [ChatRecentActionsEntry] { +func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentationData: ChatPresentationData, expandedDeletedMessages: Set) -> [ChatRecentActionsEntry] { var result: [ChatRecentActionsEntry] = [] - for entry in entries.reversed() { - result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .content), presentationData: presentationData, entry: entry)) - if eventNeedsHeader(entry.event) { - result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .header), presentationData: presentationData, entry: entry)) + var deleteMessageEntries: [ChannelAdminEventLogEntry] = [] + + func appendCurrentDeleteEntries() { + if !deleteMessageEntries.isEmpty, let lastEntry = deleteMessageEntries.last, let lastMessageId = lastEntry.event.action.messageId { + let isExpandable = deleteMessageEntries.count > 10 + let isExpanded = expandedDeletedMessages.contains(lastMessageId) || !isExpandable + let isGroup = deleteMessageEntries.count > 1 + + for i in 0 ..< deleteMessageEntries.count { + let entry = deleteMessageEntries[i] + let isLast = i == deleteMessageEntries.count - 1 + if isExpanded || isLast { + result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .content), presentationData: presentationData, entry: entry, subEntries: isGroup && isExpandable ? deleteMessageEntries : [], isExpanded: isExpandable && isLast ? isExpanded : nil)) + } + } + + result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: lastEntry.event.id, contentIndex: .header), presentationData: presentationData, entry: lastEntry, subEntries: isGroup ? deleteMessageEntries : [], isExpanded: isExpandable ? isExpanded : nil)) + + deleteMessageEntries = [] } } - assert(result == result.sorted().reversed()) + for entry in entries.reversed() { + let currentDeleteMessageEvent = deleteMessageEntries.first?.event + var skipAppending = false + if case .deleteMessage = entry.event.action { + if currentDeleteMessageEvent == nil || (currentDeleteMessageEvent!.peerId == entry.event.peerId && abs(currentDeleteMessageEvent!.date - entry.event.date) < 5) { + } else { + appendCurrentDeleteEntries() + } + deleteMessageEntries.append(entry) + skipAppending = true + } + if !skipAppending { + appendCurrentDeleteEntries() + + result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .content), presentationData: presentationData, entry: entry, subEntries: [], isExpanded: nil)) + if eventNeedsHeader(entry.event) { + result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .header), presentationData: presentationData, entry: entry, subEntries: [], isExpanded: nil)) + } + } + } + appendCurrentDeleteEntries() + +// assert(result == result.sorted().reversed()) return result } @@ -2205,17 +2302,18 @@ struct ChatRecentActionsHistoryTransition { let updates: [ListViewUpdateItem] let canLoadEarlier: Bool let displayingResults: Bool + let searchResultsState: (String, [MessageIndex])? let isEmpty: Bool } -func chatRecentActionsHistoryPreparedTransition(from fromEntries: [ChatRecentActionsEntry], to toEntries: [ChatRecentActionsEntry], type: ChannelAdminEventLogUpdateType, canLoadEarlier: Bool, displayingResults: Bool, context: AccountContext, peer: Peer, controllerInteraction: ChatControllerInteraction, chatThemes: [TelegramTheme]) -> ChatRecentActionsHistoryTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) +func chatRecentActionsHistoryPreparedTransition(from fromEntries: [ChatRecentActionsEntry], to toEntries: [ChatRecentActionsEntry], type: ChannelAdminEventLogUpdateType, canLoadEarlier: Bool, displayingResults: Bool, context: AccountContext, peer: Peer, controllerInteraction: ChatControllerInteraction, chatThemes: [TelegramTheme], searchResultsState: (String, [MessageIndex])?) -> ChatRecentActionsHistoryTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdatesReversed(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, peer: peer, controllerInteraction: controllerInteraction, chatThemes: chatThemes), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, peer: peer, controllerInteraction: controllerInteraction, chatThemes: chatThemes), directionHint: nil) } - return ChatRecentActionsHistoryTransition(filteredEntries: toEntries, type: type, deletions: deletions, insertions: insertions, updates: updates, canLoadEarlier: canLoadEarlier, displayingResults: displayingResults, isEmpty: toEntries.isEmpty) + return ChatRecentActionsHistoryTransition(filteredEntries: toEntries, type: type, deletions: deletions, insertions: insertions, updates: updates, canLoadEarlier: canLoadEarlier, displayingResults: displayingResults, searchResultsState: searchResultsState, isEmpty: toEntries.isEmpty) } private extension ExportedInvitation { diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/Contents.json new file mode 100644 index 0000000000..8ddd25baad --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "addbirthday_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/addbirthday_30.pdf b/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/addbirthday_30.pdf new file mode 100644 index 0000000000..e871e0e803 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/addbirthday_30.pdf @@ -0,0 +1,353 @@ +%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 7.000000 9.588997 cm +0.000000 0.000000 0.000000 scn +12.205765 10.441348 m +11.856519 10.554989 11.481276 10.363993 11.367635 10.014748 c +11.253994 9.665502 11.444990 9.290258 11.794235 9.176618 c +12.205765 10.441348 l +h +4.205764 9.176618 m +4.555010 9.290258 4.746006 9.665502 4.632365 10.014748 c +4.518725 10.363993 4.143481 10.554989 3.794235 10.441348 c +4.205764 9.176618 l +h +15.335000 5.911003 m +15.335000 5.015311 14.682563 4.068969 13.330831 3.308620 c +11.999943 2.559996 10.116560 2.076003 8.000000 2.076003 c +8.000000 0.746003 l +10.301718 0.746003 12.418336 1.269370 13.982878 2.149425 c +15.526576 3.017755 16.665001 4.321413 16.665001 5.911003 c +15.335000 5.911003 l +h +8.000000 2.076003 m +5.883440 2.076003 4.000058 2.559996 2.669169 3.308620 c +1.317437 4.068969 0.665000 5.015311 0.665000 5.911003 c +-0.665000 5.911003 l +-0.665000 4.321413 0.473424 3.017755 2.017122 2.149425 c +3.581664 1.269370 5.698282 0.746003 8.000000 0.746003 c +8.000000 2.076003 l +h +11.794235 9.176618 m +12.922145 8.809608 13.826619 8.294772 14.437608 7.711730 c +15.046136 7.131035 15.335000 6.514472 15.335000 5.911003 c +16.665001 5.911003 l +16.665001 6.973168 16.149467 7.916560 15.355797 8.673929 c +14.564587 9.428950 13.469061 10.030286 12.205765 10.441348 c +11.794235 9.176618 l +h +0.665000 5.911003 m +0.665000 6.514472 0.953865 7.131035 1.562393 7.711730 c +2.173381 8.294772 3.077855 8.809608 4.205764 9.176618 c +3.794235 10.441348 l +2.530939 10.030286 1.435413 9.428950 0.644203 8.673929 c +-0.149467 7.916560 -0.665000 6.973168 -0.665000 5.911003 c +0.665000 5.911003 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 15.000000 4.613129 cm +0.000000 0.000000 0.000000 scn +7.000000 5.386871 m +7.648594 5.240073 l +7.651151 5.251367 l +7.653312 5.262742 l +7.000000 5.386871 l +h +0.000000 2.051871 m +-0.367269 2.051871 -0.665000 1.754141 -0.665000 1.386871 c +-0.665000 1.019602 -0.367269 0.721871 0.000000 0.721871 c +0.000000 2.051871 l +h +8.603312 10.262742 m +8.671866 10.623556 8.434943 10.971629 8.074129 11.040184 c +7.713314 11.108738 7.365242 10.871815 7.296687 10.511001 c +8.603312 10.262742 l +h +6.351405 5.533669 m +5.950424 3.762025 3.718182 2.051871 0.000000 2.051871 c +0.000000 0.721871 l +4.013805 0.721871 7.049575 2.593439 7.648594 5.240073 c +6.351405 5.533669 l +h +7.653312 5.262742 m +8.603312 10.262742 l +7.296687 10.511001 l +6.346687 5.511001 l +7.653312 5.262742 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 13.500000 13.669998 cm +0.000000 0.000000 0.000000 scn +0.000000 7.330002 m +-0.470226 7.800228 l +-0.594938 7.675516 -0.665000 7.506371 -0.665000 7.330002 c +0.000000 7.330002 l +h +3.000000 7.330002 m +3.665000 7.330002 l +3.665000 7.506371 3.594938 7.675516 3.470226 7.800228 c +3.000000 7.330002 l +h +3.000000 1.830002 m +3.470226 1.359776 l +3.594938 1.484488 3.665000 1.653633 3.665000 1.830002 c +3.000000 1.830002 l +h +0.000000 1.830002 m +-0.665000 1.830002 l +-0.665000 1.653633 -0.594938 1.484488 -0.470226 1.359776 c +0.000000 1.830002 l +h +2.335000 7.330002 m +2.335000 1.830002 l +3.665000 1.830002 l +3.665000 7.330002 l +2.335000 7.330002 l +h +0.665000 1.830002 m +0.665000 7.330002 l +-0.665000 7.330002 l +-0.665000 1.830002 l +0.665000 1.830002 l +h +3.000000 1.830002 m +2.529774 2.300228 2.530002 2.300456 2.530228 2.300682 c +2.530301 2.300755 2.530526 2.300979 2.530672 2.301124 c +2.530965 2.301416 2.531251 2.301700 2.531530 2.301977 c +2.532089 2.302531 2.532620 2.303057 2.533125 2.303553 c +2.534134 2.304547 2.535036 2.305427 2.535831 2.306197 c +2.537421 2.307737 2.538584 2.308839 2.539320 2.309529 c +2.540790 2.310907 2.540567 2.310652 2.538657 2.308981 c +2.534820 2.305623 2.524336 2.296691 2.507250 2.283877 c +2.472987 2.258180 2.413031 2.217510 2.327603 2.174796 c +2.158924 2.090456 1.885112 1.995002 1.500000 1.995002 c +1.500000 0.665002 l +2.114888 0.665002 2.591076 0.819547 2.922397 0.985208 c +3.086969 1.067494 3.214513 1.151824 3.305250 1.219877 c +3.350664 1.253938 3.387055 1.284068 3.414468 1.308054 c +3.428183 1.320055 3.439679 1.330542 3.448961 1.339244 c +3.453604 1.343596 3.457696 1.347506 3.461239 1.350938 c +3.463011 1.352654 3.464645 1.354251 3.466143 1.355726 c +3.466892 1.356462 3.467606 1.357169 3.468287 1.357844 c +3.468627 1.358182 3.468959 1.358512 3.469282 1.358834 c +3.469444 1.358994 3.469680 1.359230 3.469760 1.359311 c +3.469994 1.359544 3.470226 1.359776 3.000000 1.830002 c +h +1.500000 1.995002 m +1.114888 1.995002 0.841076 2.090456 0.672397 2.174796 c +0.586969 2.217510 0.527013 2.258180 0.492750 2.283877 c +0.475664 2.296691 0.465180 2.305623 0.461343 2.308981 c +0.459433 2.310652 0.459210 2.310907 0.460680 2.309529 c +0.461416 2.308839 0.462579 2.307737 0.464169 2.306197 c +0.464964 2.305427 0.465866 2.304547 0.466875 2.303553 c +0.467380 2.303057 0.467912 2.302531 0.468470 2.301977 c +0.468749 2.301700 0.469035 2.301416 0.469328 2.301124 c +0.469474 2.300979 0.469699 2.300755 0.469772 2.300682 c +0.469998 2.300456 0.470226 2.300228 0.000000 1.830002 c +-0.470226 1.359776 -0.469994 1.359544 -0.469760 1.359311 c +-0.469680 1.359230 -0.469444 1.358994 -0.469282 1.358834 c +-0.468959 1.358512 -0.468627 1.358182 -0.468287 1.357844 c +-0.467606 1.357169 -0.466892 1.356462 -0.466143 1.355726 c +-0.464645 1.354251 -0.463011 1.352654 -0.461239 1.350938 c +-0.457696 1.347506 -0.453604 1.343596 -0.448961 1.339244 c +-0.439679 1.330542 -0.428183 1.320055 -0.414468 1.308054 c +-0.387055 1.284068 -0.350664 1.253938 -0.305250 1.219877 c +-0.214513 1.151824 -0.086969 1.067494 0.077603 0.985208 c +0.408924 0.819547 0.885112 0.665002 1.500000 0.665002 c +1.500000 1.995002 l +h +0.000000 7.330002 m +0.470226 6.859776 0.469998 6.859548 0.469772 6.859322 c +0.469699 6.859249 0.469474 6.859025 0.469328 6.858879 c +0.469035 6.858588 0.468749 6.858304 0.468470 6.858027 c +0.467912 6.857472 0.467380 6.856947 0.466875 6.856450 c +0.465866 6.855457 0.464964 6.854577 0.464169 6.853806 c +0.462579 6.852266 0.461416 6.851165 0.460680 6.850475 c +0.459210 6.849096 0.459433 6.849351 0.461343 6.851023 c +0.465180 6.854381 0.475664 6.863313 0.492750 6.876127 c +0.527013 6.901824 0.586969 6.942494 0.672397 6.985208 c +0.841076 7.069547 1.114888 7.165002 1.500000 7.165002 c +1.500000 8.495002 l +0.885112 8.495002 0.408924 8.340457 0.077603 8.174796 c +-0.086969 8.092510 -0.214513 8.008180 -0.305250 7.940127 c +-0.350664 7.906066 -0.387055 7.875936 -0.414468 7.851950 c +-0.428183 7.839949 -0.439679 7.829462 -0.448961 7.820759 c +-0.453604 7.816407 -0.457696 7.812498 -0.461239 7.809066 c +-0.463011 7.807350 -0.464645 7.805752 -0.466143 7.804278 c +-0.466892 7.803541 -0.467606 7.802835 -0.468287 7.802159 c +-0.468627 7.801822 -0.468959 7.801492 -0.469282 7.801170 c +-0.469444 7.801009 -0.469680 7.800774 -0.469760 7.800693 c +-0.469994 7.800459 -0.470226 7.800228 0.000000 7.330002 c +h +1.500000 7.165002 m +1.885112 7.165002 2.158924 7.069547 2.327603 6.985208 c +2.413031 6.942494 2.472987 6.901824 2.507250 6.876127 c +2.524336 6.863313 2.534820 6.854381 2.538657 6.851023 c +2.540567 6.849351 2.540790 6.849096 2.539320 6.850475 c +2.538584 6.851165 2.537421 6.852266 2.535831 6.853806 c +2.535036 6.854577 2.534134 6.855457 2.533125 6.856450 c +2.532620 6.856947 2.532089 6.857472 2.531530 6.858027 c +2.531251 6.858304 2.530965 6.858588 2.530672 6.858879 c +2.530526 6.859025 2.530301 6.859249 2.530228 6.859322 c +2.530002 6.859548 2.529774 6.859776 3.000000 7.330002 c +3.470226 7.800228 3.469994 7.800459 3.469760 7.800693 c +3.469680 7.800774 3.469444 7.801009 3.469282 7.801170 c +3.468959 7.801492 3.468627 7.801822 3.468287 7.802159 c +3.467606 7.802835 3.466892 7.803541 3.466143 7.804278 c +3.464645 7.805752 3.463011 7.807350 3.461239 7.809066 c +3.457696 7.812498 3.453604 7.816407 3.448961 7.820759 c +3.439679 7.829462 3.428183 7.839949 3.414468 7.851950 c +3.387055 7.875936 3.350664 7.906066 3.305250 7.940127 c +3.214513 8.008180 3.086969 8.092510 2.922397 8.174796 c +2.591076 8.340457 2.114888 8.495002 1.500000 8.495002 c +1.500000 7.165002 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 13.500000 20.169998 cm +0.000000 0.000000 0.000000 scn +2.335000 2.830002 m +2.335000 2.368844 1.961158 1.995002 1.500000 1.995002 c +1.500000 0.665002 l +2.695697 0.665002 3.665000 1.634305 3.665000 2.830002 c +2.335000 2.830002 l +h +1.500000 1.995002 m +1.038842 1.995002 0.665000 2.368844 0.665000 2.830002 c +-0.665000 2.830002 l +-0.665000 1.634305 0.304303 0.665002 1.500000 0.665002 c +1.500000 1.995002 l +h +0.665000 2.830002 m +0.665000 3.110801 0.793223 3.630285 1.029203 4.083600 c +1.144165 4.304442 1.265095 4.470649 1.372974 4.572635 c +1.483884 4.677485 1.525731 4.665002 1.500000 4.665002 c +1.500000 5.995002 l +1.060055 5.995002 0.707296 5.773571 0.459291 5.539114 c +0.208255 5.301792 0.005961 4.998329 -0.150523 4.697724 c +-0.457436 4.108146 -0.665000 3.377629 -0.665000 2.830002 c +0.665000 2.830002 l +h +1.500000 4.665002 m +1.474269 4.665002 1.516116 4.677485 1.627026 4.572635 c +1.734905 4.470649 1.855835 4.304442 1.970797 4.083600 c +2.206777 3.630285 2.335000 3.110801 2.335000 2.830002 c +3.665000 2.830002 l +3.665000 3.377629 3.457436 4.108146 3.150523 4.697724 c +2.994039 4.998329 2.791745 5.301792 2.540709 5.539114 c +2.292704 5.773571 1.939945 5.995002 1.500000 5.995002 c +1.500000 4.665002 l +h +f +n +Q +q +0.000000 1.000000 -1.000000 0.000000 6.330002 6.000000 cm +0.000000 0.000000 0.000000 scn +0.665000 1.330002 m +0.665000 1.697271 0.367269 1.995002 0.000000 1.995002 c +-0.367269 1.995002 -0.665000 1.697271 -0.665000 1.330002 c +0.665000 1.330002 l +h +-0.665000 -5.669998 m +-0.665000 -6.037268 -0.367269 -6.334998 0.000000 -6.334998 c +0.367269 -6.334998 0.665000 -6.037268 0.665000 -5.669998 c +-0.665000 -5.669998 l +h +-0.665000 1.330002 m +-0.665000 -5.669998 l +0.665000 -5.669998 l +0.665000 1.330002 l +-0.665000 1.330002 l +h +f +n +Q +q +1.000000 -0.000000 0.000000 1.000000 8.500000 1.169998 cm +0.000000 0.000000 0.000000 scn +0.665000 8.330002 m +0.665000 8.697271 0.367269 8.995002 0.000000 8.995002 c +-0.367269 8.995002 -0.665000 8.697271 -0.665000 8.330002 c +0.665000 8.330002 l +h +-0.665000 1.330002 m +-0.665000 0.962732 -0.367269 0.665002 0.000000 0.665002 c +0.367269 0.665002 0.665000 0.962732 0.665000 1.330002 c +-0.665000 1.330002 l +h +-0.665000 8.330002 m +-0.665000 1.330002 l +0.665000 1.330002 l +0.665000 8.330002 l +-0.665000 8.330002 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 10208 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000010298 00000 n +0000010322 00000 n +0000010495 00000 n +0000010569 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +10628 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index d530187c0b..06bf87b584 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -63,6 +63,7 @@ import BusinessIntroSetupScreen import TelegramNotices import BotSettingsScreen import CameraScreen +import BirthdayPickerScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -2151,7 +2152,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { let limit: Int32 = 10 var reachedLimitImpl: ((Int32) -> Void)? - + var presentBirthdayPickerImpl: (() -> Void)? let mode: ContactMultiselectionControllerMode var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? if case let .chatList(birthdays) = source, let birthdays, !birthdays.isEmpty { @@ -2164,6 +2165,18 @@ public final class SharedAccountContextImpl: SharedAccountContext { mode = .premiumGifting(birthdays: nil, selectToday: false) } + var contactOptions: [ContactListAdditionalOption] = [] + if currentBirthdays != nil || "".isEmpty { + contactOptions = [ContactListAdditionalOption( + title: "Add Your Birthday", + icon: .generic(UIImage(bundleImageName: "Contact List/AddBirthdayIcon")!), + action: { + presentBirthdayPickerImpl?() + }, + clearHighlightAutomatically: true + )] + } + var openProfileImpl: ((EnginePeer) -> Void)? var sendMessageImpl: ((EnginePeer) -> Void)? @@ -2171,7 +2184,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { ContactMultiselectionControllerParams( context: context, mode: mode, - options: [], + options: contactOptions, isPeerEnabled: { peer in if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { return true @@ -2275,6 +2288,33 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } + presentBirthdayPickerImpl = { [weak controller] in + guard let controller else { + return + } + let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .setupBirthday).startStandalone() + + let settingsPromise: Promise + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let current = rootController.getPrivacySettings() { + settingsPromise = current + } else { + settingsPromise = Promise() + settingsPromise.set(.single(nil) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init))) + } + let birthdayController = BirthdayPickerScreen(context: context, settings: settingsPromise.get(), openSettings: { + context.sharedContext.makeBirthdayPrivacyController(context: context, settings: settingsPromise, openedFromBirthdayScreen: true, present: { [weak controller] c in + controller?.push(c) + }) + }, completion: { [weak controller] value in + let _ = context.engine.accountData.updateBirthday(birthday: value).startStandalone() + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: presentationData.strings.Birthday_Added, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in + return true + }), in: .current) + }) + controller.push(birthdayController) + } + return controller } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 5fa784ff84..b4036b8007 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -80,6 +80,7 @@ public struct ChatTextFontAttributes: OptionSet, Hashable, Sequence { public static let italic = ChatTextFontAttributes(rawValue: 1 << 1) public static let monospace = ChatTextFontAttributes(rawValue: 1 << 2) public static let blockQuote = ChatTextFontAttributes(rawValue: 1 << 3) + public static let smaller = ChatTextFontAttributes(rawValue: 1 << 4) public func makeIterator() -> AnyIterator { var index = 0 diff --git a/submodules/TextFormat/Sources/GenerateTextEntities.swift b/submodules/TextFormat/Sources/GenerateTextEntities.swift index a97df3aef1..bb7f39b3a6 100644 --- a/submodules/TextFormat/Sources/GenerateTextEntities.swift +++ b/submodules/TextFormat/Sources/GenerateTextEntities.swift @@ -59,6 +59,7 @@ private let validTimecodePreviousSet: CharacterSet = { public struct ApplicationSpecificEntityType { public static let Timecode: Int32 = 1 + public static let Button: Int32 = 2 } private enum CurrentEntityType { diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index a3949917e9..6cf40ed818 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -253,6 +253,9 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti if let time = parseTimecodeString(text) { string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode), value: TelegramTimecode(time: time, text: text), range: range) } + } else if type == ApplicationSpecificEntityType.Button { + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Button), value: true as NSNumber, range: range) + addFontAttributes(range, .smaller) } case let .CustomEmoji(_, fileId): let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) @@ -284,6 +287,8 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti font = italicFont } else if fontAttributes == [.monospace] { font = fixedFont + } else if fontAttributes == [.smaller] { + font = baseFont.withSize(floor(baseFont.pointSize * 0.9)) } else { font = baseFont } diff --git a/submodules/TextFormat/Sources/TelegramAttributes.swift b/submodules/TextFormat/Sources/TelegramAttributes.swift index 1f75c378d3..babf5c9931 100644 --- a/submodules/TextFormat/Sources/TelegramAttributes.swift +++ b/submodules/TextFormat/Sources/TelegramAttributes.swift @@ -43,4 +43,5 @@ public struct TelegramTextAttributes { public static let Pre = "TelegramPre" public static let Spoiler = "TelegramSpoiler" public static let Code = "TelegramCode" + public static let Button = "TelegramButton" }