Fact check

This commit is contained in:
Ilya Laktyushin 2024-05-20 09:33:53 +04:00
parent cf15c15e0e
commit abe1e40e2a
31 changed files with 1354 additions and 107 deletions

View File

@ -12206,3 +12206,14 @@ Sorry for the inconvenience.";
"Chat.Context.Phone.CopyNumber" = "Copy Number";
"Chat.Context.Phone.NotOnTelegram" = "This number is not on Telegram.";
"Chat.Context.Phone.ViewProfile" = "View Profile";
"Message.FactCheck" = "Fact Check";
"Message.FactCheck.WhatIsThis" = "what's this?";
"Conversation.FactCheck.Description" = "This clarification was provided by a fact checking agency assigned by the department of the government of your country (%@) responsible for combating misinformation.";
"FactCheck.Title" = "Fact Check";
"FactCheck.Placeholder" = "Add Fact Check";
"Conversation.ContextMenuAddFactCheck" = "Add Fact Check";
"Conversation.ContextMenuEditFactCheck" = "Edit Fact Check";

View File

@ -566,6 +566,7 @@ public enum PeerInfoControllerMode {
case reaction(MessageId)
case forumTopic(thread: ChatReplyThreadMessage)
case recommendedChannels
case myProfile
}
public enum ContactListActionItemInlineIconPosition {

View File

@ -13,7 +13,8 @@ final class AlertControllerNode: ASDisplayNode {
private let rightDimView: UIView
private let containerNode: ASDisplayNode
private let effectNode: ASDisplayNode
// private let effectNode: ASDisplayNode
private let effectView: UIVisualEffectView
private let backgroundNode: ASDisplayNode
private let contentNode: AlertContentNode
private let allowInputInset: Bool
@ -51,9 +52,11 @@ final class AlertControllerNode: ASDisplayNode {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = theme.backgroundColor
self.effectNode = ASDisplayNode(viewBlock: {
return UIVisualEffectView(effect: UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark))
})
// self.effectNode = ASDisplayNode(viewBlock: {
// return UIVisualEffectView(effect: UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark))
// })
self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark))
self.contentNode = contentNode
@ -66,7 +69,8 @@ final class AlertControllerNode: ASDisplayNode {
self.dimContainerView.addSubview(self.leftDimView)
self.dimContainerView.addSubview(self.rightDimView)
self.containerNode.addSubnode(self.effectNode)
self.containerNode.view.addSubview(self.effectView)
// self.containerNode.addSubnode(self.effectNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.contentNode)
self.addSubnode(self.containerNode)
@ -100,9 +104,7 @@ final class AlertControllerNode: ASDisplayNode {
}
func updateTheme(_ theme: AlertControllerTheme) {
if let effectView = self.effectNode.view as? UIVisualEffectView {
effectView.effect = UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)
}
self.effectView.effect = UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)
self.backgroundNode.backgroundColor = theme.backgroundColor
self.contentNode.updateTheme(theme)
}
@ -186,7 +188,10 @@ final class AlertControllerNode: ASDisplayNode {
transition.updateFrame(view: self.rightDimView, frame: CGRect(origin: CGPoint(x: containerFrame.maxX, y: containerFrame.minY), size: CGSize(width: layout.size.width - containerFrame.maxX + outerEdge, height: containerFrame.height)))
transition.updateFrame(node: self.containerNode, frame: containerFrame)
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
transition.animateView {
self.effectView.frame = CGRect(origin: CGPoint(), size: containerFrame.size)
}
// transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
}

View File

