From b299cbb4529fa6c94f82eecfdcdd9a3dae62c4ad Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 11 Jan 2024 23:16:58 +0400 Subject: [PATCH] [WIP] Privacy --- .../TelegramEngine/Data/PeersData.swift | 34 +++++ .../Sources/PeerInfoHeaderNode.swift | 56 +++++--- .../ChatInterfaceStateContextMenus.swift | 122 ++++++++++++++++-- 3 files changed, 182 insertions(+), 30 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 39b69aeb03..661b57902c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -844,6 +844,40 @@ public extension TelegramEngine.EngineData.Item { } } + public struct MessageReadStatsAreHidden: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Bool? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + + if self.id.namespace == Namespaces.Peer.CloudUser { + if let cachedData = view.cachedPeerData as? CachedUserData, cachedData.flags.contains(.readDatesPrivate) { + return true + } else { + return false + } + } else { + return false + } + } + } + + public struct CanDeleteHistory: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = Bool diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 9c8d773377..46c00c3692 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -1282,6 +1282,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateFrame(node: self.avatarListNode.listContainerNode.bottomShadowNode, frame: bottomShadowFrame, beginWithCurrentState: true) self.avatarListNode.listContainerNode.bottomShadowNode.update(size: bottomShadowFrame.size, transition: transition) + let singleTitleLockOffset: CGFloat = (peer?.id == self.context.account.peerId || subtitleSize.height.isZero) ? 8.0 : 0.0 + + let titleLockOffset: CGFloat = 7.0 + singleTitleLockOffset + let titleMaxLockOffset: CGFloat = 7.0 + let titleOffset: CGFloat + let titleCollapseFraction: CGFloat + if self.isAvatarExpanded { let minTitleSize = CGSize(width: titleSize.width * expandedTitleScale, height: titleSize.height * expandedTitleScale) var minTitleFrame = CGRect(origin: CGPoint(x: 16.0, y: expandedAvatarHeight - 58.0 - UIScreenPixel + (subtitleSize.height.isZero ? 10.0 : 0.0)), size: minTitleSize) @@ -1290,14 +1297,29 @@ final class PeerInfoHeaderNode: ASDisplayNode { } titleFrame = CGRect(origin: CGPoint(x: minTitleFrame.midX - titleSize.width / 2.0, y: minTitleFrame.midY - titleSize.height / 2.0), size: titleSize) + + var titleCollapseOffset = titleFrame.midY - statusBarHeight - titleLockOffset + if case .regular = metrics.widthClass, !isSettings { + titleCollapseOffset -= 7.0 + } + titleOffset = -min(titleCollapseOffset, contentOffset) + titleCollapseFraction = max(0.0, min(1.0, contentOffset / titleCollapseOffset)) + subtitleFrame = CGRect(origin: CGPoint(x: 16.0, y: minTitleFrame.maxY + 2.0), size: subtitleSize) usernameFrame = CGRect(origin: CGPoint(x: width - usernameSize.width - 16.0, y: minTitleFrame.midY - usernameSize.height / 2.0), size: usernameSize) } else { titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - titleSize.width) / 2.0), y: avatarFrame.maxY + 9.0 + (subtitleSize.height.isZero ? 11.0 : 0.0)), size: titleSize) + + var titleCollapseOffset = titleFrame.midY - statusBarHeight - titleLockOffset + if case .regular = metrics.widthClass, !isSettings { + titleCollapseOffset -= 7.0 + } + titleOffset = -min(titleCollapseOffset, contentOffset) + titleCollapseFraction = max(0.0, min(1.0, contentOffset / titleCollapseOffset)) var effectiveSubtitleWidth = subtitleSize.width if let subtitleBadgeSize { - effectiveSubtitleWidth += subtitleBadgeSize.width + 7.0 + effectiveSubtitleWidth += (subtitleBadgeSize.width + 7.0) * (1.0 - titleCollapseFraction) } let totalSubtitleWidth = effectiveSubtitleWidth + usernameSpacing + usernameSize.width @@ -1310,17 +1332,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } - let singleTitleLockOffset: CGFloat = (peer?.id == self.context.account.peerId || subtitleSize.height.isZero) ? 8.0 : 0.0 - - let titleLockOffset: CGFloat = 7.0 + singleTitleLockOffset - let titleMaxLockOffset: CGFloat = 7.0 - var titleCollapseOffset = titleFrame.midY - statusBarHeight - titleLockOffset - if case .regular = metrics.widthClass, !isSettings { - titleCollapseOffset -= 7.0 - } - let titleOffset = -min(titleCollapseOffset, contentOffset) - let titleCollapseFraction = max(0.0, min(1.0, contentOffset / titleCollapseOffset)) - let titleMinScale: CGFloat = 0.6 let subtitleMinScale: CGFloat = 0.8 let avatarMinScale: CGFloat = 0.55 @@ -1686,17 +1697,26 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale) transition.updateSublayerTransformScale(node: self.usernameNodeContainer, scale: subtitleScale) + + if let subtitleBadgeView = self.subtitleBadgeView, let subtitleBadgeSize { + let subtitleBadgeFrame = CGRect(origin: CGPoint(x: (subtitleSize.width + 8.0) * 0.5, y: floor((-subtitleBadgeSize.height) * 0.5)), size: subtitleBadgeSize) + transition.updateFrameAdditive(view: subtitleBadgeView, frame: subtitleBadgeFrame) + transition.updateAlpha(layer: subtitleBadgeView.layer, alpha: (1.0 - transitionFraction)) + } } else { let titleScale: CGFloat let subtitleScale: CGFloat var subtitleOffset: CGFloat = 0.0 + let subtitleBadgeFraction: CGFloat if self.isAvatarExpanded { titleScale = expandedTitleScale subtitleScale = 1.0 + subtitleBadgeFraction = 1.0 } else { titleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * titleMinScale subtitleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * subtitleMinScale subtitleOffset = titleCollapseFraction * -1.0 + subtitleBadgeFraction = (1.0 - titleCollapseFraction) } let rawTitleFrame = titleFrame.offsetBy(dx: self.isAvatarExpanded ? 0.0 : titleHorizontalOffset * titleScale, dy: 0.0) @@ -1728,15 +1748,15 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale) transition.updateSublayerTransformScaleAdditive(node: self.usernameNodeContainer, scale: subtitleScale) + + if let subtitleBadgeView = self.subtitleBadgeView, let subtitleBadgeSize { + let subtitleBadgeFrame = CGRect(origin: CGPoint(x: (subtitleSize.width + 8.0) * 0.5, y: floor((-subtitleBadgeSize.height) * 0.5)), size: subtitleBadgeSize) + transition.updateFrameAdditive(view: subtitleBadgeView, frame: subtitleBadgeFrame) + transition.updateAlpha(layer: subtitleBadgeView.layer, alpha: (1.0 - transitionFraction) * subtitleBadgeFraction) + } } } - if let subtitleBadgeView = self.subtitleBadgeView, let subtitleBadgeSize { - let subtitleBadgeFrame = CGRect(origin: CGPoint(x: (subtitleSize.width + 7.0) * 0.5, y: floor((-subtitleBadgeSize.height) * 0.5)), size: subtitleBadgeSize) - transition.updateFrameAdditive(view: subtitleBadgeView, frame: subtitleBadgeFrame) - transition.updateAlpha(layer: subtitleBadgeView.layer, alpha: 1.0 - transitionFraction) - } - let buttonsTransitionDistance: CGFloat = -min(0.0, apparentBackgroundHeight - backgroundHeight) let buttonsTransitionDistanceNorm: CGFloat = 40.0 diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 4308acae86..8308bbe67d 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -244,8 +244,13 @@ private func canViewReadStats(message: Message, participantCount: Int?, isMessag if group.participantCount > maxParticipantCount { return false } - case _ as TelegramUser: - break + case let user as TelegramUser: + if user.botInfo != nil { + return false + } + if user.flags.contains(.isSupport) { + return false + } default: return false } @@ -719,18 +724,28 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState var linkedDiscusionPeerId: EnginePeerCachedInfoItem var canViewStats: Bool var participantCount: Int? + var messageReadStatsAreHidden: Bool? + + init(linkedDiscusionPeerId: EnginePeerCachedInfoItem, canViewStats: Bool, participantCount: Int?, messageReadStatsAreHidden: Bool?) { + self.linkedDiscusionPeerId = linkedDiscusionPeerId + self.canViewStats = canViewStats + self.participantCount = participantCount + self.messageReadStatsAreHidden = messageReadStatsAreHidden + } } let infoSummaryData = context.engine.data.get( TelegramEngine.EngineData.Item.Peer.LinkedDiscussionPeerId(id: messages[0].id.peerId), TelegramEngine.EngineData.Item.Peer.CanViewStats(id: messages[0].id.peerId), - TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: messages[0].id.peerId) + TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: messages[0].id.peerId), + TelegramEngine.EngineData.Item.Peer.MessageReadStatsAreHidden(id: messages[0].id.peerId) ) - |> map { linkedDiscusionPeerId, canViewStats, participantCount -> InfoSummaryData in + |> map { linkedDiscusionPeerId, canViewStats, participantCount, messageReadStatsAreHidden -> InfoSummaryData in return InfoSummaryData( linkedDiscusionPeerId: linkedDiscusionPeerId, canViewStats: canViewStats, - participantCount: participantCount + participantCount: participantCount, + messageReadStatsAreHidden: messageReadStatsAreHidden ) } @@ -1696,7 +1711,13 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - let canViewStats = canViewReadStats(message: message, participantCount: infoSummaryData.participantCount, isMessageRead: isMessageRead, appConfig: appConfig) + let canViewStats: Bool + if let messageReadStatsAreHidden = infoSummaryData.messageReadStatsAreHidden, !messageReadStatsAreHidden { + canViewStats = canViewReadStats(message: message, participantCount: infoSummaryData.participantCount, isMessageRead: isMessageRead, appConfig: appConfig) + } else { + canViewStats = false + } + var reactionCount = 0 for reaction in mergedMessageReactionsAndPeers(accountPeerId: context.account.peerId, accountPeer: nil, message: message).reactions { reactionCount += Int(reaction.count) @@ -2392,6 +2413,8 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus private let highlightedBackgroundNode: ASDisplayNode private let placeholderCalculationTextNode: ImmediateTextNode private let textNode: ImmediateTextNode + private var badgeBackground: UIImageView? + private var badgeText: ImmediateTextNode? private let shimmerNode: ShimmerEffectNode private let iconNode: ASImageNode @@ -2449,7 +2472,9 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording self.iconNode = ASImageNode() - if let reactionsAttribute = item.message.reactionsAttribute, !reactionsAttribute.reactions.isEmpty { + if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: presentationData.theme.actionSheet.primaryTextColor) + } else if let reactionsAttribute = item.message.reactionsAttribute, !reactionsAttribute.reactions.isEmpty { self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: presentationData.theme.actionSheet.primaryTextColor) } else { self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Read"), color: presentationData.theme.actionSheet.primaryTextColor) @@ -2607,12 +2632,19 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { let sideInset: CGFloat = 14.0 - let verticalInset: CGFloat = 12.0 + let verticalInset: CGFloat + let rightTextInset: CGFloat + + if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { + verticalInset = 7.0 + rightTextInset = 8.0 + } else { + verticalInset = 12.0 + rightTextInset = sideInset + 36.0 + } let iconSize: CGSize = self.iconNode.image?.size ?? CGSize(width: 10.0, height: 10.0) - let rightTextInset: CGFloat = sideInset + 36.0 - let calculatedWidth = min(constrainedWidth, 250.0) let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize) @@ -2621,6 +2653,9 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus for reaction in mergedMessageReactionsAndPeers(accountPeerId: self.item.context.account.peerId, accountPeer: nil, message: self.item.message).reactions { reactionCount += Int(reaction.count) } + + var showReadBadge = false + var animatePositions = true if let currentStats = self.currentStats { reactionCount = currentStats.reactionCount @@ -2628,7 +2663,12 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus if currentStats.peers.isEmpty { if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { //TODO:localize - self.textNode.attributedText = NSAttributedString(string: "Show Read Time", font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + let text = NSAttributedString(string: "read", font: Font.regular(floor(self.presentationData.listsFontSize.baseDisplaySize * 0.8)), textColor: self.presentationData.theme.contextMenu.primaryColor) + if self.textNode.attributedText != text { + animatePositions = false + } + self.textNode.attributedText = text + showReadBadge = true } else { if reactionCount != 0 { let text: String = self.presentationData.strings.Chat_ContextReactionCount(Int32(reactionCount)) @@ -2697,14 +2737,72 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus let textSize = self.textNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset - iconSize.width - 4.0, height: .greatestFiniteMagnitude)) let placeholderTextSize = self.placeholderCalculationTextNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset - iconSize.width - 4.0, height: .greatestFiniteMagnitude)) + + var badgeTextSize: CGSize? + if showReadBadge { + let badgeBackground: UIImageView + if let current = self.badgeBackground { + badgeBackground = current + } else { + badgeBackground = UIImageView() + badgeBackground.alpha = 0.0 + self.badgeBackground = badgeBackground + self.view.addSubview(badgeBackground) + } + + let badgeText: ImmediateTextNode + if let current = self.badgeText { + badgeText = current + } else { + badgeText = ImmediateTextNode() + badgeText.alpha = 0.0 + self.badgeText = badgeText + self.addSubnode(badgeText) + } + + //TODO:localize + badgeText.attributedText = NSAttributedString(string: "show when", font: Font.regular(self.presentationData.listsFontSize.baseDisplaySize * 11.0 / 17.0), textColor: self.presentationData.theme.contextMenu.primaryColor) + + badgeTextSize = badgeText.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset - iconSize.width - 4.0 - textSize.width - 12.0, height: 100.0)) + } else { + if let badgeBackground = self.badgeBackground { + badgeBackground.removeFromSuperview() + self.badgeBackground = nil + } + if let badgeText = self.badgeText { + badgeText.removeFromSupernode() + self.badgeText = nil + } + } let combinedTextHeight = textSize.height return (CGSize(width: calculatedWidth, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in self.validLayout = (calculatedWidth: calculatedWidth, size: size) + + let positionTransition: ContainedViewLayoutTransition = animatePositions ? transition : .immediate + let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0) let textFrame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + 4.0, y: verticalOrigin), size: textSize) - transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + positionTransition.updateFrameAdditive(node: self.textNode, frame: textFrame) transition.updateAlpha(node: self.textNode, alpha: self.currentStats == nil ? 0.0 : 1.0) + + if let badgeTextSize, let badgeText = self.badgeText, let badgeBackground = self.badgeBackground { + let backgroundSideInset: CGFloat = 5.0 + let backgroundVerticalInset: CGFloat = 3.0 + let badgeTextFrame = CGRect(origin: CGPoint(x: textFrame.maxX + 5.0 + backgroundSideInset, y: textFrame.minY + floor((textFrame.height - badgeTextSize.height) * 0.5)), size: badgeTextSize) + positionTransition.updateFrameAdditive(node: badgeText, frame: badgeTextFrame) + transition.updateAlpha(node: badgeText, alpha: self.currentStats == nil ? 0.0 : 1.0) + + let badgeBackgroundFrame = badgeTextFrame.insetBy(dx: -backgroundSideInset, dy: -backgroundVerticalInset).offsetBy(dx: 0.0, dy: 1.0) + + if badgeBackground.image?.size.height != ceil(badgeBackgroundFrame.height) { + badgeBackground.image = generateStretchableFilledCircleImage(diameter: ceil(badgeBackgroundFrame.height), color: .white, strokeColor: nil, strokeWidth: nil, backgroundColor: nil)?.withRenderingMode(.alwaysTemplate) + } + badgeBackground.tintColor = self.presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.05) + + positionTransition.updateFrame(view: badgeBackground, frame: badgeBackgroundFrame) + transition.updateAlpha(layer: badgeBackground.layer, alpha: self.currentStats == nil ? 0.0 : 1.0) + } let shimmerHeight: CGFloat = 8.0