Implement forward and copy restriction

This commit is contained in:
Ilya Laktyushin 2021-11-03 13:35:57 +04:00
parent 7b3d1c6f59
commit 003d263ebd
6 changed files with 89 additions and 62 deletions

View File

@ -576,7 +576,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.currentMessage = message self.currentMessage = message
let canDelete: Bool let canDelete: Bool
var canShare = !message.containsSecretMedia var canShare = !message.containsSecretMedia && !message.isCopyProtected()
var canFullscreen = false var canFullscreen = false

View File

@ -238,6 +238,14 @@ public extension Message {
return false return false
} }
} }
func isCopyProtected() -> Bool {
if let channel = self.peers[self.id.peerId] as? TelegramChannel, case let .broadcast(flags) = channel.info, flags.flags.contains(.copyProtectionEnabled) && channel.adminRights == nil {
return true
} else {
return false
}
}
} }
public extension Message { public extension Message {

View File

@ -376,36 +376,40 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
actions.append(.separator) actions.append(.separator)
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in if message.isCopyProtected() {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in } else {
let copyTextWithEntities = { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
var messageEntities: [MessageTextEntity]? return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
var restrictedText: String? }, action: { _, f in
for attribute in message.attributes { let copyTextWithEntities = {
if let attribute = attribute as? TextEntitiesMessageAttribute { var messageEntities: [MessageTextEntity]?
messageEntities = attribute.entities var restrictedText: String?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
}
if let attribute = attribute as? RestrictedContentMessageAttribute {
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
}
} }
if let attribute = attribute as? RestrictedContentMessageAttribute {
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? "" if let restrictedText = restrictedText {
storeMessageTextInPasteboard(restrictedText, entities: nil)
} else {
storeMessageTextInPasteboard(message.text, entities: messageEntities)
} }
Queue.mainQueue().after(0.2, {
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
controllerInteraction.displayUndo(content)
})
} }
if let restrictedText = restrictedText { copyTextWithEntities()
storeMessageTextInPasteboard(restrictedText, entities: nil) f(.default)
} else { })))
storeMessageTextInPasteboard(message.text, entities: messageEntities) }
}
Queue.mainQueue().after(0.2, {
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
controllerInteraction.displayUndo(content)
})
}
copyTextWithEntities()
f(.default)
})))
if let author = message.author, let addressName = author.addressName { if let author = message.author, let addressName = author.addressName {
let link = "https://t.me/\(addressName)" let link = "https://t.me/\(addressName)"
@ -682,7 +686,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
resourceAvailable = false resourceAvailable = false
} }
if !messages[0].text.isEmpty || resourceAvailable || diceEmoji != nil { if (!messages[0].text.isEmpty || resourceAvailable || diceEmoji != nil) && !messages[0].isCopyProtected() {
let message = messages[0] let message = messages[0]
var isExpired = false var isExpired = false
for media in message.media { for media in message.media {
@ -763,6 +767,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}))) })))
} }
} }
if resourceAvailable, !message.containsSecretMedia { if resourceAvailable, !message.containsSecretMedia {
var mediaReference: AnyMediaReference? var mediaReference: AnyMediaReference?
var isVideo = false var isVideo = false
@ -1036,12 +1041,16 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} }
if data.messageActions.options.contains(.forward) { if data.messageActions.options.contains(.forward) {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuForward, icon: { theme in if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(flags) = channel.info, flags.flags.contains(.copyProtectionEnabled) && channel.adminRights == nil {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in } else {
interfaceInteraction.forwardMessages(selectAll ? messages : [message]) actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuForward, icon: { theme in
f(.dismissWithoutContent) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.actionSheet.primaryTextColor)
}))) }, action: { _, f in
interfaceInteraction.forwardMessages(selectAll ? messages : [message])
f(.dismissWithoutContent)
})))
}
} }
if data.messageActions.options.contains(.report) { if data.messageActions.options.contains(.report) {
@ -1370,7 +1379,7 @@ func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, me
} }
} }
if !message.containsSecretMedia && !isAction { if !message.containsSecretMedia && !isAction {
if message.id.peerId.namespace != Namespaces.Peer.SecretChat { if message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.isCopyProtected() {
if !(message.flags.isSending || message.flags.contains(.Failed)) { if !(message.flags.isSending || message.flags.contains(.Failed)) {
optionsMap[id]!.insert(.forward) optionsMap[id]!.insert(.forward)
} }

View File

@ -1037,87 +1037,91 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp()) let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp())
var needShareButton = false var needsShareButton = false
if case .pinnedMessages = item.associatedData.subject { if case .pinnedMessages = item.associatedData.subject {
needShareButton = true needsShareButton = true
for media in item.message.media { for media in item.message.media {
if let _ = media as? TelegramMediaExpiredContent { if let _ = media as? TelegramMediaExpiredContent {
needShareButton = false needsShareButton = false
break break
} }
} }
} else if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id { } else if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id {
needShareButton = false needsShareButton = false
allowFullWidth = true allowFullWidth = true
} else if isFailed || Namespaces.Message.allScheduled.contains(item.message.id.namespace) { } else if isFailed || Namespaces.Message.allScheduled.contains(item.message.id.namespace) {
needShareButton = false needsShareButton = false
} else if item.message.id.peerId == item.context.account.peerId { } else if item.message.id.peerId == item.context.account.peerId {
if let _ = sourceReference { if let _ = sourceReference {
needShareButton = true needsShareButton = true
} }
} else if item.message.id.peerId.isReplies { } else if item.message.id.peerId.isReplies {
needShareButton = false needsShareButton = false
} else if item.message.effectivelyIncoming(item.context.account.peerId) { } else if item.message.effectivelyIncoming(item.context.account.peerId) {
if let _ = sourceReference { if let _ = sourceReference {
needShareButton = true needsShareButton = true
} }
if let peer = item.message.peers[item.message.id.peerId] { if let peer = item.message.peers[item.message.id.peerId] {
if let channel = peer as? TelegramChannel { if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info { if case .broadcast = channel.info {
needShareButton = true needsShareButton = true
} }
} }
} }
if let info = item.message.forwardInfo { if let info = item.message.forwardInfo {
if let author = info.author as? TelegramUser, let _ = author.botInfo, !item.message.media.isEmpty && !(item.message.media.first is TelegramMediaAction) { if let author = info.author as? TelegramUser, let _ = author.botInfo, !item.message.media.isEmpty && !(item.message.media.first is TelegramMediaAction) {
needShareButton = true needsShareButton = true
} else if let author = info.author as? TelegramChannel, case .broadcast = author.info { } else if let author = info.author as? TelegramChannel, case .broadcast = author.info {
needShareButton = true needsShareButton = true
} }
} }
if !needShareButton, let author = item.message.author as? TelegramUser, let _ = author.botInfo, !item.message.media.isEmpty && !(item.message.media.first is TelegramMediaAction) { if !needsShareButton, let author = item.message.author as? TelegramUser, let _ = author.botInfo, !item.message.media.isEmpty && !(item.message.media.first is TelegramMediaAction) {
needShareButton = true needsShareButton = true
} }
if !needShareButton { if !needsShareButton {
loop: for media in item.message.media { loop: for media in item.message.media {
if media is TelegramMediaGame || media is TelegramMediaInvoice { if media is TelegramMediaGame || media is TelegramMediaInvoice {
needShareButton = true needsShareButton = true
break loop break loop
} else if let media = media as? TelegramMediaWebpage, case .Loaded = media.content { } else if let media = media as? TelegramMediaWebpage, case .Loaded = media.content {
needShareButton = true needsShareButton = true
break loop break loop
} }
} }
} else { } else {
loop: for media in item.message.media { loop: for media in item.message.media {
if media is TelegramMediaAction { if media is TelegramMediaAction {
needShareButton = false needsShareButton = false
break loop break loop
} }
} }
} }
if item.message.isCopyProtected() {
needsShareButton = false
}
} }
if isPreview { if isPreview {
needShareButton = false needsShareButton = false
} }
let isAd = item.content.firstMessage.adAttribute != nil let isAd = item.content.firstMessage.adAttribute != nil
if isAd { if isAd {
needShareButton = false needsShareButton = false
} }
var tmpWidth: CGFloat var tmpWidth: CGFloat
if allowFullWidth { if allowFullWidth {
tmpWidth = baseWidth tmpWidth = baseWidth
if needShareButton || isAd { if needsShareButton || isAd {
tmpWidth -= 38.0 tmpWidth -= 38.0
} }
} else { } else {
tmpWidth = layoutConstants.bubble.maximumWidthFill.widthFor(baseWidth) tmpWidth = layoutConstants.bubble.maximumWidthFill.widthFor(baseWidth)
if (needShareButton || isAd) && tmpWidth + 32.0 > baseWidth { if (needsShareButton || isAd) && tmpWidth + 32.0 > baseWidth {
tmpWidth = baseWidth - 32.0 tmpWidth = baseWidth - 32.0
} }
} }
@ -2031,7 +2035,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
contentContainerNodeFrames: contentContainerNodeFrames, contentContainerNodeFrames: contentContainerNodeFrames,
mosaicStatusOrigin: mosaicStatusOrigin, mosaicStatusOrigin: mosaicStatusOrigin,
mosaicStatusSizeAndApply: mosaicStatusSizeAndApply, mosaicStatusSizeAndApply: mosaicStatusSizeAndApply,
needsShareButton: needShareButton needsShareButton: needsShareButton
) )
}) })
} }