@ -5,6 +5,17 @@ import TelegramCore
private let phoneNumberUtil = NBPhoneNumberUtil()
public func enhancePhoneNumberWithCodeFromNumber(_ phoneNumber: String, otherPhoneNumber: String, configuration: CountriesConfiguration) -> String {
guard let (_, code) = lookupCountryIdByNumber(otherPhoneNumber, configuration: configuration) else {
return phoneNumber
}
var cleanNumber = cleanPhoneNumber(phoneNumber)
while cleanNumber.hasPrefix("0") {
cleanNumber.removeFirst()
}
return "+\(code)\(cleanNumber)"
}
public func cleanPhoneNumber(_ text: String, removePlus: Bool = false) -> String {
var result = ""
for c in text {

View File

@ -224,6 +224,7 @@ private var declaredEncodables: Void = {
declareEncodable(TelegramApplicationIcons.self, f: { TelegramApplicationIcons(decoder: $0) })
declareEncodable(OutgoingQuickReplyMessageAttribute.self, f: { OutgoingQuickReplyMessageAttribute(decoder: $0) })
declareEncodable(EffectMessageAttribute.self, f: { EffectMessageAttribute(decoder: $0) })
declareEncodable(FactCheckMessageAttribute.self, f: { FactCheckMessageAttribute(decoder: $0) })
return
}()

View File

@ -609,7 +609,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes
extension StoreMessage {
convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) {
switch apiMessage {
case let .message(flags, flags2, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId, messageEffectId, _):
case let .message(flags, flags2, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId, messageEffectId, factCheck):
let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId
var namespace = namespace
@ -897,6 +897,22 @@ extension StoreMessage {
if let messageEffectId {
attributes.append(EffectMessageAttribute(id: messageEffectId))
}
if let factCheck {
switch factCheck {
case let .factCheck(_, country, text, hash):
let content: FactCheckMessageAttribute.Content
if let text, let country {
switch text {
case let .textWithEntities(text, entities):
content = .Loaded(text: text, entities: messageTextEntitiesFromApiEntities(entities), country: country)
}
} else {
content = .Pending
}
attributes.append(FactCheckMessageAttribute(content: content, hash: hash))
}
}
var storeFlags = StoreMessageFlags()

View File

@ -0,0 +1,73 @@
import Postbox
public class FactCheckMessageAttribute: MessageAttribute, Equatable {
public enum Content: PostboxCoding, Equatable {
case Pending
case Loaded(text: String, entities: [MessageTextEntity], country: String)
public init(decoder: PostboxDecoder) {
switch decoder.decodeInt32ForKey("_v", orElse: 0) {
case 0:
self = .Pending
case 1:
self = .Loaded(
text: decoder.decodeStringForKey("text", orElse: ""),
entities: decoder.decodeObjectArrayWithDecoderForKey("entities"),
country: decoder.decodeStringForKey("country", orElse: "")
)
default:
assertionFailure()
self = .Pending
}
}
public func encode(_ encoder: PostboxEncoder) {
switch self {
case .Pending:
encoder.encodeInt32(0, forKey: "_v")
case let .Loaded(text, entities, country):
encoder.encodeInt32(1, forKey: "_v")
encoder.encodeString(text, forKey: "text")
encoder.encodeObjectArray(entities, forKey: "entities")
encoder.encodeString(country, forKey: "country")
}
}
}
public let content: Content
public let hash: Int64
public var associatedPeerIds: [PeerId] {
return []
}
public init(
content: Content,
hash: Int64
) {
self.content = content
self.hash = hash
}
required public init(decoder: PostboxDecoder) {
self.content = decoder.decodeObjectForKey("content", decoder: { FactCheckMessageAttribute.Content(decoder: $0) }) as! FactCheckMessageAttribute.Content
self.hash = decoder.decodeInt64ForKey("hash", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObject(self.content, forKey: "content")
encoder.encodeInt64(self.hash, forKey: "hash")
}
public static func ==(lhs: FactCheckMessageAttribute, rhs: FactCheckMessageAttribute) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.hash != rhs.hash {
return false
}
return true
}
}

View File

@ -0,0 +1,123 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func _internal_editMessageFactCheck(account: Account, messageId: EngineMessage.Id, text: String, entities: [MessageTextEntity]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer else {
return .complete()
}
return account.network.request(Api.functions.messages.editFactCheck(
peer: inputPeer,
msgId: messageId.id,
text: .textWithEntities(
text: text,
entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary())
)
))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
}
func _internal_deleteMessageFactCheck(account: Account, messageId: EngineMessage.Id) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer else {
return .complete()
}
return account.network.request(Api.functions.messages.deleteFactCheck(peer: inputPeer, msgId: messageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
}
func _internal_getMessagesFactCheck(account: Account, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
var signals: [Signal<Never, NoError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
signals.append(_internal_getMessagesFactCheckByPeerId(account: account, peerId: peerId, messageIds: messageIds))
}
return combineLatest(signals)
|> ignoreValues
}
func _internal_getMessagesFactCheckByPeerId(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> (Api.InputPeer?, [Message]) in
return (transaction.getPeer(peerId).flatMap(apiInputPeer), messageIds.compactMap({ transaction.getMessage($0) }))
}
|> mapToSignal { (inputPeer, messages) -> Signal<Never, NoError> in
guard let inputPeer = inputPeer else {
return .never()
}
let ids: [Int32] = messageIds.map { $0.id }
let results: Signal<[Api.FactCheck]?, NoError>
if ids.isEmpty {
results = .single(nil)
} else {
results = account.network.request(Api.functions.messages.getFactCheck(peer: inputPeer, msgId: ids))
|> map(Optional.init)
|> `catch` { _ in
return .single(nil)
}
}
return results
|> mapToSignal { results -> Signal<Never, NoError> in
guard let results else {
return .complete()
}
return account.postbox.transaction { transaction in
var index = 0
for result in results {
let messageId = messageIds[index]
switch result {
case let .factCheck(_, country, text, hash):
let content: FactCheckMessageAttribute.Content
if let text, let country {
switch text {
case let .textWithEntities(text, entities):
content = .Loaded(text: text, entities: messageTextEntitiesFromApiEntities(entities), country: country)
}
} else {
content = .Pending
}
let attribute = FactCheckMessageAttribute(content: content, hash: hash)
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes.filter { !($0 is FactCheckMessageAttribute) }
attributes.append(attribute)
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
index += 1
}
}
|> ignoreValues
}
}
}

View File

@ -692,6 +692,18 @@ public extension TelegramEngine {
return _internal_searchForumTopics(account: self.account, peerId: peerId, query: query)
}
public func editMessageFactCheck(messageId: EngineMessage.Id, text: String, entities: [MessageTextEntity]) -> Signal<Never, NoError> {
return _internal_editMessageFactCheck(account: self.account, messageId: messageId, text: text, entities: entities)
}
public func deleteMessageFactCheck(messageId: EngineMessage.Id) -> Signal<Never, NoError> {
return _internal_deleteMessageFactCheck(account: self.account, messageId: messageId)
}
public func getMessagesFactCheck(messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
return _internal_getMessagesFactCheck(account: self.account, messageIds: messageIds)
}
public func debugAddHoles() -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
transaction.addHolesEverywhere(peerNamespaces: [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel], holeNamespace: Namespaces.Message.Cloud)

View File

@ -451,6 +451,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stars/StarsTransactionsScreen",
"//submodules/TelegramUI/Components/Stars/StarsPurchaseScreen",
"//submodules/TelegramUI/Components/Stars/StarsTransferScreen",
"//submodules/TelegramUI/Components/Chat/FactCheckAlertController",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -80,6 +80,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode",
"//submodules/UIKitRuntimeUtils",
"//submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode",
"//submodules/AnimatedStickerNode",

View File

@ -70,6 +70,7 @@ import ChatMessageWallpaperBubbleContentNode
import ChatMessageGiftBubbleContentNode
import ChatMessageGiveawayBubbleContentNode
import ChatMessageJoinedChannelBubbleContentNode
import ChatMessageFactCheckBubbleContentNode
import UIKitRuntimeUtils
import ChatMessageTransitionNode
import AnimatedStickerNode
@ -286,7 +287,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
break inner
}
}
if message.adAttribute != nil {
result.removeAll()
@ -297,6 +298,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
if isUnsupportedMedia {
result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
} else {
for attribute in message.attributes {
if let attribute = attribute as? FactCheckMessageAttribute, case .Loaded = attribute.content {
result.append((message, ChatMessageFactCheckBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
break
}
}
}
}

View File

@ -0,0 +1,29 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageFactCheckBubbleContentNode",
module_name = "ChatMessageFactCheckBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Postbox",
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,341 @@
import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TextFormat
import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import MessageInlineBlockBackgroundView
public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode {
private let backgroundView: MessageInlineBlockBackgroundView
private var titleNode: TextNode
private var titleBadgeLabel: TextNode
private var titleBadgeButton: HighlightTrackingButtonNode?
private let textNode: TextNode
private let statusNode: ChatMessageDateAndStatusNode
required public init() {
self.backgroundView = MessageInlineBlockBackgroundView()
self.titleNode = TextNode()
self.titleBadgeLabel = TextNode()
self.textNode = TextNode()
self.statusNode = ChatMessageDateAndStatusNode()
super.init()
self.view.addSubview(self.backgroundView)
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .topLeft
self.titleNode.contentsScale = UIScreenScale
self.titleNode.displaysAsynchronously = false
self.addSubnode(self.titleNode)
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .topLeft
self.textNode.contentsScale = UIScreenScale
self.textNode.displaysAsynchronously = false
self.addSubnode(self.textNode)
self.titleBadgeLabel.isUserInteractionEnabled = false
self.titleBadgeLabel.contentMode = .topLeft
self.titleBadgeLabel.contentsScale = UIScreenScale
self.titleBadgeLabel.displaysAsynchronously = false
self.addSubnode(self.titleBadgeLabel)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func badgePressed() {
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let titleLayout = TextNode.asyncLayout(self.titleNode)
let titleBadgeLayout = TextNode.asyncLayout(self.titleBadgeLabel)
let textLayout = TextNode.asyncLayout(self.textNode)
let statusLayout = self.statusNode.asyncLayout()
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let message = item.message
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
let maxTextWidth = CGFloat.greatestFiniteMagnitude
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - horizontalInset * 2.0), height: constrainedSize.height)
var edited = false
if item.attributes.updatingMedia != nil {
edited = true
}
var viewCount: Int?
var rawText = ""
var rawEntities: [MessageTextEntity] = []
var dateReplies = 0
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message)
if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) {
dateReactionsAndPeers = ([], [])
}
for attribute in item.message.attributes {
if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden
} else if let attribute = attribute as? ViewCountMessageAttribute {
viewCount = attribute.count
} else if let attribute = attribute as? FactCheckMessageAttribute, case let .Loaded(text, entities, _) = attribute.content {
rawText = text
rawEntities = entities
} else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
dateReplies = Int(attribute.count)
}
}
}
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
if message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if message.flags.isSending && !message.isSentOrAcknowledged {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
default:
statusType = nil
}
}
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
let fontSize = floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
let textFont = Font.regular(fontSize)
let textBoldFont = Font.semibold(fontSize)
let textItalicFont = Font.italic(fontSize)
let textBoldItalicFont = Font.semiboldItalic(fontSize)
let textFixedFont = Font.regular(fontSize)
let textBlockQuoteFont = Font.regular(fontSize)
let badgeFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0))
let attributedText = stringWithAppliedEntities(rawText, entities: rawEntities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil)
let textInsets = UIEdgeInsets(top: 2.0, left: 0.0, bottom: 5.0, right: 0.0)
var backgroundInsets = UIEdgeInsets()
backgroundInsets.left += layoutConstants.text.bubbleInsets.left
backgroundInsets.right += layoutConstants.text.bubbleInsets.right
let mainColor = messageTheme.scamColor
let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Message_FactCheck, font: textBoldFont, textColor: mainColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: mainColor))
let titleBadgeString = NSAttributedString(string: item.presentationData.strings.Message_FactCheck_WhatIsThis, font: badgeFont, textColor: mainColor)
let (titleBadgeLayout, titleBadgeApply) = titleBadgeLayout(TextNodeLayoutArguments(attributedString: titleBadgeString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize))
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
var titleFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: titleLayout.size)
titleFrame = titleFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left * 2.0 - 2.0, dy: layoutConstants.text.bubbleInsets.top - 3.0)
var titleFrameWithoutInsets = CGRect(origin: CGPoint(x: titleFrame.origin.x + textInsets.left, y: titleFrame.origin.y + textInsets.top), size: CGSize(width: titleFrame.width - textInsets.left - textInsets.right, height: titleFrame.height - textInsets.top - textInsets.bottom))
titleFrameWithoutInsets = titleFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
let textSpacing: CGFloat = 3.0
let textFrame = CGRect(origin: CGPoint(x: titleFrame.origin.x, y: -textInsets.top + titleFrameWithoutInsets.height + textSpacing), size: textLayout.size)
var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom))
textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
if let statusType = statusType {
var isReplyThread = false
if case .replyThread = item.chatLocation {
isReplyThread = true
}
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
context: item.context,
presentationData: item.presentationData,
edited: edited,
impressionCount: viewCount,
dateText: dateText,
type: statusType,
layoutInput: .trailingContent(contentWidth: nil, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions), preferAdditionalInset: false)),
constrainedSize: textConstrainedSize,
availableReactions: item.associatedData.availableReactions,
savedMessageTags: item.associatedData.savedMessageTags,
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId),
messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects),
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
}
var suggestedBoundingWidth: CGFloat = textFrameWithoutInsets.width
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
suggestedBoundingWidth = max(suggestedBoundingWidth, statusSuggestedWidthAndContinue.0)
}
let sideInsets = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
suggestedBoundingWidth += sideInsets
return (suggestedBoundingWidth, { boundingWidth in
var boundingSize: CGSize
let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth)
boundingSize = CGSize(width: textFrameWithoutInsets.size.width, height: titleFrameWithoutInsets.height + textFrameWithoutInsets.size.height + textSpacing)
if let statusSizeAndApply = statusSizeAndApply {
boundingSize.height += statusSizeAndApply.0.height
}
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
return (boundingSize, { [weak self] animation, _, _ in
if let strongSelf = self {
let themeUpdated = strongSelf.item?.presentationData.theme.theme !== item.presentationData.theme.theme
strongSelf.item = item
let cachedLayout = strongSelf.textNode.cachedLayout
if case .System = animation {
if let cachedLayout = cachedLayout {
if !cachedLayout.areLinesEqual(to: textLayout) {
if let textContents = strongSelf.textNode.contents {
let fadeNode = ASDisplayNode()
fadeNode.displaysAsynchronously = false
fadeNode.contents = textContents
fadeNode.frame = strongSelf.textNode.frame
fadeNode.isLayerBacked = true
strongSelf.addSubnode(fadeNode)
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
fadeNode?.removeFromSupernode()
})
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
}
}
let _ = titleApply()
let _ = textApply()
let _ = titleBadgeApply()
strongSelf.titleNode.frame = titleFrame
strongSelf.textNode.frame = textFrame
var titleLineWidth: CGFloat = 0.0
if let firstLine = titleLayout.linesRects().first {
titleLineWidth = firstLine.width
} else {
titleLineWidth = titleFrame.width
}
let titleBadgePadding: CGFloat = 5.0
let titleBadgeSpacing: CGFloat = 5.0
let titleBadgeFrame = CGRect(origin: CGPoint(x: titleFrame.minX + titleLineWidth + titleBadgeSpacing + titleBadgePadding, y: floorToScreenPixels(titleFrame.midY - titleBadgeLayout.size.height / 2.0) - 1.0), size: titleBadgeLayout.size)
let badgeBackgroundFrame = titleBadgeFrame.insetBy(dx: -titleBadgePadding, dy: -1.0 + UIScreenPixel)
strongSelf.titleBadgeLabel.frame = titleBadgeFrame
let button: HighlightTrackingButtonNode
if let current = strongSelf.titleBadgeButton {
button = current
button.bounds = CGRect(origin: .zero, size: badgeBackgroundFrame.size)
animation.animator.updatePosition(layer: button.layer, position: badgeBackgroundFrame.center, completion: nil)
} else {
button = HighlightTrackingButtonNode()
button.addTarget(self, action: #selector(strongSelf.badgePressed), forControlEvents: .touchUpInside)
button.frame = badgeBackgroundFrame
button.highligthedChanged = { [weak self, weak button] highlighted in
if let strongSelf = self, let button {
if highlighted {
button.layer.removeAnimation(forKey: "opacity")
button.alpha = 0.4
strongSelf.titleBadgeLabel.layer.removeAnimation(forKey: "opacity")
strongSelf.titleBadgeLabel.alpha = 0.4
} else {
button.alpha = 1.0
button.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.titleBadgeLabel.alpha = 1.0
strongSelf.titleBadgeLabel.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
strongSelf.titleBadgeButton = button
strongSelf.addSubnode(button)
}
if themeUpdated || button.backgroundImage(for: .normal) == nil {
button.setBackgroundImage(generateFilledCircleImage(diameter: badgeBackgroundFrame.height, color: mainColor.withMultipliedAlpha(0.1))?.stretchableImage(withLeftCapWidth: Int(badgeBackgroundFrame.height / 2), topCapHeight: Int(badgeBackgroundFrame.height / 2)), for: .normal)
}
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: boundingWidth - backgroundInsets.left - backgroundInsets.right, height: titleFrameWithoutInsets.height + textSpacing + textFrameWithoutInsets.height + textSpacing))
strongSelf.backgroundView.frame = backgroundFrame
strongSelf.backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: nil, thirdColor: nil, backgroundColor: nil, pattern: nil, patternTopRightPosition: nil, animation: .None)
if let statusSizeAndApply = statusSizeAndApply {
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: boundingWidth - layoutConstants.text.bubbleInsets.right - statusSizeAndApply.0.width, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0)
if strongSelf.statusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.statusNode)
statusSizeAndApply.1(.None)
} else {
statusSizeAndApply.1(animation)
}
} else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode()
}
}
})
})
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
}

