diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 4f58aff1d9..b1d9503cf2 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15769,3 +15769,17 @@ Error: %8$@"; "Gift.View.Context.Craft" = "Craft"; "Group.OwnershipTransfer.DescriptionShortInfo" = "This will transfer the full **owner rights** for **%1$@** to **%2$@**."; + +"FormattedDate.InMinutes_1" = "in %@ minute"; +"FormattedDate.InMinutes_any" = "in %@ minutes"; +"FormattedDate.InHours_1" = "in %@ hour"; +"FormattedDate.InHours_any" = "in %@ hours"; +"FormattedDate.InDays_1" = "in %@ day"; +"FormattedDate.InDays_any" = "in %@ days"; + +"FormattedDate.MinutesAgo_1" = "%@ minute ago"; +"FormattedDate.MinutesAgo_any" = "%@ minutes ago"; +"FormattedDate.HoursAgo_1" = "%@ hour ago"; +"FormattedDate.HoursAgo_any" = "%@ hours ago"; +"FormattedDate.DaysAgo_1" = "%@ day ago"; +"FormattedDate.DaysAgo_any" = "%@ days ago"; diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 3e9c3644ab..3fd22bb463 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -255,6 +255,7 @@ public enum ChatControllerInteractionLongTapAction { case hashtag(String) case timecode(Double, String) case bankCard(String) + case date(Int32) } public enum ChatHistoryMessageSelection: Equatable { diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 858f183c6a..6d1ec8d092 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -791,21 +791,21 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes if (flags & (1 << 0)) != 0 { format = .relative } else { - let timeFormat: MessageTextEntityType.DateTimeFormat.TimeFormat + let timeFormat: MessageTextEntityType.DateTimeFormat.TimeFormat? if (flags & (1 << 1)) != 0 { timeFormat = .short } else if (flags & (1 << 2)) != 0 { timeFormat = .long } else { - timeFormat = .short + timeFormat = nil } - let dateFormat: MessageTextEntityType.DateTimeFormat.DateFormat + let dateFormat: MessageTextEntityType.DateTimeFormat.DateFormat? if (flags & (1 << 3)) != 0 { dateFormat = .short } else if (flags & (1 << 4)) != 0 { dateFormat = .long } else { - dateFormat = .short + dateFormat = nil } format = .full(timeFormat: timeFormat, dateFormat: dateFormat) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift index f48fe3b8c5..0ae24d2264 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift @@ -65,12 +65,16 @@ func apiEntitiesFromMessageTextEntities(_ entities: [MessageTextEntity], associa flags |= 1 << 1 case .long: flags |= 1 << 2 + default: + break } switch dateFormat { case .short: flags |= 1 << 3 case .long: flags |= 1 << 4 + default: + break } } apiEntities.append(.messageEntityFormattedDate(.init(flags: flags, offset: offset, length: length, date: date))) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift index d5022500ac..cf7987d5c5 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift @@ -16,27 +16,27 @@ public enum MessageTextEntityType: Equatable { } case relative - case full(timeFormat: TimeFormat, dateFormat: DateFormat) + case full(timeFormat: TimeFormat?, dateFormat: DateFormat?) public init(rawValue: Int32) { if (rawValue & (1 << 0)) != 0 { self = .relative } else { - let timeFormat: MessageTextEntityType.DateTimeFormat.TimeFormat + let timeFormat: MessageTextEntityType.DateTimeFormat.TimeFormat? if (rawValue & (1 << 1)) != 0 { timeFormat = .short } else if (rawValue & (1 << 2)) != 0 { timeFormat = .long } else { - timeFormat = .short + timeFormat = nil } - let dateFormat: MessageTextEntityType.DateTimeFormat.DateFormat + let dateFormat: MessageTextEntityType.DateTimeFormat.DateFormat? if (rawValue & (1 << 3)) != 0 { dateFormat = .short } else if (rawValue & (1 << 4)) != 0 { dateFormat = .long } else { - dateFormat = .short + dateFormat = nil } self = .full(timeFormat: timeFormat, dateFormat: dateFormat) } @@ -53,12 +53,16 @@ public enum MessageTextEntityType: Equatable { rawValue |= 1 << 1 case .long: rawValue |= 1 << 2 + default: + break } switch dateFormat { case .short: rawValue |= 1 << 3 case .long: rawValue |= 1 << 4 + default: + break } } return rawValue diff --git a/submodules/TelegramStringFormatting/Sources/DateFormat.swift b/submodules/TelegramStringFormatting/Sources/DateFormat.swift index 430fde43d6..2bbb986775 100644 --- a/submodules/TelegramStringFormatting/Sources/DateFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/DateFormat.swift @@ -1,8 +1,60 @@ import Foundation +import TelegramCore import TelegramPresentationData import TelegramUIPreferences -public func stringForShortTimestamp(hours: Int32, minutes: Int32, dateTimeFormat: PresentationDateTimeFormat, formatAsPlainText: Bool = false) -> String { +public func stringForEntityFormattedDate(timestamp: Int32, format: MessageTextEntityType.DateTimeFormat, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String { + switch format { + case .relative: + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + let value = currentTimestamp - timestamp + if value > 0 { + if value <= 1 * 60 * 60 { + return strings.FormattedDate_MinutesAgo(Int32(round(Float(value) / 60))) + } else if value <= 24 * 60 * 60 { + return strings.FormattedDate_HoursAgo(Int32(round(Float(value) / (60 * 60)))) + } else { + return strings.FormattedDate_DaysAgo(Int32(round(Float(value) / (24 * 60 * 60)))) + } + } else { + let value = abs(value) + if value <= 1 * 60 * 60 { + return strings.FormattedDate_InMinutes(Int32(round(Float(value) / 60))) + } else if value <= 24 * 60 * 60 { + return strings.FormattedDate_InHours(Int32(round(Float(value) / (60 * 60)))) + } else { + return strings.FormattedDate_InDays(Int32(round(Float(value) / (24 * 60 * 60)))) + } + } + case let .full(timeFormat, dateFormat): + var string = "" + if let dateFormat { + switch dateFormat { + case .short: + string += stringForMediumDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat, withTime: false) + case .long: + string += stringForFullDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat) + } + } + if let timeFormat { + let timeString: String + switch timeFormat { + case .short: + timeString = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat) + case .long: + timeString = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat, withSeconds: true) + } + if !string.isEmpty { + string = strings.Time_AtPreciseDate(string, timeString).string + } else { + string = timeString + } + } + return string + } +} + +public func stringForShortTimestamp(hours: Int32, minutes: Int32, seconds: Int32? = nil, dateTimeFormat: PresentationDateTimeFormat, formatAsPlainText: Bool = false) -> String { switch dateTimeFormat.timeFormat { case .regular: let hourString: String @@ -28,17 +80,23 @@ public func stringForShortTimestamp(hours: Int32, minutes: Int32, dateTimeFormat spaceCharacter = "\u{00a0}" } - if minutes >= 10 { - return "\(hourString):\(minutes)\(spaceCharacter)\(periodString)" + let minuteString = String(format: "%02d", arguments: [Int(minutes)]) + if let seconds { + let secondString = String(format: "%02d", arguments: [Int(seconds)]) + return "\(hourString):\(minuteString):\(secondString)\(spaceCharacter)\(periodString)" } else { - return "\(hourString):0\(minutes)\(spaceCharacter)\(periodString)" + return "\(hourString):\(minuteString)\(spaceCharacter)\(periodString)" } case .military: - return String(format: "%02d:%02d", arguments: [Int(hours), Int(minutes)]) + if let seconds { + return String(format: "%02d:%02d:%02d", arguments: [Int(hours), Int(minutes), Int(seconds)]) + } else { + return String(format: "%02d:%02d", arguments: [Int(hours), Int(minutes)]) + } } } -public func stringForMessageTimestamp(timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat, local: Bool = true) -> String { +public func stringForMessageTimestamp(timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat, withSeconds: Bool = false, local: Bool = true) -> String { var t = Int(timestamp) var timeinfo = tm() if local { @@ -47,7 +105,7 @@ public func stringForMessageTimestamp(timestamp: Int32, dateTimeFormat: Presenta gmtime_r(&t, &timeinfo) } - return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat) + return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, seconds: withSeconds ? timeinfo.tm_sec : nil, dateTimeFormat: dateTimeFormat) } public func getDateTimeComponents(timestamp: Int32) -> (day: Int32, month: Int32, year: Int32, hour: Int32, minutes: Int32) { diff --git a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift index ed37649c86..7a29136f38 100644 --- a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift @@ -175,7 +175,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode { break case .ignore: return .fail - case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .custom: + case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .date, .custom: return .waitForSingleTap } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 45ef4deb9b..34420151b1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -1672,7 +1672,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { TelegramTextAttributes.PeerTextMention, TelegramTextAttributes.BotCommand, TelegramTextAttributes.Hashtag, - TelegramTextAttributes.BankCard + TelegramTextAttributes.BankCard, + TelegramTextAttributes.Date ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift index a7a021d09b..01f95aa3ff 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift @@ -159,6 +159,7 @@ public struct ChatMessageBubbleContentTapAction { case ignore case openPollResults(Data) case copy(String) + case date(Int32, String) case largeEmoji(String, String?, TelegramMediaFile) case customEmoji(TelegramMediaFile) case custom(() -> Void) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 0a6ef35dea..12999921c9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -21,6 +21,7 @@ import PersistentStringHash import GridMessageSelectionNode import AppBundle import Markdown +import TelegramStringFormatting import WallpaperBackgroundNode import ChatPresentationInterfaceState import ChatMessageBackground @@ -1296,7 +1297,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI break case .ignore: return .fail - case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .custom: + case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .date, .custom: return .waitForSingleTap } } @@ -5658,6 +5659,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI item.controllerInteraction.openLargeEmojiInfo(emoji, fitz, file) }) } + case let .date(date, _): + if let item = self.item { + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(stringForFullDate(timestamp: date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), rects: rects) else { + return + } + item.controllerInteraction.longTap(.date(date), ChatControllerInteraction.LongTapParams(message: item.message, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) + }) + } case let .customEmoji(file): if let item = self.item { return .optionalAction({ @@ -5824,6 +5834,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI break case .customEmoji: break + case let .date(date, _): + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(stringForFullDate(timestamp: date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), rects: rects) else { + return + } + item.controllerInteraction.longTap(.date(date), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) + }) case .custom: break } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift index 6102bf227c..f3a4e4a6c9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift @@ -233,7 +233,8 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode TelegramTextAttributes.PeerTextMention, TelegramTextAttributes.BotCommand, TelegramTextAttributes.Hashtag, - TelegramTextAttributes.BankCard + TelegramTextAttributes.BankCard, + TelegramTextAttributes.Date ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 23010fb8b5..29586c43c8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -1149,7 +1149,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { TelegramTextAttributes.BotCommand, TelegramTextAttributes.Hashtag, TelegramTextAttributes.Timecode, - TelegramTextAttributes.BankCard + TelegramTextAttributes.BankCard, + TelegramTextAttributes.Date ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { @@ -1202,6 +1203,12 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return ChatMessageBubbleContentTapAction(content: .copy(pre)) } else if let code = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Code)] as? String { return ChatMessageBubbleContentTapAction(content: .copy(code)) + } else if let date = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Date)] as? Int32 { + var string = "" + if let (_, text, _) = self.textNode.textNode.attributeSubstringWithRange(name: TelegramTextAttributes.Date, index: index) { + string = text + } + return ChatMessageBubbleContentTapAction(content: .date(date, string), rects: rects) } else if let _ = attributes[NSAttributedString.Key(rawValue: "Attribute__Blockquote")] { if let _ = self.textNode.textNode.collapsibleBlockAtPoint(textLocalPoint) { return ChatMessageBubbleContentTapAction(content: .none) @@ -1296,7 +1303,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { TelegramTextAttributes.BotCommand, TelegramTextAttributes.Hashtag, TelegramTextAttributes.Timecode, - TelegramTextAttributes.BankCard + TelegramTextAttributes.BankCard, + TelegramTextAttributes.Date ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift index bb614b86a1..92bd0fa3b8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift @@ -686,7 +686,8 @@ private final class ChatMessageTodoItemNode: ASDisplayNode { TelegramTextAttributes.PeerTextMention, TelegramTextAttributes.BotCommand, TelegramTextAttributes.Hashtag, - TelegramTextAttributes.BankCard + TelegramTextAttributes.BankCard, + TelegramTextAttributes.Date ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)], let textRects = textNode.textNode.attributeRects(name: name, at: index) { @@ -1584,7 +1585,8 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode { TelegramTextAttributes.PeerTextMention, TelegramTextAttributes.BotCommand, TelegramTextAttributes.Hashtag, - TelegramTextAttributes.BankCard + TelegramTextAttributes.BankCard, + TelegramTextAttributes.Date ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)], let textRects = self.textNode.textNode.attributeRects(name: name, at: index) { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 823e154722..7c51b2709f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -566,6 +566,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { strongSelf.presentController(actionSheet, .window(.root), nil) case .bankCard: break + case .date: + break } } }, todoItemLongTap: { _, _ in diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 0a40d81eb6..2707b92685 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -564,7 +564,8 @@ final class StoryContentCaptionComponent: Component { TelegramTextAttributes.BotCommand, TelegramTextAttributes.Hashtag, TelegramTextAttributes.Timecode, - TelegramTextAttributes.BankCard + TelegramTextAttributes.BankCard, + TelegramTextAttributes.Date ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenDateContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenDateContextMenu.swift new file mode 100644 index 0000000000..755194bbf5 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenDateContextMenu.swift @@ -0,0 +1,98 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import ChatControllerInteraction +import EventKit +import EventKitUI + +extension ChatControllerImpl: EKEventEditViewDelegate { + func openDateContextMenu(date: Int32, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { + return + } + + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } + + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + //TODO:localize + var items: [ContextMenuItem] = [] + items.append( + .action(ContextMenuActionItem(text: "Copy Date", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = "\(date)" + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_CardNumberCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + items.append( + .action(ContextMenuActionItem(text: "Add to Calendar", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Calendar"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + let eventStore = EKEventStore() + let event = EKEvent(eventStore: eventStore) + event.startDate = Date(timeIntervalSince1970: Double(date)) + event.endDate = Date(timeIntervalSince1970: Double(date + 3600)) + + let editViewController = EKEventEditViewController() + editViewController.eventStore = eventStore + editViewController.event = event + editViewController.editViewDelegate = self + + if let rootController = self.navigationController?.view.window?.rootViewController { + rootController.present(editViewController, animated: true) + } + })) + ) + + self.canReadHistory.set(false) + + let controller = makeContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(controller) + } + + public func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction) { + controller.dismiss(animated: true) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkLongTap.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkLongTap.swift index 4f48129509..bc6fec844e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkLongTap.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkLongTap.swift @@ -35,6 +35,8 @@ extension ChatControllerImpl { self.openBankCardContextMenu(number: number, params: params) case let .phone(number): self.openPhoneContextMenu(number: number, params: params) + case let .date(date): + self.openDateContextMenu(date: date, params: params) } } } diff --git a/submodules/TextFormat/Sources/GenerateTextEntities.swift b/submodules/TextFormat/Sources/GenerateTextEntities.swift index 4079103b32..d74adcd694 100644 --- a/submodules/TextFormat/Sources/GenerateTextEntities.swift +++ b/submodules/TextFormat/Sources/GenerateTextEntities.swift @@ -15,6 +15,8 @@ private let whitelistedHosts: Set = Set([ private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue) private let dataAndPhoneNumberDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link, .phoneNumber]).rawValue) private let phoneNumberDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.phoneNumber]).rawValue) +private let dataAndPhoneNumberAndDateDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link, .phoneNumber, .date]).rawValue) + private let validHashtagSet: CharacterSet = { var set = CharacterSet.alphanumerics set.insert("_") @@ -100,8 +102,9 @@ public struct EnabledEntityTypes: OptionSet { public static let timecode = EnabledEntityTypes(rawValue: 1 << 5) public static let external = EnabledEntityTypes(rawValue: 1 << 6) public static let internalUrl = EnabledEntityTypes(rawValue: 1 << 7) + public static let date = EnabledEntityTypes(rawValue: 1 << 8) - public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .allUrl, .phoneNumber] + public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .allUrl, .phoneNumber, .date] } private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity], mediaDuration: Double? = nil) { @@ -254,7 +257,9 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType let utf16 = text.utf16 var detector: NSDataDetector? - if enabledTypes.contains(.phoneNumber) && (enabledTypes.contains(.allUrl) || enabledTypes.contains(.internalUrl)) { + if enabledTypes.contains(.phoneNumber) && enabledTypes.contains(.date) && (enabledTypes.contains(.allUrl) || enabledTypes.contains(.internalUrl)) { + detector = dataAndPhoneNumberAndDateDetector + } else if enabledTypes.contains(.phoneNumber) && (enabledTypes.contains(.allUrl) || enabledTypes.contains(.internalUrl)) { detector = dataAndPhoneNumberDetector } else if enabledTypes.contains(.phoneNumber) { detector = phoneNumberDetector @@ -267,7 +272,7 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType if let detector = detector { detector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in if let result = result { - if result.resultType == NSTextCheckingResult.CheckingType.link || result.resultType == NSTextCheckingResult.CheckingType.phoneNumber { + if [NSTextCheckingResult.CheckingType.link, NSTextCheckingResult.CheckingType.phoneNumber, NSTextCheckingResult.CheckingType.date].contains(result.resultType) { let lowerBound = utf16.index(utf16.startIndex, offsetBy: result.range.location).samePosition(in: text) let upperBound = utf16.index(utf16.startIndex, offsetBy: result.range.location + result.range.length).samePosition(in: text) if let lowerBound = lowerBound, let upperBound = upperBound { @@ -293,6 +298,8 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType } type = .Url + } else if result.resultType == NSTextCheckingResult.CheckingType.date, let date = result.date?.timeIntervalSince1970 { + type = .FormattedDate(format: .relative, date: Int32(date)) } else { type = .PhoneNumber } diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index 55f1cc2c25..c05f5f71c7 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -84,13 +84,47 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti let baseQuoteTintColor = baseQuoteTintColor ?? baseColor var nsString: NSString? - let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor]) + let baseAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor] + let string = NSMutableAttributedString(string: text, attributes: baseAttributes) var skipEntity = false var underlineAllLinks = false if linkColor.argb == baseColor.argb { underlineAllLinks = true } + var adjustedRanges: [NSRange?] = [] + adjustedRanges.reserveCapacity(entities.count) + let rangeDelta = 0 + for entity in entities { + let originalRange = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + var range = NSRange(location: originalRange.location + rangeDelta, length: originalRange.length) + let stringLength = string.length + if range.location > stringLength { + adjustedRanges.append(nil) + continue + } else if range.location + range.length > stringLength { + range.length = stringLength - range.location + } + +// switch entity.type { +// case let .FormattedDate(format, date): +// let replacement = stringForEntityFormattedDate() +// switch format { +// case .relative: +// replacement = relative +// case let .full(timeFormat, dateFormat): +// } +// +// let replacementString = NSAttributedString(string: replacement, attributes: baseAttributes) +// string.replaceCharacters(in: range, with: replacementString) +// let newRange = NSRange(location: range.location, length: (replacement as NSString).length) +// adjustedRanges.append(newRange) +// rangeDelta += newRange.length - range.length +// default: + adjustedRanges.append(range) +// } + } + var fontAttributeMask: [ChatTextFontAttributes] = Array(repeating: [], count: string.length) let addFontAttributes: (NSRange, ChatTextFontAttributes) -> Void = { range, attributes in for i in range.lowerBound ..< range.upperBound { @@ -103,12 +137,11 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti skipEntity = false continue } - let stringLength = string.length let entity = entities[i] - var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) - if nsString == nil { - nsString = text as NSString + guard var range = adjustedRanges[i] else { + continue } + let stringLength = string.length if range.location > stringLength { continue } else if range.location + range.length > stringLength { @@ -121,13 +154,13 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } if nsString == nil { - nsString = text as NSString + nsString = string.string as NSString } string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: nsString!.substring(with: range), range: range) case .Email: string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) if nsString == nil { - nsString = text as NSString + nsString = string.string as NSString } if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) @@ -136,7 +169,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti case .PhoneNumber: string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) if nsString == nil { - nsString = text as NSString + nsString = string.string as NSString } if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) @@ -145,7 +178,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti case let .TextUrl(url): string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) if nsString == nil { - nsString = text as NSString + nsString = string.string as NSString } if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) @@ -168,7 +201,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti string.addAttribute(NSAttributedString.Key.font, value: linkFont, range: range) } if nsString == nil { - nsString = text as NSString + nsString = string.string as NSString } string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention), value: nsString!.substring(with: range), range: range) case .Strikethrough: @@ -183,16 +216,21 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti if linkFont !== baseFont { string.addAttribute(NSAttributedString.Key.font, value: linkFont, range: range) } + if nsString == nil { + nsString = string.string as NSString + } let mention = nsString!.substring(with: range) string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: peerId, mention: mention), range: range) case .Hashtag: if nsString == nil { - nsString = text as NSString + nsString = string.string as NSString } let hashtag = nsString!.substring(with: range) if i + 1 != entities.count { if case .Mention = entities[i + 1].type { - let nextRange = NSRange(location: entities[i + 1].range.lowerBound, length: entities[i + 1].range.upperBound - entities[i + 1].range.lowerBound) + guard let nextRange = adjustedRanges[i + 1] else { + break + } if nextRange.location == range.location + range.length + 1 && nsString!.character(at: range.location + range.length) == 43 { skipEntity = true if nextRange.length > 0 { @@ -231,17 +269,20 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } if nsString == nil { - nsString = text as NSString + nsString = string.string as NSString } string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand), value: nsString!.substring(with: range), range: range) case .Code: addFontAttributes(range, .monospace) + if nsString == nil { + nsString = string.string as NSString + } string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Code), value: nsString!.substring(with: range), range: range) case let .Pre(language): addFontAttributes(range, .monospace) addFontAttributes(range, .blockQuote) if nsString == nil { - nsString = text as NSString + nsString = string.string as NSString } if let codeBlockTitleColor, let codeBlockAccentColor, let codeBlockBackgroundColor { var title: NSAttributedString? @@ -260,7 +301,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } if nsString == nil { - nsString = text as NSString + nsString = string.string as NSString } string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard), value: nsString!.substring(with: range), range: range) case .Spoiler: @@ -269,6 +310,12 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti } else { string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler), value: true as NSNumber, range: range) } + case let .FormattedDate(_, date): + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) + if underlineLinks && underlineAllLinks { + string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) + } + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Date), value: date, range: range) case let .Custom(type): if type == ApplicationSpecificEntityType.Timecode { string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) @@ -276,7 +323,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } if nsString == nil { - nsString = text as NSString + nsString = string.string as NSString } let text = nsString!.substring(with: range) if let time = parseTimecodeString(text) { diff --git a/submodules/TextFormat/Sources/TelegramAttributes.swift b/submodules/TextFormat/Sources/TelegramAttributes.swift index babf5c9931..4ee86a0215 100644 --- a/submodules/TextFormat/Sources/TelegramAttributes.swift +++ b/submodules/TextFormat/Sources/TelegramAttributes.swift @@ -44,4 +44,5 @@ public struct TelegramTextAttributes { public static let Spoiler = "TelegramSpoiler" public static let Code = "TelegramCode" public static let Button = "TelegramButton" + public static let Date = "TelegramDate" }