Various improvements

This commit is contained in:
Ilya Laktyushin 2024-04-20 19:37:16 +04:00
parent 6e42f96c19
commit acf905a098
24 changed files with 772 additions and 47 deletions

View File

@ -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";

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)"

View File

@ -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

View File

@ -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)
}

View File

@ -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 {

View File

@ -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<MessageId, Message> = 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 {

View File

@ -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<MessageId, Message> = 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? {

View File

@ -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 {

View File

@ -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)

View File

@ -83,6 +83,7 @@ public enum ChatMessageItemAdditionalContent {
case eventLogPreviousMessage(Message)
case eventLogPreviousDescription(Message)
case eventLogPreviousLink(Message)
case eventLogGroupedMessages([Message], Bool)
}
public enum ChatMessageMerge: Int32 {

View File

@ -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",

View File

@ -76,6 +76,13 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
private let navigationActionDisposable = MetaDisposable()
private var expandedDeletedMessages = Set<EngineMessage.Id>() {
didSet {
self.expandedDeletedMessagesPromise.set(self.expandedDeletedMessages)
}
}
private let expandedDeletedMessagesPromise = ValuePromise<Set<EngineMessage.Id>>(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<Set<EngineMessage.Id>>(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<ChatRecentActionsHistoryTransition, NoError> 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<ChatRecentActionsHistoryTransition, NoError> 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<Void, NoError> 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
}
}
}

View File

@ -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<EnginePeer.Id>()
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<PeerId, Peer>()
@ -2184,16 +2244,53 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
}
}
func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentationData: ChatPresentationData) -> [ChatRecentActionsEntry] {
func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentationData: ChatPresentationData, expandedDeletedMessages: Set<EngineMessage.Id>) -> [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 {

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "addbirthday_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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

View File

@ -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<AccountPrivacySettings?>
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
}

View File

@ -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<ChatTextFontAttributes> {
var index = 0

View File

@ -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 {

View File

@ -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
}

View File

@ -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"
}