Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,35 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PeerReportScreen",
module_name = "PeerReportScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/ContextUI",
"//submodules/UndoUI",
"//submodules/PresentationDataUtils",
"//submodules/AlertUI",
"//submodules/AppBundle",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramPermissionsUI",
"//submodules/Markdown",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/ShareController",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,345 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import ContextUI
import AlertUI
import PresentationDataUtils
import UndoUI
import AppBundle
import TelegramPermissionsUI
import Markdown
public enum PeerReportSubject {
case peer(EnginePeer.Id)
case messages([EngineMessage.Id])
case profilePhoto(EnginePeer.Id, Int64)
case story(EnginePeer.Id, Int32)
}
public enum PeerReportOption {
case spam
case fake
case violence
case copyright
case pornography
case childAbuse
case illegalDrugs
case personalDetails
case other
}
public func presentPeerReportOptions(
context: AccountContext,
parent: ViewController,
contextController: ContextControllerProtocol?,
backAction: ((ContextControllerProtocol) -> Void)? = nil,
subject: PeerReportSubject,
options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other],
passthrough: Bool = false,
forceTheme: PresentationTheme? = nil,
isDetailedReportingVisible: ((Bool) -> Void)? = nil,
completion: @escaping (ReportReason?, Bool) -> Void
) {
if let contextController = contextController {
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
if let forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
var items: [ContextMenuItem] = []
if let _ = backAction {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, iconPosition: .left, action: { (c, _) in
c?.popItems()
})))
items.append(.separator)
}
for option in options {
let title: String
let color: ContextMenuActionItemTextColor = .primary
var icon: UIImage?
switch option {
case .spam:
title = presentationData.strings.ReportPeer_ReasonSpam
icon = UIImage(bundleImageName: "Chat/Context Menu/Delete")
case .fake:
title = presentationData.strings.ReportPeer_ReasonFake
icon = UIImage(bundleImageName: "Chat/Context Menu/ReportFake")
case .violence:
title = presentationData.strings.ReportPeer_ReasonViolence
icon = UIImage(bundleImageName: "Chat/Context Menu/ReportViolence")
case .pornography:
title = presentationData.strings.ReportPeer_ReasonPornography
icon = UIImage(bundleImageName: "Chat/Context Menu/ReportXxx")
case .childAbuse:
title = presentationData.strings.ReportPeer_ReasonChildAbuse
icon = UIImage(bundleImageName: "Chat/Context Menu/Restrict")
case .copyright:
title = presentationData.strings.ReportPeer_ReasonCopyright
icon = UIImage(bundleImageName: "Chat/Context Menu/ReportCopyright")
case .illegalDrugs:
title = presentationData.strings.ReportPeer_ReasonIllegalDrugs
icon = UIImage(bundleImageName: "Chat/Context Menu/ReportDrugs")
case .personalDetails:
title = presentationData.strings.ReportPeer_ReasonPersonalDetails
icon = UIImage(bundleImageName: "Chat/Context Menu/ReportPersonal")
case .other:
title = presentationData.strings.ReportPeer_ReasonOther
icon = UIImage(bundleImageName: "Chat/Context Menu/Report")
}
items.append(.action(ContextMenuActionItem(text: title, textColor: color, icon: { theme in
return generateTintedImage(image: icon, color: theme.contextMenu.primaryColor)
}, action: { [weak parent] _, f in
let reportReason: ReportReason
switch option {
case .spam:
reportReason = .spam
case .fake:
reportReason = .fake
case .violence:
reportReason = .violence
case .pornography:
reportReason = .porno
case .childAbuse:
reportReason = .childAbuse
case .copyright:
reportReason = .copyright
case .illegalDrugs:
reportReason = .illegalDrugs
case .personalDetails:
reportReason = .personalDetails
case .other:
reportReason = .custom
}
var passthrough = passthrough
if [.fake, .custom].contains(reportReason) {
passthrough = false
}
let displaySuccess = {
parent?.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current)
}
if passthrough {
completion(reportReason, true)
} else {
let action: (String) -> Void = { message in
if passthrough {
completion(reportReason, true)
} else {
switch subject {
case let .peer(peerId):
let _ = (context.engine.peers.reportPeer(peerId: peerId, reason: reportReason, message: "")
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, false)
})
case let .messages(messageIds):
let _ = (context.engine.peers.reportPeerMessages(messageIds: messageIds, reason: reportReason, message: "")
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, false)
})
case let .profilePhoto(peerId, _):
let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: "")
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, false)
})
case let .story(peerId, storyId):
let _ = (context.engine.peers.reportPeerStory(peerId: peerId, storyId: storyId, reason: reportReason, message: "")
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, false)
})
}
}
}
isDetailedReportingVisible?(true)
let controller = ActionSheetController(presentationData: presentationData, allowInputInset: true)
controller.dismissed = { _ in
isDetailedReportingVisible?(false)
}
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
var message = ""
var items: [ActionSheetItem] = []
items.append(ReportPeerHeaderActionSheetItem(context: context, text: presentationData.strings.Report_AdditionalDetailsText))
items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, strings: presentationData.strings, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in
message = text
}))
items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: {
dismissAction()
action(message)
}))
controller.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: {
dismissAction()
})])
])
parent?.present(controller, in: .window(.root))
}
f(.dismissWithoutContent)
})))
}
contextController.pushItems(items: .single(ContextController.Items(content: .list(items))))
} else {
contextController?.dismiss(completion: nil)
parent.view.endEditing(true)
parent.present(peerReportOptionsController(context: context, subject: subject, passthrough: passthrough, present: { [weak parent] c, a in
parent?.present(c, in: .window(.root), with: a)
}, push: { [weak parent] c in
parent?.push(c)
}, completion: completion), in: .window(.root))
}
}
public func peerReportOptionsController(context: AccountContext, subject: PeerReportSubject, options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other], passthrough: Bool, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping (ReportReason?, Bool) -> Void) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(theme: ActionSheetControllerTheme(presentationData: presentationData))
var items: [ActionSheetItem] = []
for option in options {
let title: String
let color: ActionSheetButtonColor = .accent
switch option {
case .spam:
title = presentationData.strings.ReportPeer_ReasonSpam
case .fake:
title = presentationData.strings.ReportPeer_ReasonFake
case .violence:
title = presentationData.strings.ReportPeer_ReasonViolence
case .pornography:
title = presentationData.strings.ReportPeer_ReasonPornography
case .childAbuse:
title = presentationData.strings.ReportPeer_ReasonChildAbuse
case .copyright:
title = presentationData.strings.ReportPeer_ReasonCopyright
case .illegalDrugs:
title = presentationData.strings.ReportPeer_ReasonIllegalDrugs
case .personalDetails:
title = presentationData.strings.ReportPeer_ReasonPersonalDetails
case .other:
title = presentationData.strings.ReportPeer_ReasonOther
}
items.append(ActionSheetButtonItem(title: title, color: color, action: { [weak controller] in
var reportReason: ReportReason?
switch option {
case .spam:
reportReason = .spam
case .fake:
reportReason = .fake
case .violence:
reportReason = .violence
case .pornography:
reportReason = .porno
case .childAbuse:
reportReason = .childAbuse
case .copyright:
reportReason = .copyright
case .illegalDrugs:
reportReason = .illegalDrugs
case .personalDetails:
reportReason = .personalDetails
case .other:
reportReason = .custom
}
if let reportReason = reportReason {
var passthrough = passthrough
if [.fake, .custom].contains(reportReason) {
passthrough = false
}
let displaySuccess = {
present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), nil)
}
let action: (String) -> Void = { message in
if passthrough {
completion(reportReason, true)
} else {
switch subject {
case let .peer(peerId):
let _ = (context.engine.peers.reportPeer(peerId: peerId, reason: reportReason, message: message)
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, true)
})
case let .messages(messageIds):
let _ = (context.engine.peers.reportPeerMessages(messageIds: messageIds, reason: reportReason, message: message)
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, true)
})
case let .profilePhoto(peerId, _):
let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: message)
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, true)
})
case let .story(peerId, storyId):
let _ = (context.engine.peers.reportPeerStory(peerId: peerId, storyId: storyId, reason: reportReason, message: message)
|> deliverOnMainQueue).start(completed: {
displaySuccess()
completion(nil, true)
})
}
}
}
if [.fake, .custom].contains(reportReason) {
let controller = ActionSheetController(presentationData: presentationData, allowInputInset: true)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
var message = ""
var items: [ActionSheetItem] = []
items.append(ReportPeerHeaderActionSheetItem(context: context, text: presentationData.strings.Report_AdditionalDetailsText))
items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, strings: presentationData.strings, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in
message = text
}))
items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: {
dismissAction()
action(message)
}))
controller.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
present(controller, nil)
} else {
action("")
}
}
controller?.dismissAnimated()
}))
}
controller.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { [weak controller] in
controller?.dismissAnimated()
completion(nil, false)
})
])
])
return controller
}
@@ -0,0 +1,71 @@
import Foundation
import UIKit
import Display
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import ShareController
import AppBundle
public final class ReportPeerDetailsActionSheetItem: ActionSheetItem {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let placeholderText: String
let textUpdated: (String) -> Void
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, placeholderText: String, textUpdated: @escaping (String) -> Void) {
self.context = context
self.theme = theme
self.strings = strings
self.placeholderText = placeholderText
self.textUpdated = textUpdated
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return ReportPeerDetailsActionSheetItemNode(theme: theme, presentationTheme: self.theme, strings: self.strings, context: self.context, placeholderText: self.placeholderText, textUpdated: self.textUpdated)
}
public func updateNode(_ node: ActionSheetItemNode) {
}
}
private final class ReportPeerDetailsActionSheetItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let inputFieldNode: ShareInputFieldNode
private let accessibilityArea: AccessibilityAreaNode
init(theme: ActionSheetControllerTheme, presentationTheme: PresentationTheme, strings: PresentationStrings, context: AccountContext, placeholderText: String, textUpdated: @escaping (String) -> Void) {
self.theme = theme
self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: presentationTheme), strings: strings, placeholder: placeholderText)
self.accessibilityArea = AccessibilityAreaNode()
super.init(theme: theme)
self.hasSeparator = false
self.addSubnode(self.inputFieldNode)
self.inputFieldNode.updateText = { text in
textUpdated(String(text.prefix(512)))
}
self.inputFieldNode.updateHeight = { [weak self] in
self?.requestLayout?()
}
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let inputHeight = self.inputFieldNode.updateLayout(width: constrainedSize.width, inputCopyText: nil, transition: .immediate)
self.inputFieldNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: inputHeight))
let size = CGSize(width: constrainedSize.width, height: inputHeight)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
}
@@ -0,0 +1,87 @@
import Foundation
import UIKit
import Display
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
public final class ReportPeerHeaderActionSheetItem: ActionSheetItem {
let context: AccountContext
let text: String
public init(context: AccountContext, text: String) {
self.context = context
self.text = text
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return ReportPeerHeaderActionSheetItemNode(theme: theme, context: self.context, text: self.text)
}
public func updateNode(_ node: ActionSheetItemNode) {
}
}
private final class ReportPeerHeaderActionSheetItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let animationNode: AnimatedStickerNode
private let textNode: ImmediateTextNode
private let accessibilityArea: AccessibilityAreaNode
init(theme: ActionSheetControllerTheme, context: AccountContext, text: String) {
self.theme = theme
let textFont = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0))
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "Cop"), width: 192, height: 192, playbackMode: .count(2), mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0
self.textNode.textAlignment = .center
self.textNode.isAccessibilityElement = false
self.accessibilityArea = AccessibilityAreaNode()
super.init(theme: theme)
self.hasSeparator = false
self.addSubnode(self.animationNode)
self.addSubnode(self.textNode)
self.addSubnode(self.accessibilityArea)
let attributedText = NSAttributedString(string: text, font: textFont, textColor: theme.primaryTextColor)
self.textNode.attributedText = attributedText
self.accessibilityArea.accessibilityLabel = attributedText.string
self.accessibilityArea.accessibilityTraits = .staticText
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let textSize = self.textNode.updateLayout(CGSize(width: constrainedSize.width - 120.0, height: .greatestFiniteMagnitude))
let topInset: CGFloat = 26.0
let textSpacing: CGFloat = 17.0
let bottomInset: CGFloat = 15.0
let iconSize = CGSize(width: 96.0, height: 96.0)
self.animationNode.frame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - iconSize.width) / 2.0), y: topInset), size: iconSize)
self.animationNode.updateLayout(size: iconSize)
self.textNode.frame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - textSize.width) / 2.0), y: topInset + iconSize.height + textSpacing), size: textSize)
let size = CGSize(width: constrainedSize.width, height: topInset + iconSize.height + textSpacing + textSize.height + bottomInset)
self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
}