mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
564 lines
18 KiB
Swift
564 lines
18 KiB
Swift
import Foundation
|
|
import Postbox
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import PlatformRestrictionMatching
|
|
import TextFormat
|
|
|
|
public enum MessageContentKindKey {
|
|
case text
|
|
case image
|
|
case video
|
|
case videoMessage
|
|
case audioMessage
|
|
case sticker
|
|
case animation
|
|
case file
|
|
case contact
|
|
case game
|
|
case location
|
|
case liveLocation
|
|
case expiredImage
|
|
case expiredVideo
|
|
case expiredVoiceMessage
|
|
case expiredVideoMessage
|
|
case poll
|
|
case restricted
|
|
case dice
|
|
case invoice
|
|
case story
|
|
case giveaway
|
|
case paidContent
|
|
}
|
|
|
|
public enum MessageContentKind: Equatable {
|
|
case text(NSAttributedString)
|
|
case image
|
|
case video
|
|
case videoMessage
|
|
case audioMessage
|
|
case sticker(String)
|
|
case animation
|
|
case file(String)
|
|
case contact
|
|
case game(String)
|
|
case location
|
|
case liveLocation
|
|
case expiredImage
|
|
case expiredVideo
|
|
case expiredVoiceMessage
|
|
case expiredVideoMessage
|
|
case poll(String)
|
|
case restricted(String)
|
|
case dice(String)
|
|
case invoice(String)
|
|
case story
|
|
case giveaway
|
|
|
|
public func isSemanticallyEqual(to other: MessageContentKind) -> Bool {
|
|
switch self {
|
|
case .text:
|
|
if case .text = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .image:
|
|
if case .image = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .video:
|
|
if case .video = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .videoMessage:
|
|
if case .videoMessage = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .audioMessage:
|
|
if case .audioMessage = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .sticker:
|
|
if case .sticker = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .animation:
|
|
if case .animation = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .file:
|
|
if case .file = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .contact:
|
|
if case .contact = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .game:
|
|
if case .game = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .location:
|
|
if case .location = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .liveLocation:
|
|
if case .liveLocation = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .expiredImage:
|
|
if case .expiredImage = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .expiredVideo:
|
|
if case .expiredVideo = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .expiredVoiceMessage:
|
|
if case .expiredVoiceMessage = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .expiredVideoMessage:
|
|
if case .expiredVideoMessage = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .poll:
|
|
if case .poll = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .restricted:
|
|
if case .restricted = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .dice:
|
|
if case .dice = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .invoice:
|
|
if case .invoice = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .story:
|
|
if case .story = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .giveaway:
|
|
if case .giveaway = other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
public var key: MessageContentKindKey {
|
|
switch self {
|
|
case .text:
|
|
return .text
|
|
case .image:
|
|
return .image
|
|
case .video:
|
|
return .video
|
|
case .videoMessage:
|
|
return .videoMessage
|
|
case .audioMessage:
|
|
return .audioMessage
|
|
case .sticker:
|
|
return .sticker
|
|
case .animation:
|
|
return .animation
|
|
case .file:
|
|
return .file
|
|
case .contact:
|
|
return .contact
|
|
case .game:
|
|
return .game
|
|
case .location:
|
|
return .location
|
|
case .liveLocation:
|
|
return .liveLocation
|
|
case .expiredImage:
|
|
return .expiredImage
|
|
case .expiredVideo:
|
|
return .expiredVideo
|
|
case .expiredVoiceMessage:
|
|
return .expiredVoiceMessage
|
|
case .expiredVideoMessage:
|
|
return .expiredVideoMessage
|
|
case .poll:
|
|
return .poll
|
|
case .restricted:
|
|
return .restricted
|
|
case .dice:
|
|
return .dice
|
|
case .invoice:
|
|
return .invoice
|
|
case .story:
|
|
return .story
|
|
case .giveaway:
|
|
return .giveaway
|
|
}
|
|
}
|
|
}
|
|
|
|
public func messageTextWithAttributes(message: EngineMessage) -> NSAttributedString {
|
|
var attributedText = NSAttributedString(string: message.text)
|
|
|
|
var entities: TextEntitiesMessageAttribute?
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
|
entities = attribute
|
|
break
|
|
}
|
|
}
|
|
if let entities = entities?.entities {
|
|
let updatedString = NSMutableAttributedString(attributedString: attributedText)
|
|
|
|
for entity in entities.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) {
|
|
guard case let .CustomEmoji(_, fileId) = entity.type else {
|
|
continue
|
|
}
|
|
|
|
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
|
|
|
let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil)
|
|
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict
|
|
updatedAttributes[ChatTextInputAttributes.customEmoji] = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile)
|
|
|
|
let insertString = NSAttributedString(string: updatedString.attributedSubstring(from: range).string, attributes: updatedAttributes)
|
|
updatedString.replaceCharacters(in: range, with: insertString)
|
|
}
|
|
attributedText = updatedString
|
|
}
|
|
|
|
return attributedText
|
|
}
|
|
|
|
public func messageContentKind(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> MessageContentKind {
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? RestrictedContentMessageAttribute {
|
|
if let text = attribute.platformText(platform: "ios", contentSettings: contentSettings) {
|
|
return .restricted(text)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
for media in message.media {
|
|
if let kind = mediaContentKind(EngineMedia(media), message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) {
|
|
return kind
|
|
}
|
|
}
|
|
return .text(messageTextWithAttributes(message: message))
|
|
}
|
|
|
|
public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, accountPeerId: EnginePeer.Id? = nil) -> MessageContentKind? {
|
|
switch media {
|
|
case let .expiredContent(expiredMedia):
|
|
switch expiredMedia.data {
|
|
case .image:
|
|
return .expiredImage
|
|
case .file:
|
|
return .expiredVideo
|
|
case .voiceMessage:
|
|
return .expiredVoiceMessage
|
|
case .videoMessage:
|
|
return .expiredVideoMessage
|
|
}
|
|
case .image:
|
|
return .image
|
|
case let .file(file):
|
|
var fileName: String = ""
|
|
|
|
var result: MessageContentKind?
|
|
for attribute in file.attributes {
|
|
switch attribute {
|
|
case let .Sticker(text, _, _):
|
|
return .sticker(text)
|
|
case let .FileName(name):
|
|
fileName = name
|
|
case let .Audio(isVoice, _, title, performer, _):
|
|
if isVoice {
|
|
return .audioMessage
|
|
} else {
|
|
if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty {
|
|
return .file(title + " — " + performer)
|
|
} else if let title = title, !title.isEmpty {
|
|
return .file(title)
|
|
} else if let performer = performer, !performer.isEmpty {
|
|
return .file(performer)
|
|
}
|
|
}
|
|
case let .Video(_, _, flags, _, _, _):
|
|
if file.isAnimated {
|
|
result = .animation
|
|
} else {
|
|
if flags.contains(.instantRoundVideo) {
|
|
result = .videoMessage
|
|
} else {
|
|
result = .video
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
if let result = result {
|
|
return result
|
|
}
|
|
if file.isVideoSticker || file.isAnimatedSticker {
|
|
return .sticker("")
|
|
}
|
|
return .file(fileName)
|
|
case .contact:
|
|
return .contact
|
|
case let .game(game):
|
|
return .game(game.title)
|
|
case let .geo(location):
|
|
if location.liveBroadcastingTimeout != nil {
|
|
return .liveLocation
|
|
} else {
|
|
return .location
|
|
}
|
|
case .action:
|
|
if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId {
|
|
return .text(NSAttributedString(string: plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false)?.0 ?? ""))
|
|
} else {
|
|
return nil
|
|
}
|
|
case let .poll(poll):
|
|
return .poll(poll.text)
|
|
case let .dice(dice):
|
|
return .dice(dice.emoji)
|
|
case let .invoice(invoice):
|
|
if !invoice.description.isEmpty {
|
|
return .invoice(invoice.description)
|
|
} else {
|
|
return .invoice(invoice.title)
|
|
}
|
|
case .story:
|
|
return .story
|
|
case .giveaway, .giveawayResults:
|
|
return .giveaway
|
|
case let .webpage(webpage):
|
|
if let message, message.text.isEmpty, case let .Loaded(content) = webpage.content {
|
|
return .text(NSAttributedString(string: content.displayUrl))
|
|
} else {
|
|
return nil
|
|
}
|
|
case let .paidContent(paidContent):
|
|
switch paidContent.extendedMedia.first {
|
|
case let .preview(_, _, videoDuration):
|
|
if let _ = videoDuration {
|
|
return .video
|
|
} else {
|
|
return .image
|
|
}
|
|
case let .full(media):
|
|
if media is TelegramMediaImage {
|
|
return .image
|
|
} else if media is TelegramMediaFile {
|
|
return .video
|
|
} else {
|
|
return nil
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public func stringForMediaKind(_ kind: MessageContentKind, strings: PresentationStrings) -> (NSAttributedString, Bool) {
|
|
switch kind {
|
|
case let .text(text):
|
|
return (foldLineBreaks(text), false)
|
|
case .image:
|
|
return (NSAttributedString(string: strings.Message_Photo), true)
|
|
case .video:
|
|
return (NSAttributedString(string: strings.Message_Video), true)
|
|
case .videoMessage:
|
|
return (NSAttributedString(string: strings.Message_VideoMessage), true)
|
|
case .audioMessage:
|
|
return (NSAttributedString(string: strings.Message_Audio), true)
|
|
case let .sticker(text):
|
|
if text.isEmpty {
|
|
return (NSAttributedString(string: strings.Message_Sticker), true)
|
|
} else {
|
|
return (NSAttributedString(string: strings.Message_StickerText(text).string), true)
|
|
}
|
|
case .animation:
|
|
return (NSAttributedString(string: strings.Message_Animation), true)
|
|
case let .file(text):
|
|
if text.isEmpty {
|
|
return (NSAttributedString(string: strings.Message_File), true)
|
|
} else {
|
|
return (NSAttributedString(string: text), true)
|
|
}
|
|
case .contact:
|
|
return (NSAttributedString(string: strings.Message_Contact), true)
|
|
case let .game(text):
|
|
return (NSAttributedString(string: text), true)
|
|
case .location:
|
|
return (NSAttributedString(string: strings.Message_Location), true)
|
|
case .liveLocation:
|
|
return (NSAttributedString(string: strings.Message_LiveLocation), true)
|
|
case .expiredImage:
|
|
return (NSAttributedString(string: strings.Message_ImageExpired), true)
|
|
case .expiredVideo:
|
|
return (NSAttributedString(string: strings.Message_VideoExpired), true)
|
|
case .expiredVoiceMessage:
|
|
return (NSAttributedString(string: strings.Message_VoiceMessageExpired), true)
|
|
case .expiredVideoMessage:
|
|
return (NSAttributedString(string: strings.Message_VideoMessageExpired), true)
|
|
case let .poll(text):
|
|
return (NSAttributedString(string: "📊 \(text)"), false)
|
|
case let .restricted(text):
|
|
return (NSAttributedString(string: text), false)
|
|
case let .dice(emoji):
|
|
return (NSAttributedString(string: emoji), true)
|
|
case let .invoice(text):
|
|
return (NSAttributedString(string: text), true)
|
|
case .story:
|
|
return (NSAttributedString(string: strings.Message_Story), true)
|
|
case .giveaway:
|
|
return (NSAttributedString(string: strings.Message_Giveaway), true)
|
|
}
|
|
}
|
|
|
|
public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (NSAttributedString, Bool, Bool) {
|
|
let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
|
|
if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) {
|
|
return (foldLineBreaks(messageTextWithAttributes(message: message)), false, true)
|
|
}
|
|
let result = stringForMediaKind(contentKind, strings: strings)
|
|
return (result.0, result.1, false)
|
|
}
|
|
|
|
public func foldLineBreaks(_ text: String) -> String {
|
|
let lines = text.split { $0.isNewline }
|
|
var result = ""
|
|
for line in lines {
|
|
if line.isEmpty {
|
|
continue
|
|
}
|
|
if result.isEmpty {
|
|
result += line
|
|
} else {
|
|
result += " " + line
|
|
}
|
|
}
|
|
result = result.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
|
|
return result
|
|
}
|
|
|
|
public func foldLineBreaks(_ text: NSAttributedString) -> NSAttributedString {
|
|
let remainingString = NSMutableAttributedString(attributedString: text)
|
|
|
|
var lines: [NSAttributedString] = []
|
|
while true {
|
|
if let range = remainingString.string.range(of: "\n") {
|
|
let mappedRange = NSRange(range, in: remainingString.string)
|
|
let restString = remainingString.attributedSubstring(from: NSRange(location: 0, length: mappedRange.upperBound - 1))
|
|
lines.append(restString)
|
|
remainingString.replaceCharacters(in: NSRange(location: 0, length: mappedRange.upperBound), with: "")
|
|
} else {
|
|
if lines.isEmpty {
|
|
return text
|
|
}
|
|
if !remainingString.string.isEmpty {
|
|
lines.append(remainingString)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
let result = NSMutableAttributedString()
|
|
|
|
for line in lines {
|
|
if line.string.isEmpty {
|
|
continue
|
|
}
|
|
if result.string.isEmpty {
|
|
result.append(line)
|
|
} else {
|
|
let currentAttributes = line.attributes(at: 0, effectiveRange: nil).filter { key, _ in
|
|
switch key {
|
|
case .font, .foregroundColor:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
result.append(NSAttributedString(string: " ", attributes: currentAttributes))
|
|
result.append(line)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
public func trimToLineCount(_ text: String, lineCount: Int) -> String {
|
|
if lineCount < 1 {
|
|
return ""
|
|
}
|
|
|
|
var result = ""
|
|
|
|
var i = 0
|
|
text.enumerateLines { line, stop in
|
|
if !result.isEmpty {
|
|
result += "\n"
|
|
}
|
|
result += line
|
|
i += 1
|
|
if i == lineCount {
|
|
stop = true
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|