View File

@ -617,6 +617,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, openPhoneContextMenu: { _ in
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {

View File

@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "FactCheckAlertController",
module_name = "FactCheckAlertController",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TextFormat",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,387 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import ComponentFlow
import MultilineTextComponent
import BalancedTextComponent
import TextFieldComponent
import ComponentDisplayAdapters
import TextFormat
private final class FactCheckAlertContentNode: AlertContentNode {
private let context: AccountContext
private var theme: AlertControllerTheme
private var presentationTheme: PresentationTheme
private let strings: PresentationStrings
private let text: String
private let titleView = ComponentView<Empty>()
private let state = ComponentState()
private let inputBackgroundNode = ASImageNode()
private let inputField = ComponentView<Empty>()
private let inputFieldExternalState = TextFieldComponent.ExternalState()
private let inputPlaceholderView = ComponentView<Empty>()
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
private var validLayout: CGSize?
private let hapticFeedback = HapticFeedback()
var present: (ViewController) -> () = { _ in }
var complete: (() -> Void)? {
didSet {
// self.inputFieldNode.complete = self.complete
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(context: AccountContext, theme: AlertControllerTheme, presentationTheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, value: String, entities: [MessageTextEntity], characterLimit: Int) {
self.context = context
self.theme = theme
self.presentationTheme = presentationTheme
self.strings = strings
self.text = text
if !value.isEmpty {
self.inputFieldExternalState.initialText = chatInputStateStringWithAppliedEntities(value, entities: entities)
}
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.inputBackgroundNode.displaysAsynchronously = false
self.inputBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: presentationTheme.actionSheet.inputHollowBackgroundColor, strokeColor: presentationTheme.actionSheet.inputBorderColor, strokeWidth: UIScreenPixel)
self.addSubnode(self.actionNodesSeparator)
self.addSubnode(self.inputBackgroundNode)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
self.actionNodes.last?.actionEnabled = true
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
self.state._updated = { [weak self] transition, _ in
guard let self, let _ = self.validLayout else {
return
}
self.requestLayout?(transition.containedViewLayoutTransition)
}
}
deinit {
self.disposable.dispose()
}
var textAndEntities: (String, [MessageTextEntity]) {
let text = self.inputFieldExternalState.text.string
let entities = generateChatInputTextEntities(self.inputFieldExternalState.text)
return (text, entities)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.theme = theme
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
let hadValidLayout = self.validLayout != nil
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 16.0)
let spacing: CGFloat = 5.0
let titleSize = self.titleView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: self.text, font: Font.semibold(17.0), textColor: self.theme.primaryColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: measureSize.width, height: 1000.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: origin.y), size: titleSize)
if let titleComponentView = self.titleView.view {
if titleComponentView.superview == nil {
self.view.addSubview(titleComponentView)
}
titleComponentView.frame = titleFrame
}
origin.y += titleSize.height + 17.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let inputInset: CGFloat = 16.0
let inputWidth = resultWidth - inputInset * 2.0
let inputFieldSize = self.inputField.update(
transition: .immediate,
component: AnyComponent(TextFieldComponent(
context: self.context,
theme: self.presentationTheme,
strings: self.strings,
externalState: self.inputFieldExternalState,
fontSize: 14.0,
textColor: self.presentationTheme.actionSheet.inputTextColor,
insets: UIEdgeInsets(top: 8.0, left: 2.0, bottom: 8.0, right: 2.0),
hideKeyboard: false,
customInputView: nil,
resetText: nil,
isOneLineWhenUnfocused: false,
characterLimit: nil,
emptyLineHandling: .oneConsecutive,
formatMenuAvailability: .available([.bold, .italic, .monospace, .link, .strikethrough, .underline]),
returnKeyType: .done,
lockedFormatAction: {
},
present: { [weak self] c in
self?.present(c)
},
paste: { _ in
},
returnKeyAction: nil,
backspaceKeyAction: nil
)),
environment: {},
containerSize: CGSize(width: inputWidth, height: 270.0)
)
self.inputField.parentState = self.state
let inputFieldFrame = CGRect(origin: CGPoint(x: inputInset, y: origin.y), size: inputFieldSize)
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
if inputFieldView.superview == nil {
self.view.addSubview(inputFieldView)
}
transition.updateFrame(view: inputFieldView, frame: inputFieldFrame)
transition.updateFrame(node: self.inputBackgroundNode, frame: inputFieldFrame)
if !hadValidLayout {
inputFieldView.activateInput()
}
}
let inputPlaceholderSize = self.inputPlaceholderView.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(
string: self.strings.FactCheck_Placeholder,
font: Font.regular(14.0),
textColor: self.presentationTheme.actionSheet.inputPlaceholderColor
)))
),
environment: {},
containerSize: CGSize(width: inputWidth, height: 240.0)
)
let inputPlaceholderFrame = CGRect(origin: CGPoint(x: inputInset + 10.0, y: floorToScreenPixels(inputFieldFrame.midY - inputPlaceholderSize.height / 2.0)), size: inputPlaceholderSize)
if let inputPlaceholderView = self.inputPlaceholderView.view {
if inputPlaceholderView.superview == nil {
inputPlaceholderView.isUserInteractionEnabled = false
self.view.addSubview(inputPlaceholderView)
}
inputPlaceholderView.frame = inputPlaceholderFrame
inputPlaceholderView.isHidden = self.inputFieldExternalState.hasText
}
if let lastActionNode = self.actionNodes.last {
lastActionNode.actionEnabled = self.inputFieldExternalState.hasText
}
let resultSize = CGSize(width: resultWidth, height: titleSize.height + spacing + inputFieldSize.height + 17.0 + actionsHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
func deactivateInput() {
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
inputFieldView.deactivateInput()
}
}
func animateError() {
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
inputFieldView.layer.addShakeAnimation()
}
self.hapticFeedback.error()
}
}
public func factCheckAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, value: String, entities: [MessageTextEntity], characterLimit: Int = 1000, apply: @escaping (String, [MessageTextEntity]) -> Void) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)?
var applyImpl: (() -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
dismissImpl?(true)
applyImpl?()
})]
let contentNode = FactCheckAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), presentationTheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: presentationData.strings.FactCheck_Title, value: value, entities: entities, characterLimit: characterLimit)
contentNode.complete = {
applyImpl?()
}
applyImpl = { [weak contentNode] in
guard let contentNode = contentNode else {
return
}
let (text, entities) = contentNode.textAndEntities
apply(text, entities)
}
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in
contentNode.deactivateInput()
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
contentNode.present = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
return controller
}

