mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge commit '057a7cc148fb77bcb8f58b8135c8ccb99725b5d4'
This commit is contained in:
commit
1831f1e84b
BIN
Telegram/Telegram-iOS/Resources/Passcode.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Passcode.tgs
Normal file
Binary file not shown.
@ -535,7 +535,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
} else {
|
||||
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
|
||||
}
|
||||
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||
let (_, initialHideAuthor, messageText, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, case .user = author {
|
||||
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).string)"
|
||||
}
|
||||
@ -569,7 +569,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
} else {
|
||||
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
|
||||
}
|
||||
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||
let (_, initialHideAuthor, messageText, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
|
||||
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, case .user = author {
|
||||
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).string)"
|
||||
}
|
||||
@ -965,7 +965,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
let leftInset: CGFloat = params.leftInset + avatarLeftInset
|
||||
|
||||
enum ContentData {
|
||||
case chat(itemPeer: EngineRenderedPeer, peer: EnginePeer?, hideAuthor: Bool, messageText: String)
|
||||
case chat(itemPeer: EngineRenderedPeer, peer: EnginePeer?, hideAuthor: Bool, messageText: String, spoilers: [NSRange]?)
|
||||
case group(peers: [EngineChatList.GroupItem.Item])
|
||||
}
|
||||
|
||||
@ -974,14 +974,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
var hideAuthor = false
|
||||
switch contentPeer {
|
||||
case let .chat(itemPeer):
|
||||
var (peer, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup)
|
||||
var (peer, initialHideAuthor, messageText, spoilers) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup)
|
||||
|
||||
if case let .psa(_, maybePsaText) = promoInfo, let psaText = maybePsaText {
|
||||
initialHideAuthor = true
|
||||
messageText = psaText
|
||||
}
|
||||
|
||||
contentData = .chat(itemPeer: itemPeer, peer: peer, hideAuthor: hideAuthor, messageText: messageText)
|
||||
contentData = .chat(itemPeer: itemPeer, peer: peer, hideAuthor: hideAuthor, messageText: messageText, spoilers: spoilers)
|
||||
hideAuthor = initialHideAuthor
|
||||
case let .group(groupPeers):
|
||||
contentData = .group(peers: groupPeers)
|
||||
@ -1012,7 +1012,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
var contentImageSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = []
|
||||
|
||||
switch contentData {
|
||||
case let .chat(itemPeer, _, _, text):
|
||||
case let .chat(itemPeer, _, _, text, spoilers):
|
||||
var isUser = false
|
||||
if case .user = itemPeer.chatMainPeer {
|
||||
isUser = true
|
||||
@ -1039,7 +1039,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
messageText = currentChatListText.1
|
||||
chatListText = currentChatListText
|
||||
} else {
|
||||
messageText = foldLineBreaks(text)
|
||||
if let spoilers = spoilers, !spoilers.isEmpty {
|
||||
messageText = text
|
||||
} else {
|
||||
messageText = foldLineBreaks(text)
|
||||
}
|
||||
chatListText = (text, messageText)
|
||||
}
|
||||
|
||||
@ -1053,6 +1057,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
} else if let message = messages.last {
|
||||
var composedString: NSMutableAttributedString
|
||||
|
||||
if let peerText = peerText {
|
||||
authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: theme.authorNameColor)
|
||||
}
|
||||
|
||||
let entities = (message._asMessage().textEntitiesAttribute?.entities ?? []).filter { entity in
|
||||
if case .Spoiler = entity.type {
|
||||
return true
|
||||
@ -1062,7 +1070,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
let messageString: NSAttributedString
|
||||
if !message.text.isEmpty && entities.count > 0 {
|
||||
messageString = stringWithAppliedEntities(message.text, entities: entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||
messageString = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: authorAttributedString == nil ? 2 : 1), entities: entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||
} else if let spoilers = spoilers {
|
||||
let mutableString = NSMutableAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)
|
||||
for range in spoilers {
|
||||
mutableString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler), value: true, range: range)
|
||||
}
|
||||
messageString = mutableString
|
||||
} else {
|
||||
messageString = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)
|
||||
}
|
||||
@ -1109,9 +1123,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
attributedText = composedString
|
||||
|
||||
if let peerText = peerText {
|
||||
authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: theme.authorNameColor)
|
||||
}
|
||||
|
||||
|
||||
var displayMediaPreviews = true
|
||||
if message._asMessage().containsSecretMedia {
|
||||
@ -1202,7 +1214,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
switch contentData {
|
||||
case let .chat(itemPeer, _, _, _):
|
||||
case let .chat(itemPeer, _, _, _, _):
|
||||
if let message = messages.last, case let .user(author) = message.author, displayAsMessage {
|
||||
titleAttributedString = NSAttributedString(string: author.id == account.peerId ? item.presentationData.strings.DialogList_You : EnginePeer.user(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: titleFont, textColor: theme.titleColor)
|
||||
} else if isPeerGroup {
|
||||
|
@ -44,13 +44,14 @@ private func messageGroupType(messages: [EngineMessage]) -> MessageGroupType {
|
||||
return currentType
|
||||
}
|
||||
|
||||
public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, messages: [EngineMessage], chatPeer: EngineRenderedPeer, accountPeerId: EnginePeer.Id, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: EnginePeer?, hideAuthor: Bool, messageText: String) {
|
||||
public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, messages: [EngineMessage], chatPeer: EngineRenderedPeer, accountPeerId: EnginePeer.Id, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: EnginePeer?, hideAuthor: Bool, messageText: String, spoilers: [NSRange]?) {
|
||||
let peer: EnginePeer?
|
||||
|
||||
let message = messages.last
|
||||
|
||||
var hideAuthor = false
|
||||
var messageText: String
|
||||
var spoilers: [NSRange]?
|
||||
if let message = message {
|
||||
if let messageMain = messageMainPeer(message) {
|
||||
peer = messageMain
|
||||
@ -268,12 +269,13 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
||||
}
|
||||
default:
|
||||
hideAuthor = true
|
||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||
if let (text, textSpoilers) = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||
messageText = text
|
||||
spoilers = textSpoilers
|
||||
}
|
||||
}
|
||||
case _ as TelegramMediaExpiredContent:
|
||||
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||
if let (text, _) = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) {
|
||||
messageText = text
|
||||
}
|
||||
case let poll as TelegramMediaPoll:
|
||||
@ -312,5 +314,5 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
||||
}
|
||||
}
|
||||
|
||||
return (peer, hideAuthor, messageText)
|
||||
return (peer, hideAuthor, messageText, spoilers)
|
||||
}
|
||||
|
@ -409,8 +409,15 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
let overlayContainerLayout = layout
|
||||
|
||||
if let inCallStatusBar = self.inCallStatusBar {
|
||||
var inCallStatusBarFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(layout.statusBarHeight ?? 0.0, max(40.0, layout.safeInsets.top))))
|
||||
if layout.deviceMetrics.hasTopNotch {
|
||||
let isLandscape = layout.size.width > layout.size.height
|
||||
var minHeight: CGFloat
|
||||
if case .compact = layout.metrics.widthClass, isLandscape {
|
||||
minHeight = 22.0
|
||||
} else {
|
||||
minHeight = 40.0
|
||||
}
|
||||
var inCallStatusBarFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(layout.statusBarHeight ?? 0.0, max(minHeight, layout.safeInsets.top))))
|
||||
if layout.deviceMetrics.hasTopNotch && !isLandscape {
|
||||
inCallStatusBarFrame.size.height += 12.0
|
||||
}
|
||||
if inCallStatusBar.frame.isEmpty {
|
||||
|
@ -182,10 +182,12 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
var nodeHeight: CGFloat = (self.peerNodes.isEmpty ? 264.0 : 364.0)
|
||||
|
||||
let showPeers = !self.peerNodes.isEmpty && !isLandscape
|
||||
var nodeHeight: CGFloat = (!showPeers ? 236.0 : 320.0)
|
||||
let paddedSize = CGSize(width: size.width - 60.0, height: size.height)
|
||||
|
||||
self.peersScrollNode.isHidden = !showPeers
|
||||
|
||||
var aboutSize: CGSize?
|
||||
var descriptionSize: CGSize?
|
||||
if self.aboutNode.supernode != nil {
|
||||
@ -210,14 +212,16 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer
|
||||
descriptionSize = measuredSize
|
||||
}
|
||||
|
||||
let constrainSize = CGSize(width: size.width - 32.0, height: size.height)
|
||||
let titleSize = self.titleNode.measure(constrainSize)
|
||||
nodeHeight += titleSize.height
|
||||
|
||||
let verticalOrigin = size.height - nodeHeight
|
||||
|
||||
let avatarSize: CGFloat = 100.0
|
||||
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floor((size.width - avatarSize) / 2.0), y: verticalOrigin + 32.0), size: CGSize(width: avatarSize, height: avatarSize)))
|
||||
|
||||
let constrainSize = CGSize(width: size.width - 32.0, height: size.height)
|
||||
let titleSize = self.titleNode.measure(constrainSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: verticalOrigin + 27.0 + avatarSize + 15.0), size: titleSize))
|
||||
|
||||
let countSize = self.countNode.measure(constrainSize)
|
||||
@ -246,9 +250,9 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer
|
||||
}
|
||||
|
||||
self.peersScrollNode.view.contentSize = CGSize(width: CGFloat(self.peerNodes.count) * peerSize.width + (self.moreNode != nil ? peerSize.width : 0.0) + peerInset * 2.0, height: peerSize.height)
|
||||
transition.updateFrame(node: self.peersScrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin + 210.0), size: CGSize(width: size.width, height: peerSize.height)))
|
||||
transition.updateFrame(node: self.peersScrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0 + countSize.height + 12.0), size: CGSize(width: size.width, height: peerSize.height)))
|
||||
|
||||
if !self.peerNodes.isEmpty {
|
||||
if showPeers {
|
||||
verticalOffset += 100.0
|
||||
}
|
||||
|
||||
|
@ -95,6 +95,8 @@ swift_library(
|
||||
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
|
||||
"//submodules/Translate:Translate",
|
||||
"//submodules/QrCodeUI:QrCodeUI",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -13,6 +13,15 @@ enum PrivacyIntroControllerMode {
|
||||
case passcode
|
||||
case twoStepVerification
|
||||
|
||||
var animationName: String? {
|
||||
switch self {
|
||||
case .passcode:
|
||||
return "Passcode"
|
||||
case .twoStepVerification:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func icon(theme: PresentationTheme) -> UIImage? {
|
||||
switch self {
|
||||
case .passcode:
|
||||
|
@ -8,6 +8,8 @@ import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import AuthorizationUI
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
|
||||
private func generateButtonImage(backgroundColor: UIColor, highlightColor: UIColor?) -> UIImage? {
|
||||
return generateImage(CGSize(width: 24.0, height: 44.0), contextGenerator: { size, context in
|
||||
@ -39,6 +41,7 @@ final class PrivacyIntroControllerNode: ViewControllerTracingNode {
|
||||
private let proceedAction: () -> Void
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
private let animationNode: AnimatedStickerNode
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
@ -55,6 +58,8 @@ final class PrivacyIntroControllerNode: ViewControllerTracingNode {
|
||||
self.proceedAction = proceedAction
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.animationNode = AnimatedStickerNode()
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.textNode = ASTextNode()
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
@ -68,7 +73,19 @@ final class PrivacyIntroControllerNode: ViewControllerTracingNode {
|
||||
|
||||
super.init()
|
||||
|
||||
if let animationName = mode.animationName {
|
||||
self.iconNode.isHidden = true
|
||||
self.animationNode.isHidden = false
|
||||
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 380, height: 380, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationNode.visibility = true
|
||||
} else {
|
||||
self.iconNode.isHidden = false
|
||||
self.animationNode.isHidden = true
|
||||
}
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.animationNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.buttonBackgroundNode)
|
||||
@ -98,7 +115,9 @@ final class PrivacyIntroControllerNode: ViewControllerTracingNode {
|
||||
self.presentationData = presentationData
|
||||
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||
|
||||
self.iconNode.image = self.mode.icon(theme: presentationData.theme)
|
||||
if self.animationNode.isHidden {
|
||||
self.iconNode.image = self.mode.icon(theme: presentationData.theme)
|
||||
}
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.mode.title(strings: presentationData.strings), font: titleFont, textColor: presentationData.theme.list.sectionHeaderTextColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.mode.text(strings: presentationData.strings), font: textFont, textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center)
|
||||
self.noticeNode.attributedText = NSAttributedString(string: self.mode.notice(strings: presentationData.strings), font: textFont, textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center)
|
||||
@ -120,7 +139,18 @@ final class PrivacyIntroControllerNode: ViewControllerTracingNode {
|
||||
insets.top += navigationBarHeight
|
||||
|
||||
var iconSize = CGSize()
|
||||
if let size = self.iconNode.image?.size {
|
||||
var animationSize = CGSize()
|
||||
if !self.animationNode.isHidden {
|
||||
animationSize = CGSize(width: 180.0, height: 180.0)
|
||||
self.animationNode.updateLayout(size: animationSize)
|
||||
|
||||
var iconAlpha: CGFloat = 1.0
|
||||
if case .compact = layout.metrics.widthClass, layout.size.width > layout.size.height {
|
||||
iconAlpha = 0.0
|
||||
iconSize = CGSize()
|
||||
}
|
||||
transition.updateAlpha(node: self.animationNode, alpha: iconAlpha)
|
||||
} else if let size = self.iconNode.image?.size {
|
||||
iconSize = size
|
||||
|
||||
var iconAlpha: CGFloat = 1.0
|
||||
@ -142,9 +172,10 @@ final class PrivacyIntroControllerNode: ViewControllerTracingNode {
|
||||
} else {
|
||||
buttonInset = 0.0
|
||||
}
|
||||
|
||||
|
||||
let items: [AuthorizationLayoutItem] = [
|
||||
AuthorizationLayoutItem(node: self.iconNode, size: iconSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
|
||||
AuthorizationLayoutItem(node: self.animationNode, size: animationSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
|
||||
AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 20.0, maxValue: 30.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
|
||||
AuthorizationLayoutItem(node: self.textNode, size: textSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 16.0, maxValue: 16.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
|
||||
AuthorizationLayoutItem(node: self.buttonNode, size: CGSize(width: layout.size.width - buttonInset * 2.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 40.0, maxValue: 40.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
|
||||
|
@ -200,17 +200,17 @@ private final class CallVideoNode: ASDisplayNode, PreviewVideoNode {
|
||||
case .rotation90:
|
||||
rotationAngle = CGFloat.pi / 2.0
|
||||
case .rotation180:
|
||||
if isCompactLayout {
|
||||
// if isCompactLayout {
|
||||
rotationAngle = CGFloat.pi
|
||||
} else {
|
||||
rotationAngle = 0.0
|
||||
}
|
||||
// } else {
|
||||
// rotationAngle = 0.0
|
||||
// }
|
||||
case .rotation270:
|
||||
if isCompactLayout {
|
||||
// if isCompactLayout {
|
||||
rotationAngle = -CGFloat.pi / 2.0
|
||||
} else {
|
||||
rotationAngle = CGFloat.pi / 2.0
|
||||
}
|
||||
// } else {
|
||||
// rotationAngle = CGFloat.pi / 2.0
|
||||
// }
|
||||
}
|
||||
|
||||
var additionalAngle: CGFloat = 0.0
|
||||
|
@ -165,7 +165,7 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil
|
||||
}
|
||||
case .action:
|
||||
if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId {
|
||||
return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false) ?? "")
|
||||
return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false)?.0 ?? "")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -253,3 +253,20 @@ public func foldLineBreaks(_ text: String) -> String {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
public func trimToLineCount(_ text: String, lineCount: Int) -> String {
|
||||
if lineCount < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
let lines = text.split { $0.isNewline }
|
||||
var result = ""
|
||||
for line in lines.prefix(lineCount) {
|
||||
if !result.isEmpty {
|
||||
result += "\n"
|
||||
}
|
||||
result += line
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -11,6 +11,10 @@ import Markdown
|
||||
private let titleFont = Font.regular(13.0)
|
||||
private let titleBoldFont = Font.bold(13.0)
|
||||
|
||||
private func spoilerAttributes(primaryTextColor: UIColor) -> MarkdownAttributeSet {
|
||||
return MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [TelegramTextAttributes.Spoiler: true])
|
||||
}
|
||||
|
||||
private func peerMentionAttributes(primaryTextColor: UIColor, peerId: EnginePeer.Id) -> MarkdownAttributeSet {
|
||||
return MarkdownAttributeSet(font: titleBoldFont, textColor: primaryTextColor, additionalAttributes: [TelegramTextAttributes.PeerMention: TelegramPeerMention(peerId: peerId, mention: "")])
|
||||
}
|
||||
@ -25,8 +29,18 @@ private func peerMentionsAttributes(primaryTextColor: UIColor, peerIds: [(Int, E
|
||||
return result
|
||||
}
|
||||
|
||||
public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool) -> String? {
|
||||
return universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: forChatList)?.string
|
||||
public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool) -> (String, [NSRange])? {
|
||||
if let attributedString = universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: forChatList) {
|
||||
var ranges: [NSRange] = []
|
||||
attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: [], using: { attributes, range, _ in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)] {
|
||||
ranges.append(range)
|
||||
}
|
||||
})
|
||||
return (attributedString.string, ranges)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool) -> NSAttributedString? {
|
||||
@ -137,7 +151,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
}
|
||||
case .pinnedMessageUpdated:
|
||||
enum PinnnedMediaType {
|
||||
case text(String)
|
||||
case text(String, [MessageTextEntity])
|
||||
case game
|
||||
case photo
|
||||
case video
|
||||
@ -160,8 +174,15 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
}
|
||||
|
||||
var type: PinnnedMediaType
|
||||
if let pinnedMessage = pinnedMessage {
|
||||
type = .text(pinnedMessage.text)
|
||||
if let pinnedMessage = pinnedMessage?._asMessage() {
|
||||
let entities = (pinnedMessage.textEntitiesAttribute?.entities ?? []).filter { entity in
|
||||
if case .Spoiler = entity.type {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
type = .text(pinnedMessage.text, entities)
|
||||
inner: for media in pinnedMessage.media {
|
||||
if media is TelegramMediaGame {
|
||||
type = .game
|
||||
@ -213,8 +234,13 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
}
|
||||
|
||||
switch type {
|
||||
case let .text(text):
|
||||
var clippedText = text.replacingOccurrences(of: "\n", with: " ")
|
||||
case let .text(text, entities):
|
||||
var clippedText = text
|
||||
if !entities.isEmpty {
|
||||
clippedText = trimToLineCount(clippedText, lineCount: 1)
|
||||
} else {
|
||||
clippedText = clippedText.replacingOccurrences(of: "\n", with: " ")
|
||||
}
|
||||
if clippedText.count > 14 {
|
||||
clippedText = "\(clippedText[...clippedText.index(clippedText.startIndex, offsetBy: 14)])..."
|
||||
}
|
||||
@ -224,7 +250,21 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
} else {
|
||||
textWithRanges = strings.Notification_PinnedTextMessage(authorName, clippedText)
|
||||
}
|
||||
attributedString = addAttributesToStringWithRanges(textWithRanges._tuple, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]))
|
||||
|
||||
let string = textWithRanges._tuple.0
|
||||
let stringLength = (clippedText as NSString).length
|
||||
var ranges = textWithRanges._tuple.1
|
||||
let entityOffset = ranges.first(where: { $0.0 == 1 })?.1.location ?? 0
|
||||
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])
|
||||
for entity in entities {
|
||||
if entity.range.startIndex >= stringLength {
|
||||
continue
|
||||
}
|
||||
let index = ranges.count
|
||||
ranges.append((ranges.count, NSRange(location: entityOffset + entity.range.startIndex, length: entity.range.count)))
|
||||
attributes[index] = spoilerAttributes(primaryTextColor: primaryTextColor)
|
||||
}
|
||||
attributedString = addAttributesToStringWithRanges((string, ranges), body: bodyAttributes, argumentAttributes: attributes)
|
||||
case .game:
|
||||
attributedString = addAttributesToStringWithRanges(strings.Message_AuthorPinnedGame(authorName)._tuple, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]))
|
||||
case .photo:
|
||||
|
File diff suppressed because one or more lines are too long
@ -3961,7 +3961,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
var renderedPeer: RenderedPeer?
|
||||
var contactStatus: ChatContactStatus?
|
||||
var copyProtectionEnabled: Bool = false
|
||||
if let peer = peerView.peers[peerView.peerId] {
|
||||
copyProtectionEnabled = peer.isCopyProtectionEnabled
|
||||
if let cachedData = peerView.cachedData as? CachedUserData {
|
||||
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil)
|
||||
} else if let cachedData = peerView.cachedData as? CachedGroupData {
|
||||
@ -4087,6 +4089,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return $0.updatedPeer { _ in
|
||||
return renderedPeer
|
||||
}.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(false).updatedCurrentSendAsPeerId(currentSendAsPeerId)
|
||||
.updatedCopyProtectionEnabled(copyProtectionEnabled)
|
||||
})
|
||||
if !strongSelf.didSetChatLocationInfoReady {
|
||||
strongSelf.didSetChatLocationInfoReady = true
|
||||
@ -8715,7 +8718,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
interfaceState = interfaceState.withUpdatedHistoryScrollState(scrollState)
|
||||
}
|
||||
interfaceState = interfaceState.withUpdatedInputLanguage(self.chatDisplayNode.currentTextInputLanguage)
|
||||
if interfaceState.composeInputState.inputText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
if interfaceState.composeInputState.inputText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && interfaceState.replyMessageId == nil {
|
||||
interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "")))
|
||||
}
|
||||
let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: threadId, { _ in
|
||||
|
@ -277,6 +277,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
if attribute is ViewCountMessageAttribute{
|
||||
return false
|
||||
}
|
||||
if attribute is ForwardCountMessageAttribute {
|
||||
return false
|
||||
}
|
||||
if attribute is ReactionsMessageAttribute {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
|
@ -17,6 +17,7 @@ import UniversalMediaPlayer
|
||||
import TelegramUniversalVideoContent
|
||||
import GalleryUI
|
||||
import WallpaperBackgroundNode
|
||||
import InvisibleInkDustNode
|
||||
|
||||
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
|
||||
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), accountPeerId: accountPeerId, forChatList: false)
|
||||
@ -24,6 +25,7 @@ private func attributedServiceMessageString(theme: ChatPresentationThemeData, st
|
||||
|
||||
class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let labelNode: TextNode
|
||||
private var dustNode: InvisibleInkDustNode?
|
||||
var backgroundNode: WallpaperBubbleBackgroundNode?
|
||||
var backgroundColorNode: ASDisplayNode
|
||||
let backgroundMaskNode: ASImageNode
|
||||
@ -277,6 +279,25 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.labelNode.frame = labelFrame
|
||||
strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
|
||||
if !labelLayout.spoilers.isEmpty {
|
||||
let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
|
||||
|
||||
let dustNode: InvisibleInkDustNode
|
||||
if let current = strongSelf.dustNode {
|
||||
dustNode = current
|
||||
} else {
|
||||
dustNode = InvisibleInkDustNode(textNode: nil)
|
||||
dustNode.isUserInteractionEnabled = false
|
||||
strongSelf.dustNode = dustNode
|
||||
strongSelf.insertSubnode(dustNode, aboveSubnode: strongSelf.labelNode)
|
||||
}
|
||||
dustNode.frame = labelFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 1.0)
|
||||
dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: labelLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: labelLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
||||
} else if let dustNode = strongSelf.dustNode {
|
||||
dustNode.removeFromSupernode()
|
||||
strongSelf.dustNode = nil
|
||||
}
|
||||
|
||||
let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
|
||||
|
||||
if let (offset, image) = backgroundMaskImage {
|
||||
|
@ -206,7 +206,7 @@ final class ChatMessageAccessibilityData {
|
||||
if let chatPeer = message.peers[item.message.id.peerId] {
|
||||
let authorName = message.author.flatMap(EnginePeer.init)?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
|
||||
let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: [EngineMessage(message)], chatPeer: EngineRenderedPeer(peer: EnginePeer(chatPeer)), accountPeerId: item.context.account.peerId)
|
||||
let (_, _, messageText, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: [EngineMessage(message)], chatPeer: EngineRenderedPeer(peer: EnginePeer(chatPeer)), accountPeerId: item.context.account.peerId)
|
||||
|
||||
var text = messageText
|
||||
|
||||
|
@ -876,6 +876,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.votersNode.contentMode = .topLeft
|
||||
self.votersNode.contentsScale = UIScreenScale
|
||||
self.votersNode.displaysAsynchronously = false
|
||||
self.votersNode.clipsToBounds = true
|
||||
|
||||
var displaySolution: (() -> Void)?
|
||||
self.solutionButtonNode = SolutionButtonNode(pressed: {
|
||||
|
@ -106,7 +106,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
if entities.count > 0 {
|
||||
messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||
messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||
} else {
|
||||
messageText = NSAttributedString(string: textString, font: textFont, textColor: textColor)
|
||||
}
|
||||
|
@ -468,7 +468,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
}
|
||||
let textColor = theme.chat.inputPanel.primaryTextColor
|
||||
if entities.count > 0 {
|
||||
messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||
messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||
} else {
|
||||
messageText = NSAttributedString(string: foldLineBreaks(textString), font: textFont, textColor: textColor)
|
||||
}
|
||||
|
@ -1597,9 +1597,9 @@ private class QrContentNode: ASDisplayNode, ContentNode {
|
||||
|
||||
self.codeTextNode.attributedText = NSAttributedString(string: self.codeTextNode.attributedText?.string ?? "", font: Font.with(size: fontSize, design: .round, weight: .bold, traits: []), textColor: .black)
|
||||
|
||||
let codeBackgroundWidth = size.width - codeInset * 2.0
|
||||
let codeBackgroundWidth = min(300.0, size.width - codeInset * 2.0)
|
||||
let codeBackgroundHeight = floor(codeBackgroundWidth * 1.1)
|
||||
let codeBackgroundFrame = CGRect(x: codeInset, y: topInset + floor((size.height - bottomInset - codeBackgroundHeight) / 2.0), width: codeBackgroundWidth, height: codeBackgroundHeight)
|
||||
let codeBackgroundFrame = CGRect(x: floor((size.width - codeBackgroundWidth) / 2.0), y: topInset + floor((size.height - bottomInset - codeBackgroundHeight) / 2.0), width: codeBackgroundWidth, height: codeBackgroundHeight)
|
||||
transition.updateFrame(node: self.codeBackgroundNode, frame: codeBackgroundFrame)
|
||||
transition.updateFrame(node: self.codeForegroundNode, frame: codeBackgroundFrame)
|
||||
transition.updateFrame(node: self.codeMaskNode, frame: CGRect(origin: CGPoint(), size: codeBackgroundFrame.size))
|
||||
|
@ -49,6 +49,9 @@ private func chatInputStateString(attributedString: NSAttributedString) -> NSAtt
|
||||
string.addAttribute(ChatTextInputAttributes.monospace, value: true as NSNumber, range: range)
|
||||
}
|
||||
}
|
||||
if let value = attributes[.backgroundColor] as? UIColor, value.rgb == UIColor.gray.rgb {
|
||||
string.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: range)
|
||||
}
|
||||
if let _ = attributes[.strikethroughStyle] {
|
||||
string.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: range)
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
}
|
||||
let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor
|
||||
if entities.count > 0 {
|
||||
messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||
messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||
} else {
|
||||
messageText = NSAttributedString(string: text, font: textFont, textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor)
|
||||
}
|
||||
|
@ -69,7 +69,9 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
|
||||
if nsString == nil {
|
||||
nsString = text as NSString
|
||||
}
|
||||
if range.location + range.length > stringLength {
|
||||
if range.location > stringLength {
|
||||
continue
|
||||
} else if range.location + range.length > stringLength {
|
||||
range.location = max(0, stringLength - range.length)
|
||||
range.length = stringLength - range.location
|
||||
}
|
||||
@ -226,7 +228,11 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
|
||||
}
|
||||
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard), value: nsString!.substring(with: range), range: range)
|
||||
case .Spoiler:
|
||||
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler), value: true as NSNumber, range: range)
|
||||
if external {
|
||||
string.addAttribute(NSAttributedString.Key.backgroundColor, value: UIColor.gray, range: range)
|
||||
} else {
|
||||
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler), value: true as NSNumber, range: range)
|
||||
}
|
||||
case let .Custom(type):
|
||||
if type == ApplicationSpecificEntityType.Timecode {
|
||||
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
|
||||
|
@ -11,11 +11,12 @@ private final class LinkHelperClass: NSObject {
|
||||
public var supportedTranslationLanguages = [
|
||||
"en",
|
||||
"ar",
|
||||
"zh",
|
||||
"zh-Hans",
|
||||
"zh-Hant",
|
||||
"fr",
|
||||
"de",
|
||||
"it",
|
||||
"jp",
|
||||
"ja",
|
||||
"ko",
|
||||
"pt",
|
||||
"ru",
|
||||
|
Loading…
x
Reference in New Issue
Block a user