View File

@ -70,7 +70,6 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.forwardButton.accessibilityLabel = strings.VoiceOver_MessageContextForward self.forwardButton.accessibilityLabel = strings.VoiceOver_MessageContextForward
self.shareButton = HighlightableButtonNode(pointerStyle: .default) self.shareButton = HighlightableButtonNode(pointerStyle: .default)
self.shareButton.isEnabled = false
self.shareButton.isAccessibilityElement = true self.shareButton.isAccessibilityElement = true
self.shareButton.accessibilityLabel = strings.VoiceOver_MessageContextShare self.shareButton.accessibilityLabel = strings.VoiceOver_MessageContextShare
@ -95,6 +94,7 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.addSubnode(self.separatorNode) self.addSubnode(self.separatorNode)
self.forwardButton.isEnabled = false self.forwardButton.isEnabled = false
self.shareButton.isEnabled = false
self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), forControlEvents: .touchUpInside) self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), forControlEvents: .touchUpInside)
self.reportButton.addTarget(self, action: #selector(self.reportButtonPressed), forControlEvents: .touchUpInside) self.reportButton.addTarget(self, action: #selector(self.reportButtonPressed), forControlEvents: .touchUpInside)
@ -116,6 +116,8 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.reportButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionReport"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) self.reportButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionReport"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.separatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor self.separatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor
} }
@ -149,7 +151,6 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.deleteButton.isEnabled = false self.deleteButton.isEnabled = false
self.reportButton.isEnabled = false self.reportButton.isEnabled = false
self.forwardButton.isEnabled = actions.options.contains(.forward) self.forwardButton.isEnabled = actions.options.contains(.forward)
self.shareButton.isEnabled = false
if self.peerMedia { if self.peerMedia {
self.deleteButton.isEnabled = !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty self.deleteButton.isEnabled = !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty

View File

@ -1191,6 +1191,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
case groupLocation case groupLocation
case peerPublicSettings case peerPublicSettings
case peerSettings case peerSettings
case peerAdditionalSettings
case peerActions case peerActions
} }
@ -1278,7 +1279,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
})) }))
items[.peerSettings]!.append(PeerInfoScreenCommentItem(id: ItemSignMessagesHelp, text: presentationData.strings.Channel_SignMessages_Help)) items[.peerSettings]!.append(PeerInfoScreenCommentItem(id: ItemSignMessagesHelp, text: presentationData.strings.Channel_SignMessages_Help))
items[.peerSettings]!.append(PeerInfoScreenSwitchItem(id: ItemCopyProtection, text: "Disable Forwards", value: messagesCopyProtection, icon: UIImage(bundleImageName: "Chat/Info/GroupSignIcon"), toggled: { value in items[.peerAdditionalSettings]!.append(PeerInfoScreenSwitchItem(id: ItemCopyProtection, text: "Disable Forwards", value: messagesCopyProtection, icon: UIImage(bundleImageName: "Chat/Info/GroupSignIcon"), toggled: { value in
interaction.editingToggleChannelMessageCopyProtection(value) interaction.editingToggleChannelMessageCopyProtection(value)
})) }))
} }
@ -1851,7 +1852,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}))) })))
} }
if message.id.peerId.namespace != Namespaces.Peer.SecretChat { if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(flags) = channel.info, flags.flags.contains(.copyProtectionEnabled) && channel.adminRights == nil {
} else if message.id.peerId.namespace != Namespaces.Peer.SecretChat {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, _ in items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss(completion: { c.dismiss(completion: {
if let strongSelf = self { if let strongSelf = self {
@ -1987,7 +1990,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}) })
}))) })))
if message.id.peerId.namespace != Namespaces.Peer.SecretChat { if message.isCopyProtected() {
} else if message.id.peerId.namespace != Namespaces.Peer.SecretChat {
items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, f in items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, f in
c.dismiss(completion: { c.dismiss(completion: {
if let strongSelf = self { if let strongSelf = self {