View File

@ -261,6 +261,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let openPhoneContextMenu: (OpenPhone) -> Void
public let openAgeRestrictedMessageMedia: (Message, @escaping () -> Void) -> Void
public let playMessageEffect: (Message) -> Void
public let editMessageFactCheck: (MessageId) -> Void
public let requestMessageUpdate: (MessageId, Bool) -> Void
public let cancelInteractiveKeyboardGestures: () -> Void
@ -389,6 +390,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
openPhoneContextMenu: @escaping (OpenPhone) -> Void,
openAgeRestrictedMessageMedia: @escaping (Message, @escaping () -> Void) -> Void,
playMessageEffect: @escaping (Message) -> Void,
editMessageFactCheck: @escaping (MessageId) -> Void,
requestMessageUpdate: @escaping (MessageId, Bool) -> Void,
cancelInteractiveKeyboardGestures: @escaping () -> Void,
dismissTextInput: @escaping () -> Void,
@ -497,6 +499,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
self.openPhoneContextMenu = openPhoneContextMenu
self.openAgeRestrictedMessageMedia = openAgeRestrictedMessageMedia
self.playMessageEffect = playMessageEffect
self.editMessageFactCheck = editMessageFactCheck
self.requestMessageUpdate = requestMessageUpdate
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures

