Files
Swiftgram/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift
Kylmakalle fd86110711 Version 11.3.1
Fixes

fix localeWithStrings globally (#30)

Fix badge on zoomed devices. closes #9

Hide channel bottom panel closes #27

Another attempt to fix badge on some Zoomed devices

Force System Share sheet tg://sg/debug

fixes for device badge

New Crowdin updates (#34)

* New translations sglocalizable.strings (Chinese Traditional)

* New translations sglocalizable.strings (Chinese Simplified)

* New translations sglocalizable.strings (Chinese Traditional)

Fix input panel hidden on selection (#31)

* added if check for selectionState != nil

* same order of subnodes

Revert "Fix input panel hidden on selection (#31)"

This reverts commit e8a8bb1496.

Fix input panel for channels Closes #37

Quickly share links with system's share menu

force tabbar when editing

increase height for correct animation

New translations sglocalizable.strings (Ukrainian) (#38)

Hide Post Story button

Fix 10.15.1

Fix archive option for long-tap

Enable in-app Safari

Disable some unsupported purchases

disableDeleteChatSwipeOption + refactor restart alert

Hide bot in suggestions list

Fix merge v11.0

Fix exceptions for safari webview controller

New Crowdin updates (#47)

* New translations sglocalizable.strings (Romanian)

* New translations sglocalizable.strings (French)

* New translations sglocalizable.strings (Spanish)

* New translations sglocalizable.strings (Afrikaans)

* New translations sglocalizable.strings (Arabic)

* New translations sglocalizable.strings (Catalan)

* New translations sglocalizable.strings (Czech)

* New translations sglocalizable.strings (Danish)

* New translations sglocalizable.strings (German)

* New translations sglocalizable.strings (Greek)

* New translations sglocalizable.strings (Finnish)

* New translations sglocalizable.strings (Hebrew)

* New translations sglocalizable.strings (Hungarian)

* New translations sglocalizable.strings (Italian)

* New translations sglocalizable.strings (Japanese)

* New translations sglocalizable.strings (Korean)

* New translations sglocalizable.strings (Dutch)

* New translations sglocalizable.strings (Norwegian)

* New translations sglocalizable.strings (Polish)

* New translations sglocalizable.strings (Portuguese)

* New translations sglocalizable.strings (Serbian (Cyrillic))

* New translations sglocalizable.strings (Swedish)

* New translations sglocalizable.strings (Turkish)

* New translations sglocalizable.strings (Vietnamese)

* New translations sglocalizable.strings (Indonesian)

* New translations sglocalizable.strings (Hindi)

* New translations sglocalizable.strings (Uzbek)

New Crowdin updates (#49)

* New translations sglocalizable.strings (Arabic)

* New translations sglocalizable.strings (Arabic)

New translations sglocalizable.strings (Russian) (#51)

Call confirmation

WIP Settings search

Settings Search

Localize placeholder

Update AccountUtils.swift

mark mutual contact

Align back context action to left

New Crowdin updates (#54)

* New translations sglocalizable.strings (Chinese Simplified)

* New translations sglocalizable.strings (Chinese Traditional)

* New translations sglocalizable.strings (Ukrainian)

Independent Playground app for simulator

New translations sglocalizable.strings (Ukrainian) (#55)

Playground UIKit base and controllers

Inject SwiftUI view with overflow to AsyncDisplayKit

Launch Playgound project on simulator

Create .swiftformat

Move Playground to example

Update .swiftformat

Init SwiftUIViewController

wip

New translations sglocalizable.strings (Chinese Traditional) (#57)

Xcode 16 fixes

Fix

New translations sglocalizable.strings (Italian) (#59)

New translations sglocalizable.strings (Chinese Simplified) (#63)

Force disable CallKit integration due to missing NSE Entitlement

Fix merge

Fix whole chat translator

Sweetpad config

Bump version

11.3.1 fixes

Mutual contact placement fix

Disable Video PIP swipe

Update versions.json

Fix PIP crash
2024-12-20 09:38:13 +02:00

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, chatId: message.author?.id.id._internalGetInt64Value()) {
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
}