Story reporting

This commit is contained in:
Ilya Laktyushin 2023-06-22 07:44:24 +04:00
parent 53aa40353a
commit 3ec2d62783
20 changed files with 394 additions and 88 deletions

View File

@ -2515,7 +2515,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let isMuted = notificationSettings.storiesMuted == true
items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Not Notify", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Muted": "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor)
return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)

View File

@ -8667,6 +8667,28 @@ public extension Api.functions.stories {
})
}
}
public extension Api.functions.stories {
static func report(userId: Api.InputUser, id: [Int32], reason: Api.ReportReason, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-916725654)
userId.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(id.count))
for item in id {
serializeInt32(item, buffer: buffer, boxed: false)
}
reason.serialize(buffer, true)
serializeString(message, buffer: buffer, boxed: false)
return (FunctionDescription(name: "stories.report", parameters: [("userId", String(describing: userId)), ("id", String(describing: id)), ("reason", String(describing: reason)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
}
public extension Api.functions.stories {
static func sendStory(flags: Int32, media: Api.InputMedia, caption: String?, entities: [Api.MessageEntity]?, privacyRules: [Api.InputPrivacyRule], randomId: Int64, period: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()

View File

@ -170,6 +170,22 @@ func _internal_reportPeerMessages(account: Account, messageIds: [MessageId], rea
} |> switchToLatest
}
func _internal_reportPeerStory(account: Account, peerId: PeerId, storyId: Int32, reason: ReportReason, message: String) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
if let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) {
return account.network.request(Api.functions.stories.report(userId: inputUser, id: [storyId], reason: reason.apiReason, message: message))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
} else {
return .complete()
}
} |> switchToLatest
}
func _internal_reportPeerReaction(account: Account, authorId: PeerId, messageId: MessageId) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> (Api.InputPeer, Api.InputPeer)? in
guard let peer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else {

View File

@ -231,6 +231,10 @@ public extension TelegramEngine {
return _internal_reportPeerMessages(account: self.account, messageIds: messageIds, reason: reason, message: message)
}
public func reportPeerStory(peerId: PeerId, storyId: Int32, reason: ReportReason, message: String) -> Signal<Void, NoError> {
return _internal_reportPeerStory(account: self.account, peerId: peerId, storyId: storyId, reason: reason, message: message)
}
public func reportPeerReaction(authorId: PeerId, messageId: MessageId) -> Signal<Never, NoError> {
return _internal_reportPeerReaction(account: self.account, authorId: authorId, messageId: messageId)
}

View File

@ -377,6 +377,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent",
"//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode",
"//submodules/TelegramUI/Components/PeerReportScreen",
"//submodules/Utils/VolumeButtons",
"//submodules/ChatContextQuery",
] + select({

View File

@ -968,6 +968,7 @@ final class MediaEditorScreenComponent: Component {
})
},
forwardAction: nil,
moreAction: nil,
presentVoiceMessagesUnavailableTooltip: nil,
audioRecorder: nil,
videoRecordingStatus: nil,

View File

@ -264,7 +264,8 @@ final class StoryPreviewComponent: Component {
attachmentAction: { },
inputModeAction: nil,
timeoutAction: nil,
forwardAction: nil,
forwardAction: {},
moreAction: { _, _ in },
presentVoiceMessagesUnavailableTooltip: nil,
audioRecorder: nil,
videoRecordingStatus: nil,

View File

@ -27,6 +27,7 @@ swift_library(
"//submodules/ChatContextQuery",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/TelegramUI/Components/MoreHeaderButton",
],
visibility = [
"//visibility:public",

View File

@ -7,6 +7,7 @@ import ChatTextInputMediaRecordingButton
import AccountContext
import TelegramPresentationData
import ChatPresentationInterfaceState
import MoreHeaderButton
private extension MessageInputActionButtonComponent.Mode {
var iconName: String? {
@ -34,6 +35,7 @@ public final class MessageInputActionButtonComponent: Component {
case delete
case attach
case forward
case more
}
public enum Action {
@ -47,6 +49,7 @@ public final class MessageInputActionButtonComponent: Component {
public let updateMediaCancelFraction: (CGFloat) -> Void
public let lockMediaRecording: () -> Void
public let stopAndPreviewMediaRecording: () -> Void
public let moreAction: (UIView, ContextGesture?) -> Void
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
@ -61,6 +64,7 @@ public final class MessageInputActionButtonComponent: Component {
updateMediaCancelFraction: @escaping (CGFloat) -> Void,
lockMediaRecording: @escaping () -> Void,
stopAndPreviewMediaRecording: @escaping () -> Void,
moreAction: @escaping (UIView, ContextGesture?) -> Void,
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
@ -74,6 +78,7 @@ public final class MessageInputActionButtonComponent: Component {
self.updateMediaCancelFraction = updateMediaCancelFraction
self.lockMediaRecording = lockMediaRecording
self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording
self.moreAction = moreAction
self.context = context
self.theme = theme
self.strings = strings
@ -107,6 +112,7 @@ public final class MessageInputActionButtonComponent: Component {
public final class View: HighlightTrackingButton {
private var micButton: ChatTextInputMediaRecordingButton?
private let sendIconView: UIImageView
private var moreButton: MoreHeaderButton?
private var component: MessageInputActionButtonComponent?
private weak var componentState: EmptyComponentState?
@ -228,20 +234,47 @@ public final class MessageInputActionButtonComponent: Component {
}
}
if self.moreButton == nil {
let moreButton = MoreHeaderButton(color: .white)
self.moreButton = moreButton
self.addSubnode(moreButton)
moreButton.isUserInteractionEnabled = true
moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: .white)))
moreButton.onPressed = { [weak self] in
guard let self, let component = self.component, let moreButton = self.moreButton else {
return
}
moreButton.play()
component.moreAction(moreButton.view, nil)
}
moreButton.contextAction = { [weak self] sourceNode, gesture in
guard let self, let component = self.component, let moreButton = self.moreButton else {
return
}
moreButton.play()
component.moreAction(moreButton.view, gesture)
}
self.moreButton = moreButton
self.addSubnode(moreButton)
}
var sendAlpha: CGFloat = 0.0
var microphoneAlpha: CGFloat = 0.0
var moreAlpha: CGFloat = 0.0
switch component.mode {
case .none:
break
case .send, .apply, .attach, .delete, .forward:
sendAlpha = 1.0
case .more:
moreAlpha = 1.0
case .videoInput, .voiceInput:
microphoneAlpha = 1.0
case .unavailableVoiceInput:
microphoneAlpha = 0.4
}
if self.sendIconView.image == nil || previousComponent?.mode.iconName != component.mode.iconName {
if let iconName = component.mode.iconName {
self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: .white)
@ -310,6 +343,17 @@ public final class MessageInputActionButtonComponent: Component {
transition.setBounds(view: self.sendIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
}
if let moreButton = self.moreButton {
let buttonSize = CGSize(width: 32.0, height: 44.0)
moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: .white)))
let moreFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - buttonSize.height) * 0.5)), size: buttonSize)
transition.setPosition(view: moreButton.view, position: moreFrame.center)
transition.setBounds(view: moreButton.view, bounds: CGRect(origin: CGPoint(), size: moreFrame.size))
transition.setAlpha(view: moreButton.view, alpha: moreAlpha)
transition.setScale(view: moreButton.view, scale: moreAlpha == 0.0 ? 0.01 : 1.0)
}
if let micButton = self.micButton {
if themeUpdated {
micButton.updateTheme(theme: component.theme)
@ -325,7 +369,7 @@ public final class MessageInputActionButtonComponent: Component {
if previousComponent?.mode != component.mode {
switch component.mode {
case .none, .send, .apply, .voiceInput, .attach, .delete, .forward, .unavailableVoiceInput:
case .none, .send, .apply, .voiceInput, .attach, .delete, .forward, .unavailableVoiceInput, .more:
micButton.updateMode(mode: .audio, animated: !transition.animation.isImmediate)
case .videoInput:
micButton.updateMode(mode: .video, animated: !transition.animation.isImmediate)

View File

@ -55,6 +55,7 @@ public final class MessageInputPanelComponent: Component {
public let inputModeAction: (() -> Void)?
public let timeoutAction: ((UIView) -> Void)?
public let forwardAction: (() -> Void)?
public let moreAction: ((UIView, ContextGesture?) -> Void)?
public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?
public let audioRecorder: ManagedAudioRecorder?
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
@ -87,6 +88,7 @@ public final class MessageInputPanelComponent: Component {
inputModeAction: (() -> Void)?,
timeoutAction: ((UIView) -> Void)?,
forwardAction: (() -> Void)?,
moreAction: ((UIView, ContextGesture?) -> Void)?,
presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?,
audioRecorder: ManagedAudioRecorder?,
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
@ -118,6 +120,7 @@ public final class MessageInputPanelComponent: Component {
self.inputModeAction = inputModeAction
self.timeoutAction = timeoutAction
self.forwardAction = forwardAction
self.moreAction = moreAction
self.presentVoiceMessagesUnavailableTooltip = presentVoiceMessagesUnavailableTooltip
self.audioRecorder = audioRecorder
self.videoRecordingStatus = videoRecordingStatus
@ -189,6 +192,9 @@ public final class MessageInputPanelComponent: Component {
if (lhs.forwardAction == nil) != (rhs.forwardAction == nil) {
return false
}
if (lhs.moreAction == nil) != (rhs.moreAction == nil) {
return false
}
if lhs.hideKeyboard != rhs.hideKeyboard {
return false
}
@ -498,7 +504,11 @@ public final class MessageInputPanelComponent: Component {
if component.attachmentAction != nil {
let attachmentButtonMode: MessageInputActionButtonComponent.Mode
attachmentButtonMode = .attach
if !self.textFieldExternalState.isEditing && component.moreAction != nil {
attachmentButtonMode = .more
} else {
attachmentButtonMode = .attach
}
let attachmentButtonSize = self.attachmentButton.update(
transition: transition,
@ -526,6 +536,12 @@ public final class MessageInputPanelComponent: Component {
},
stopAndPreviewMediaRecording: {
},
moreAction: { [weak self] view, gesture in
guard let self, let component = self.component else {
return
}
component.moreAction?(view, gesture)
},
context: component.context,
theme: component.theme,
strings: component.strings,
@ -709,6 +725,7 @@ public final class MessageInputPanelComponent: Component {
}
component.stopAndPreviewMediaRecording?()
},
moreAction: { _, _ in },
context: component.context,
theme: component.theme,
strings: component.strings,

View File

@ -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",
],
)

View File

@ -19,6 +19,7 @@ public enum PeerReportSubject {
case peer(EnginePeer.Id)
case messages([EngineMessage.Id])
case profilePhoto(EnginePeer.Id, Int64)
case story(EnginePeer.Id, Int32)
}
public enum PeerReportOption {
@ -33,10 +34,34 @@ public enum PeerReportOption {
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, completion: @escaping (ReportReason?, Bool) -> Void) {
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 {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
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
@ -73,8 +98,6 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro
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 presentationData = context.sharedContext.currentPresentationData.with { $0 }
let reportReason: ReportReason
switch option {
case .spam:
@ -114,36 +137,46 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro
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 .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, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in
items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, 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: {
@ -154,22 +187,16 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro
controller.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: {
dismissAction()
})])
])
parent?.present(controller, in: .window(.root))
}
f(.dismissWithoutContent)
})))
}
if let backAction = backAction {
items.append(.separator)
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
backAction(c)
})))
}
contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil)
contextController.pushItems(items: .single(ContextController.Items(content: .list(items))))
} else {
contextController?.dismiss(completion: nil)
parent.view.endEditing(true)
@ -246,24 +273,30 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe
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 .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)
})
}
}
}
@ -276,7 +309,7 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe
var message = ""
var items: [ActionSheetItem] = []
items.append(ReportPeerHeaderActionSheetItem(context: context, text: presentationData.strings.Report_AdditionalDetailsText))
items.append(ReportPeerDetailsActionSheetItem(context: context, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in
items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, 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: {

View File

@ -10,17 +10,19 @@ import AppBundle
public final class ReportPeerDetailsActionSheetItem: ActionSheetItem {
let context: AccountContext
let theme: PresentationTheme
let placeholderText: String
let textUpdated: (String) -> Void
public init(context: AccountContext, placeholderText: String, textUpdated: @escaping (String) -> Void) {
public init(context: AccountContext, theme: PresentationTheme, placeholderText: String, textUpdated: @escaping (String) -> Void) {
self.context = context
self.theme = theme
self.placeholderText = placeholderText
self.textUpdated = textUpdated
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return ReportPeerDetailsActionSheetItemNode(theme: theme, context: self.context, placeholderText: self.placeholderText, textUpdated: self.textUpdated)
return ReportPeerDetailsActionSheetItemNode(theme: theme, presentationTheme: self.theme, context: self.context, placeholderText: self.placeholderText, textUpdated: self.textUpdated)
}
public func updateNode(_ node: ActionSheetItemNode) {
@ -34,11 +36,10 @@ private final class ReportPeerDetailsActionSheetItemNode: ActionSheetItemNode {
private let accessibilityArea: AccessibilityAreaNode
init(theme: ActionSheetControllerTheme, context: AccountContext, placeholderText: String, textUpdated: @escaping (String) -> Void) {
init(theme: ActionSheetControllerTheme, presentationTheme: PresentationTheme, context: AccountContext, placeholderText: String, textUpdated: @escaping (String) -> Void) {
self.theme = theme
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: presentationData.theme), placeholder: placeholderText)
self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: presentationTheme), placeholder: placeholderText)
self.accessibilityArea = AccessibilityAreaNode()

View File

@ -64,6 +64,7 @@ swift_library(
"//submodules/UrlEscaping",
"//submodules/OverlayStatusController",
"//submodules/Utils/VolumeButtons",
"//submodules/TelegramUI/Components/PeerReportScreen",
],
visibility = [
"//visibility:public",

View File

@ -120,12 +120,17 @@ public final class StoryContentItemSlice {
public final class StoryContentContextState {
public final class AdditionalPeerData: Equatable {
public static func == (lhs: StoryContentContextState.AdditionalPeerData, rhs: StoryContentContextState.AdditionalPeerData) -> Bool {
return lhs.areVoiceMessagesAvailable == rhs.areVoiceMessagesAvailable
return lhs.isMuted == rhs.isMuted && lhs.areVoiceMessagesAvailable == rhs.areVoiceMessagesAvailable
}
public let isMuted: Bool
public let areVoiceMessagesAvailable: Bool
public init(areVoiceMessagesAvailable: Bool) {
public init(
isMuted: Bool,
areVoiceMessagesAvailable: Bool
) {
self.isMuted = isMuted
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
}
}

View File

@ -22,6 +22,7 @@ import ShareWithPeersScreen
import PlainButtonComponent
import TooltipUI
import PresentationDataUtils
import PeerReportScreen
public final class StoryItemSetContainerComponent: Component {
public final class ExternalState {
@ -265,6 +266,8 @@ public final class StoryItemSetContainerComponent: Component {
weak var contextController: ContextController?
weak var privacyController: ShareWithPeersScreen?
var isReporting: Bool = false
var component: StoryItemSetContainerComponent?
weak var state: EmptyComponentState?
@ -552,6 +555,9 @@ public final class StoryItemSetContainerComponent: Component {
if self.privacyController != nil {
return true
}
if self.isReporting {
return true
}
if self.isEditingStory {
return true
}
@ -1167,6 +1173,95 @@ public final class StoryItemSetContainerComponent: Component {
}
self.sendMessageContext.performShareAction(view: self)
} : nil,
moreAction: { [weak self] sourceView, gesture in
guard let self, let component = self.component, let controller = component.controller() else {
return
}
component.controller()?.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
}
return true
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: component.slice.additionalPeerData.isMuted ? "Notify" : "Not Notify", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: component.slice.additionalPeerData.isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self, let component = self.component else {
return
}
let _ = component.context.engine.peers.togglePeerStoriesMuted(peerId: component.slice.peer.id).start()
})))
var isHidden = false
if case let .user(user) = component.slice.peer, let storiesHidden = user.storiesHidden {
isHidden = storiesHidden
}
items.append(.action(ContextMenuActionItem(text: isHidden ? "Unhide \(component.slice.peer.compactDisplayTitle)" : "Hide \(component.slice.peer.compactDisplayTitle)", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self, let component = self.component else {
return
}
let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: true)
})))
items.append(.action(ContextMenuActionItem(text: "Report", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, a in
guard let self, let component = self.component, let controller = component.controller() else {
return
}
let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other]
presentPeerReportOptions(
context: component.context,
parent: controller,
contextController: c,
backAction: { _ in },
subject: .story(component.slice.peer.id, component.slice.item.storyItem.id),
options: options,
passthrough: true,
forceTheme: defaultDarkPresentationTheme,
isDetailedReportingVisible: { [weak self] isReporting in
guard let self else {
return
}
self.isReporting = isReporting
self.updateIsProgressPaused()
},
completion: { [weak self] reason, _ in
guard let self, let component = self.component, let controller = component.controller(), let reason else {
return
}
let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.peer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").start()
controller.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current)
}
)
})))
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
contextController.dismissed = { [weak self] in
guard let self else {
return
}
self.contextController = nil
self.updateIsProgressPaused()
}
self.contextController = contextController
self.updateIsProgressPaused()
controller.present(contextController, in: .window(.root))
},
presentVoiceMessagesUnavailableTooltip: { [weak self] view in
guard let self, let component = self.component, self.voiceMessagesRestrictedTooltipController == nil else {
return
@ -1337,6 +1432,13 @@ public final class StoryItemSetContainerComponent: Component {
return
}
component.controller()?.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
}
return true
}
var items: [ContextMenuItem] = []
let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0
@ -1392,14 +1494,7 @@ public final class StoryItemSetContainerComponent: Component {
})))
items.append(.separator)
component.controller()?.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
}
return true
}
items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from profile" : "Save to profile", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Check" : "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in