View File

@ -791,7 +791,7 @@ public final class MessageInputPanelComponent: Component {
}
},
isOneLineWhenUnfocused: component.style == .media,
formatMenuAvailability: component.isFormattingLocked ? .locked : .available,
formatMenuAvailability: component.isFormattingLocked ? .locked : .available(TextFieldComponent.FormatMenuAvailability.Action.all),
lockedFormatAction: {
component.presentTextFormattingTooltip?()
},

View File

@ -3340,6 +3340,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, openPhoneContextMenu: { _ in
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {

View File

@ -84,7 +84,30 @@ public final class TextFieldComponent: Component {
}
public enum FormatMenuAvailability: Equatable {
case available
public enum Action: CaseIterable {
case bold
case italic
case monospace
case link
case strikethrough
case underline
case spoiler
case quote
case code
public static var all: [Action] = [
.bold,
.italic,
.monospace,
.link,
.strikethrough,
.underline,
.spoiler,
.quote,
.code
]
}
case available([Action])
case locked
case none
}
@ -517,84 +540,105 @@ public final class TextFieldComponent: Component {
updatedActions.insert(formatAction, at: 1)
return UIMenu(children: updatedActions)
}
guard case let .available(availableActions) = component.formatMenuAvailability else {
return UIMenu(children: suggestedActions)
}
var actions: [UIAction] = [
UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] action in
var actions: [UIAction] = []
if availableActions.contains(.bold) {
actions.append(UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] action in
if let self {
self.toggleAttribute(key: ChatTextInputAttributes.bold)
}
},
UIAction(title: strings.TextFormat_Italic, image: nil) { [weak self] action in
})
}
if availableActions.contains(.italic) {
actions.append(UIAction(title: strings.TextFormat_Italic, image: nil) { [weak self] action in
if let self {
self.toggleAttribute(key: ChatTextInputAttributes.italic)
}
},
UIAction(title: strings.TextFormat_Monospace, image: nil) { [weak self] action in
})
}
if availableActions.contains(.monospace) {
actions.append(UIAction(title: strings.TextFormat_Monospace, image: nil) { [weak self] action in
if let self {
self.toggleAttribute(key: ChatTextInputAttributes.monospace)
}
},
UIAction(title: strings.TextFormat_Link, image: nil) { [weak self] action in
})
}
if availableActions.contains(.link) {
actions.append(UIAction(title: strings.TextFormat_Link, image: nil) { [weak self] action in
if let self {
self.openLinkEditing()
}
},
UIAction(title: strings.TextFormat_Strikethrough, image: nil) { [weak self] action in
})
}
if availableActions.contains(.strikethrough) {
actions.append(UIAction(title: strings.TextFormat_Strikethrough, image: nil) { [weak self] action in
if let self {
self.toggleAttribute(key: ChatTextInputAttributes.strikethrough)
}
},
UIAction(title: strings.TextFormat_Underline, image: nil) { [weak self] action in
})
}
if availableActions.contains(.underline) {
actions.append(UIAction(title: strings.TextFormat_Underline, image: nil) { [weak self] action in
if let self {
self.toggleAttribute(key: ChatTextInputAttributes.underline)
}
}
]
actions.append(UIAction(title: strings.TextFormat_Spoiler, image: nil) { [weak self] action in
if let self {
var animated = false
let attributedText = self.inputState.inputText
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
if let _ = attributes[ChatTextInputAttributes.spoiler] {
animated = true
}
})
self.toggleAttribute(key: ChatTextInputAttributes.spoiler)
self.updateSpoilersRevealed(animated: animated)
}
})
actions.insert(UIAction(title: strings.TextFormat_Quote, image: nil) { [weak self] action in
if let self {
var animated = false
let attributedText = self.inputState.inputText
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
if let _ = attributes[ChatTextInputAttributes.block] {
animated = true
}
})
self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote))
self.updateSpoilersRevealed(animated: animated)
}
}, at: 0)
actions.append(UIAction(title: strings.TextFormat_Code, image: nil) { [weak self] action in
if let self {
var animated = false
let attributedText = self.inputState.inputText
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
if let _ = attributes[ChatTextInputAttributes.block] {
animated = true
}
})
self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil)))
self.updateSpoilersRevealed(animated: animated)
}
})
})
}
if availableActions.contains(.spoiler) {
actions.append(UIAction(title: strings.TextFormat_Spoiler, image: nil) { [weak self] action in
if let self {
var animated = false
let attributedText = self.inputState.inputText
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
if let _ = attributes[ChatTextInputAttributes.spoiler] {
animated = true
}
})
self.toggleAttribute(key: ChatTextInputAttributes.spoiler)
self.updateSpoilersRevealed(animated: animated)
}
})
}
if availableActions.contains(.quote) {
actions.insert(UIAction(title: strings.TextFormat_Quote, image: nil) { [weak self] action in
if let self {
var animated = false
let attributedText = self.inputState.inputText
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
if let _ = attributes[ChatTextInputAttributes.block] {
animated = true
}
})
self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote))
self.updateSpoilersRevealed(animated: animated)
}
}, at: 0)
}
if availableActions.contains(.code) {
actions.append(UIAction(title: strings.TextFormat_Code, image: nil) { [weak self] action in
if let self {
var animated = false
let attributedText = self.inputState.inputText
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
if let _ = attributes[ChatTextInputAttributes.block] {
animated = true
}
})
self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil)))
self.updateSpoilersRevealed(animated: animated)
}
})
}
var updatedActions = suggestedActions
let formatMenu = UIMenu(title: strings.TextFormat_Format, image: nil, children: actions)
@ -1176,6 +1220,8 @@ public final class TextFieldComponent: Component {
let pointSize = floor(24.0 * 1.3)
return EmojiTextAttachmentView(context: component.context, userLocation: .other, emoji: emoji, file: emoji.file, cache: component.context.animationCache, renderer: component.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize))
}
self.chatInputTextNodeDidUpdateText()
}
let wasEditing = component.externalState.isEditing