View File

@ -51,7 +51,7 @@ public final class StoryContentContextImpl: StoryContentContext {
PostboxViewKey.basicPeer(peerId),
PostboxViewKey.cachedPeerData(peerId: peerId),
PostboxViewKey.storiesState(key: .peer(peerId)),
PostboxViewKey.storyItems(peerId: peerId)
PostboxViewKey.storyItems(peerId: peerId),
]
if peerId == context.account.peerId {
inputKeys.append(PostboxViewKey.storiesState(key: .local))
@ -60,10 +60,11 @@ public final class StoryContentContextImpl: StoryContentContext {
self.currentFocusedIdUpdatedPromise.get(),
context.account.postbox.combinedView(
keys: inputKeys
)
),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global())
)
|> mapToSignal { _, views -> Signal<(CombinedView, [PeerId: Peer]), NoError> in
return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer]) in
|> mapToSignal { _, views, globalNotificationSettings -> Signal<(CombinedView, [PeerId: Peer], EngineGlobalNotificationSettings), NoError> in
return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer], EngineGlobalNotificationSettings) in
var peers: [PeerId: Peer] = [:]
if let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView {
for item in itemsView.items {
@ -78,10 +79,10 @@ public final class StoryContentContextImpl: StoryContentContext {
}
}
}
return (views, peers)
return (views, peers, globalNotificationSettings)
}
}
|> deliverOnMainQueue).start(next: { [weak self] views, peers in
|> deliverOnMainQueue).start(next: { [weak self] views, peers, globalNotificationSettings in
guard let self else {
return
}
@ -99,10 +100,15 @@ public final class StoryContentContextImpl: StoryContentContext {
}
let additionalPeerData: StoryContentContextState.AdditionalPeerData
if let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView, let cachedUserData = cachedPeerDataView.cachedPeerData as? CachedUserData {
let _ = cachedUserData
additionalPeerData = StoryContentContextState.AdditionalPeerData(areVoiceMessagesAvailable: cachedUserData.voiceMessagesAvailable)
var isMuted = false
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings, let storiesMuted = notificationSettings.storiesMuted {
isMuted = storiesMuted
} else {
isMuted = globalNotificationSettings.privateChats.storiesMuted
}
additionalPeerData = StoryContentContextState.AdditionalPeerData(isMuted: isMuted, areVoiceMessagesAvailable: cachedUserData.voiceMessagesAvailable)
} else {
additionalPeerData = StoryContentContextState.AdditionalPeerData(areVoiceMessagesAvailable: true)
additionalPeerData = StoryContentContextState.AdditionalPeerData(isMuted: true, areVoiceMessagesAvailable: true)
}
let state = stateView.value?.get(Stories.PeerState.self)
@ -874,7 +880,9 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
self.storyDisposable = (combineLatest(queue: .mainQueue(),
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: storyId.peerId),
TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: storyId.peerId)
TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: storyId.peerId),
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: storyId.peerId),
TelegramEngine.EngineData.Item.NotificationSettings.Global()
),
context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer]) in
guard let item = transaction.getStory(id: storyId)?.get(Stories.StoredItem.self) else {
@ -893,15 +901,23 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
return (item, peers)
}
)
|> deliverOnMainQueue).start(next: { [weak self] peerAndVoiceMessages, itemAndPeers in
|> deliverOnMainQueue).start(next: { [weak self] data, itemAndPeers in
guard let self else {
return
}
let (peer, areVoiceMessagesAvailable) = peerAndVoiceMessages
let (peer, areVoiceMessagesAvailable, notificationSettings, globalNotificationSettings) = data
let (item, peers) = itemAndPeers
var isMuted = false
if let storiesMuted = notificationSettings.storiesMuted {
isMuted = storiesMuted
} else {
isMuted = globalNotificationSettings.privateChats.storiesMuted
}
let additionalPeerData = StoryContentContextState.AdditionalPeerData(
isMuted: isMuted,
areVoiceMessagesAvailable: areVoiceMessagesAvailable
)
@ -1039,19 +1055,30 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
self.storyDisposable = (combineLatest(queue: .mainQueue(),
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: peerId)
TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: peerId),
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId),
TelegramEngine.EngineData.Item.NotificationSettings.Global()
),
listContext.state,
self.focusedIdUpdated.get()
)
//|> delay(0.4, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] peerAndVoiceMessages, state, _ in
|> deliverOnMainQueue).start(next: { [weak self] data, state, _ in
guard let self else {
return
}
let (peer, areVoiceMessagesAvailable) = peerAndVoiceMessages
let (peer, areVoiceMessagesAvailable, notificationSettings, globalNotificationSettings) = data
var isMuted = false
if let storiesMuted = notificationSettings.storiesMuted {
isMuted = storiesMuted
} else {
isMuted = globalNotificationSettings.privateChats.storiesMuted
}
let additionalPeerData = StoryContentContextState.AdditionalPeerData(
isMuted: isMuted,
areVoiceMessagesAvailable: areVoiceMessagesAvailable
)

View File

@ -100,6 +100,7 @@ import MoreHeaderButton
import VolumeButtons
import ChatAvatarNavigationNode
import ChatContextQuery
import PeerReportScreen
#if DEBUG
import os.signpost
@ -8249,7 +8250,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var message = ""
var items: [ActionSheetItem] = []
items.append(ReportPeerHeaderActionSheetItem(context: strongSelf.context, text: presentationData.strings.Report_AdditionalDetailsText))
items.append(ReportPeerDetailsActionSheetItem(context: strongSelf.context, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in
items.append(ReportPeerDetailsActionSheetItem(context: strongSelf.context, theme: presentationData.theme, 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: {

View File

@ -91,6 +91,7 @@ import PeerInfoStoryGridScreen
import StoryContainerScreen
import StoryContentComponent
import ChatAvatarNavigationNode
import PeerReportScreen
enum PeerInfoAvatarEditingMode {
case generic