View File

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

View File

@ -0,0 +1,27 @@
import Foundation
import TelegramCore
import FactCheckAlertController
extension ChatControllerImpl {
func openEditMessageFactCheck(messageId: EngineMessage.Id) {
guard let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) else {
return
}
var currentText: String = ""
var currentEntities: [MessageTextEntity] = []
for attribute in message.attributes {
if let attribute = attribute as? FactCheckMessageAttribute, case let .Loaded(text, entities, _) = attribute.content {
currentText = text
currentEntities = entities
break
}
}
let controller = factCheckAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, value: currentText, entities: currentEntities, characterLimit: 4096, apply: { [weak self] text, entities in
guard let self else {
return
}
let _ = self.context.engine.messages.editMessageFactCheck(messageId: messageId, text: text, entities: entities).startStandalone()
})
self.present(controller, in: .window(.root))
}
}

View File

@ -186,6 +186,9 @@ extension ChatControllerImpl {
if case let .info(params) = navigation, let params, params.switchToRecommendedChannels {
mode = .recommendedChannels
}
if peer.id == strongSelf.context.account.peerId {
mode = .myProfile
}
var expandAvatar = expandAvatar
if peer.smallProfileImage == nil {
expandAvatar = false

View File

@ -20,7 +20,7 @@ extension ChatControllerImpl: MFMessageComposeViewControllerDelegate {
if self.presentationInterfaceState.interfaceState.selectionState != nil {
return
}
self.dismissAllTooltips()
let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer
@ -66,35 +66,39 @@ extension ChatControllerImpl: MFMessageComposeViewControllerDelegate {
)
items.append(.separator)
if let peer {
items.append(
.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Phone_SendMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil)
}))
)
items.append(
.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Phone_TelegramVoiceCall, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.controllerInteraction?.callPeer(peer.id, false)
}))
)
items.append(
.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Phone_TelegramVideoCall, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VideoCall"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.controllerInteraction?.callPeer(peer.id, true)
}))
)
if peer.id == self.context.account.peerId {
} else {
items.append(
.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Phone_SendMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil)
}))
)
items.append(
.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Phone_TelegramVoiceCall, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.controllerInteraction?.callPeer(peer.id, false)
}))
)
items.append(
.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Phone_TelegramVideoCall, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VideoCall"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.controllerInteraction?.callPeer(peer.id, true)
}))
)
}
} else {
items.append(
.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Phone_InviteToTelegram, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Telegram"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in

View File

@ -124,6 +124,7 @@ import ChatEmptyNode
import ChatMediaInputStickerGridItem
import AdsInfoScreen
import MessageUI
import PhoneNumberFormat
public enum ChatControllerPeekActions {
case standard
@ -4662,14 +4663,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
phoneData.progress?.set(.single(true))
let _ = (self.context.engine.peers.resolvePeerByPhone(phone: phoneData.number)
|> deliverOnMainQueue).start(next: { [weak self] peer in
let context = self.context
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> mapToSignal { peer -> Signal<(String, EnginePeer?), NoError> in
guard let peer, case let .user(user) = peer else {
return .complete()
}
var normalizedNumber = phoneData.number
if normalizedNumber.hasPrefix("0"), let accountPhone = user.phone, !accountPhone.hasPrefix("888") {
normalizedNumber = enhancePhoneNumberWithCodeFromNumber(normalizedNumber, otherPhoneNumber: accountPhone, configuration: context.currentCountriesConfiguration.with { $0 })
}
normalizedNumber = formatPhoneNumber(context: context, number: cleanPhoneNumber(normalizedNumber))
return self.context.engine.peers.resolvePeerByPhone(phone: normalizedNumber)
|> map { peer -> (String, EnginePeer?) in
return (normalizedNumber, peer)
}
} |> deliverOnMainQueue).start(next: { [weak self] number, peer in
guard let self else {
return
}
phoneData.progress?.set(.single(false))
self.openPhoneContextMenu(number: phoneData.number, peer: peer, message: phoneData.message, contentNode: phoneData.contentNode, messageNode: phoneData.messageNode, frame: phoneData.messageNode.bounds, anyRecognizer: nil, location: nil)
self.openPhoneContextMenu(number: number, peer: peer, message: phoneData.message, contentNode: phoneData.contentNode, messageNode: phoneData.messageNode, frame: phoneData.messageNode.bounds, anyRecognizer: nil, location: nil)
})
}, openAgeRestrictedMessageMedia: { [weak self] message, reveal in
guard let self else {
@ -4690,6 +4706,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
self.playMessageEffect(message: message)
}, editMessageFactCheck: { [weak self] messageId in
guard let self else {
return
}
self.openEditMessageFactCheck(messageId: messageId)
}, requestMessageUpdate: { [weak self] id, scroll in
if let self {
self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll)

View File

@ -557,6 +557,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
private let extendedMediaProcessingManager = ChatMessageVisibleThrottledProcessingManager(interval: 5.0)
private let translationProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0)
private let refreshStoriesProcessingManager = ChatMessageThrottledProcessingManager()
private let factCheckProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0)
let prefetchManager: InChatPrefetchManager
private var currentEarlierPrefetchMessages: [(Message, Media)] = []
@ -820,6 +821,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), toLang: toLang).startStandalone()
}
}
self.factCheckProcessingManager.process = { [weak self, weak context] messageIds in
if let context = context, let toLang = self?.toLang {
let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), toLang: toLang).startStandalone()
}
}
self.messageMentionProcessingManager.process = { [weak self, weak context] messageIds in
if let strongSelf = self {
@ -2462,6 +2468,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
var messageIdsToTranslate: [MessageId] = []
var messageIdsToFactCheck: [MessageId] = []
if let translateToLanguage {
let extendedRange: Int = 2
var wideIndexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex - extendedRange, historyView.filteredEntries.count - 1 - visible.firstIndex + extendedRange)
@ -2554,6 +2561,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
var mediaRequiredValidation = false
var hasUnseenReactions = false
var storiesRequiredValidation = false
var factCheckRequired = false
for attribute in message.attributes {
if attribute is ViewCountMessageAttribute {
if message.id.namespace == Namespaces.Message.Cloud {
@ -2575,6 +2583,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
} else if let _ = attribute as? ReplyStoryAttribute {
storiesRequiredValidation = true
} else if let attribute = attribute as? FactCheckMessageAttribute, case .Pending = attribute.content {
factCheckRequired = true
}
}
@ -2635,6 +2645,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
if hasUnseenReactions {
messageIdsWithUnseenReactions.append(message.id)
}
if factCheckRequired {
messageIdsToFactCheck.append(message.id)
}
if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.effectiveTopId == message.id {
isTopReplyThreadMessageShownValue = true
@ -2656,6 +2669,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
var hasUnconsumedMention = false
var hasUnconsumedContent = false
var hasUnseenReactions = false
var factCheckRequired = false
if message.tags.contains(.unseenPersonalMessage) {
for attribute in message.attributes {
if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.pending {
@ -2685,6 +2699,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
hasUnconsumedContent = true
} else if let attribute = attribute as? ReactionsMessageAttribute, attribute.hasUnseen {
hasUnseenReactions = true
} else if let attribute = attribute as? FactCheckMessageAttribute, case .Pending = attribute.content {
factCheckRequired = true
}
}
if hasUnconsumedMention && !hasUnconsumedContent {
@ -2693,6 +2709,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
if hasUnseenReactions {
messageIdsWithUnseenReactions.append(message.id)
}
if factCheckRequired {
messageIdsToFactCheck.append(message.id)
}
if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.effectiveTopId == message.id {
isTopReplyThreadMessageShownValue = true
}
@ -2871,6 +2890,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
if !messageIdsToTranslate.isEmpty {
self.translationProcessingManager.add(messageIdsToTranslate.map { MessageAndThreadId(messageId: $0, threadId: nil) })
}
if !messageIdsToFactCheck.isEmpty {
self.factCheckProcessingManager.add(messageIdsToFactCheck.map { MessageAndThreadId(messageId: $0, threadId: nil) })
}
if !visibleAdOpaqueIds.isEmpty {
for opaqueId in visibleAdOpaqueIds {
self.markAdAsSeen(opaqueId: opaqueId)

View File

@ -178,6 +178,13 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo
return false
}
private func canEditFactCheck(appConfig: AppConfiguration) -> Bool {
if let data = appConfig.data, let value = data["can_edit_factcheck"] as? Bool {
return value
}
return false
}
private func canViewReadStats(message: Message, participantCount: Int?, isMessageRead: Bool, isPremium: Bool, appConfig: AppConfiguration) -> Bool {
guard let peer = message.peers[message.id.peerId] else {
return false
@ -1696,6 +1703,51 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
clearCacheAsDelete = true
}
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, canEditFactCheck(appConfig: appConfig) {
var hasFactCheck = false
for attribute in message.attributes {
if let _ = attribute as? FactCheckMessageAttribute {
hasFactCheck = true
break
}
}
let title: String
if hasFactCheck {
title = chatPresentationInterfaceState.strings.Conversation_ContextMenuAddFactCheck
} else {
title = chatPresentationInterfaceState.strings.Conversation_ContextMenuEditFactCheck
}
actions.append(.action(ContextMenuActionItem(text: title, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/FactCheck"), color: theme.actionSheet.primaryTextColor)
}, action: { c, f in
f(.dismissWithoutContent)
controllerInteraction.editMessageFactCheck(messages[0].id)
})))
}
// if message.id.peerId.isGroupOrChannel {
// //TODO:localize
// if message.isAgeRestricted() {
// actions.append(.action(ContextMenuActionItem(text: "Unmark as 18+", icon: { theme in
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AgeUnmark"), color: theme.actionSheet.primaryTextColor)
// }, action: { c, _ in
// c?.dismiss(completion: {
// controllerInteraction.openMessageStats(messages[0].id)
// })
// })))
// } else {
// actions.append(.action(ContextMenuActionItem(text: "Mark as 18+", icon: { theme in
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AgeMark"), color: theme.actionSheet.primaryTextColor)
// }, action: { c, _ in
// c?.dismiss(completion: {
// controllerInteraction.openMessageStats(messages[0].id)
// })
// })))
// }
// }
if isReplyThreadHead {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ViewInChannel, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.actionSheet.primaryTextColor)

View File

@ -178,6 +178,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}, openPhoneContextMenu: { _ in
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {

View File

@ -1777,6 +1777,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, openPhoneContextMenu: { _ in
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {
@ -2651,7 +2652,8 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
var callMessages: [Message] = []
var hintGroupInCommon: PeerId?
var forumTopicThread: ChatReplyThreadMessage?
var isMyProfile = false
switch mode {
case let .nearbyPeer(distance):
nearbyPeerDistance = distance
@ -2665,10 +2667,12 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
reactionSourceMessageId = messageId
case let .forumTopic(thread):
forumTopicThread = thread
case .myProfile:
isMyProfile = true
default:
break
}
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread)
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread)
} else if peer is TelegramSecretChat {
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [])
}