diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 8cb08592bf..f46776de21 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -933,7 +933,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } else if case let .legacyGroup(group) = peer { titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) } else if case let .channel(channel) = peer { - titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + if case let .channel(mainChannel) = chatPeer, mainChannel.isMonoForum { + titleAttributedString = NSAttributedString(string: item.presentationData.strings.Monoforum_NameFormat(channel.title).string, font: titleBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + } else { + titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + } } switch item.status { diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index acc66397d7..dfd17d37da 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -3240,6 +3240,22 @@ public extension Api.functions.channels { }) } } +public extension Api.functions.channels { + static func getMessageAuthor(channel: Api.InputChannel, id: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-320691994) + channel.serialize(buffer, true) + serializeInt32(id, buffer: buffer, boxed: false) + return (FunctionDescription(name: "channels.getMessageAuthor", parameters: [("channel", String(describing: channel)), ("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.User? in + let reader = BufferReader(buffer) + var result: Api.User? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.User + } + return result + }) + } +} public extension Api.functions.channels { static func getMessages(channel: Api.InputChannel, id: [Api.InputMessage]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index f1e29ea204..36c71c6413 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1575,5 +1575,9 @@ public extension TelegramEngine { return filteredResult } } + + public func requestMessageAuthor(id: EngineMessage.Id) -> Signal { + return _internal_requestMessageAuthor(account: self.account, id: id) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index 90f1e186bf..9b3d384efc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -156,3 +156,31 @@ func _internal_searchLocalSavedMessagesPeers(account: Account, query: String, in return transaction.searchSubPeers(peerId: account.peerId, query: query, indexNameMapping: indexNameMapping).map(EnginePeer.init) } } + +func _internal_requestMessageAuthor(account: Account, id: EngineMessage.Id) -> Signal { + return account.postbox.transaction { transaction -> Api.InputChannel? in + return transaction.getPeer(id.peerId).flatMap(apiInputChannel) + } + |> mapToSignal { inputChannel -> Signal in + guard let inputChannel else { + return .single(nil) + } + if id.namespace != Namespaces.Message.Cloud { + return .single(nil) + } + return account.network.request(Api.functions.channels.getMessageAuthor(channel: inputChannel, id: id.id)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { user -> Signal in + guard let user else { + return .single(nil) + } + return account.postbox.transaction { transaction -> EnginePeer? in + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: [user])) + return transaction.getPeer(user.peerId).flatMap(EnginePeer.init) + } + } + } +} diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index c94fa8ed06..1fac92579b 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -85,7 +85,7 @@ public struct PresentationResourcesSettings { public static let balance = renderIcon(name: "Settings/Menu/Balance", scaleFactor: 0.97, backgroundColors: [UIColor(rgb: 0x34c759)]) public static let affiliateProgram = renderIcon(name: "Settings/Menu/AffiliateProgram") public static let earnStars = renderIcon(name: "Settings/Menu/EarnStars") - public static let channelMessages = renderIcon(name: "Chat/Info/ChannelMessages", backgroundColors: [UIColor(rgb: 0xFF9500)]) + public static let channelMessages = renderIcon(name: "Chat/Info/ChannelMessages", backgroundColors: [UIColor(rgb: 0x5856D6)]) public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index cb84bca837..caacd5b146 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -1041,212 +1041,230 @@ private final class AdminUserActionsSheetComponent: Component { case changeInfo } - var allConfigItems: [(ConfigItem, Bool)] = [] - if !self.allowedMediaRights.isEmpty || !self.allowedParticipantRights.isEmpty { - for configItem in ConfigItem.allCases { - let isEnabled: Bool + if case let .channel(channel) = component.chatPeer, channel.isMonoForum { + } else { + var allConfigItems: [(ConfigItem, Bool)] = [] + if !self.allowedMediaRights.isEmpty || !self.allowedParticipantRights.isEmpty { + for configItem in ConfigItem.allCases { + let isEnabled: Bool + switch configItem { + case .sendMessages: + isEnabled = self.allowedParticipantRights.contains(.sendMessages) + case .sendMedia: + isEnabled = !self.allowedMediaRights.isEmpty + case .addUsers: + isEnabled = self.allowedParticipantRights.contains(.addMembers) + case .pinMessages: + isEnabled = self.allowedParticipantRights.contains(.pinMessages) + case .changeInfo: + isEnabled = self.allowedParticipantRights.contains(.changeInfo) + } + allConfigItems.append((configItem, isEnabled)) + } + } + + loop: for (configItem, isEnabled) in allConfigItems { + let itemTitle: AnyComponent + let itemValue: Bool switch configItem { case .sendMessages: - isEnabled = self.allowedParticipantRights.contains(.sendMessages) + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_BanUser_PermissionSendMessages, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = self.participantRights.contains(.sendMessages) case .sendMedia: - isEnabled = !self.allowedMediaRights.isEmpty - case .addUsers: - isEnabled = self.allowedParticipantRights.contains(.addMembers) - case .pinMessages: - isEnabled = self.allowedParticipantRights.contains(.pinMessages) - case .changeInfo: - isEnabled = self.allowedParticipantRights.contains(.changeInfo) - } - allConfigItems.append((configItem, isEnabled)) - } - } - - loop: for (configItem, isEnabled) in allConfigItems { - let itemTitle: AnyComponent - let itemValue: Bool - switch configItem { - case .sendMessages: - itemTitle = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_BanUser_PermissionSendMessages, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - )) - itemValue = self.participantRights.contains(.sendMessages) - case .sendMedia: - if isEnabled { - itemTitle = AnyComponent(HStack([ - AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + if isEnabled { + itemTitle = AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_BanUser_PermissionSendMedia, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent( + theme: environment.theme, + title: "\(self.mediaRights.count)/\(self.allowedMediaRights.count)", + isExpanded: self.isMediaSectionExpanded + ))) + ], spacing: 7.0)) + } else { + itemTitle = AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.Channel_BanUser_PermissionSendMedia, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 - ))), - AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent( - theme: environment.theme, - title: "\(self.mediaRights.count)/\(self.allowedMediaRights.count)", - isExpanded: self.isMediaSectionExpanded - ))) - ], spacing: 7.0)) - } else { + )) + } + + itemValue = !self.mediaRights.isEmpty + case .addUsers: itemTitle = AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: environment.strings.Channel_BanUser_PermissionSendMedia, + string: environment.strings.Channel_BanUser_PermissionAddMembers, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 )) + itemValue = self.participantRights.contains(.addMembers) + case .pinMessages: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_EditAdmin_PermissionPinMessages, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = self.participantRights.contains(.pinMessages) + case .changeInfo: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_BanUser_PermissionChangeGroupInfo, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = self.participantRights.contains(.changeInfo) } - itemValue = !self.mediaRights.isEmpty - case .addUsers: - itemTitle = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_BanUser_PermissionAddMembers, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor + configSectionItems.append(AnyComponentWithIdentity(id: configItem, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: itemTitle, + accessory: .toggle(ListActionItemComponent.Toggle( + style: isEnabled ? .icons : .lock, + isOn: itemValue, + isInteractive: isEnabled, + action: isEnabled ? { [weak self] _ in + guard let self else { + return + } + + switch configItem { + case .sendMessages: + if self.participantRights.contains(.sendMessages) { + self.participantRights.remove(.sendMessages) + } else { + self.participantRights.insert(.sendMessages) + } + case .sendMedia: + if self.mediaRights.isEmpty { + self.mediaRights = self.allowedMediaRights + } else { + self.mediaRights = [] + } + case .addUsers: + if self.participantRights.contains(.addMembers) { + self.participantRights.remove(.addMembers) + } else { + self.participantRights.insert(.addMembers) + } + case .pinMessages: + if self.participantRights.contains(.pinMessages) { + self.participantRights.remove(.pinMessages) + } else { + self.participantRights.insert(.pinMessages) + } + case .changeInfo: + if self.participantRights.contains(.changeInfo) { + self.participantRights.remove(.changeInfo) + } else { + self.participantRights.insert(.changeInfo) + } + } + self.state?.updated(transition: .spring(duration: 0.35)) + } : nil )), - maximumNumberOfLines: 1 - )) - itemValue = self.participantRights.contains(.addMembers) - case .pinMessages: - itemTitle = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_EditAdmin_PermissionPinMessages, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - )) - itemValue = self.participantRights.contains(.pinMessages) - case .changeInfo: - itemTitle = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_BanUser_PermissionChangeGroupInfo, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - )) - itemValue = self.participantRights.contains(.changeInfo) - } - - configSectionItems.append(AnyComponentWithIdentity(id: configItem, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: itemTitle, - accessory: .toggle(ListActionItemComponent.Toggle( - style: isEnabled ? .icons : .lock, - isOn: itemValue, - isInteractive: isEnabled, - action: isEnabled ? { [weak self] _ in - guard let self else { + action: ((isEnabled && configItem == .sendMedia) || !isEnabled) ? { [weak self] _ in + guard let self, let component = self.component else { return } - - switch configItem { - case .sendMessages: - if self.participantRights.contains(.sendMessages) { - self.participantRights.remove(.sendMessages) - } else { - self.participantRights.insert(.sendMessages) - } - case .sendMedia: - if self.mediaRights.isEmpty { - self.mediaRights = self.allowedMediaRights - } else { - self.mediaRights = [] - } - case .addUsers: - if self.participantRights.contains(.addMembers) { - self.participantRights.remove(.addMembers) - } else { - self.participantRights.insert(.addMembers) - } - case .pinMessages: - if self.participantRights.contains(.pinMessages) { - self.participantRights.remove(.pinMessages) - } else { - self.participantRights.insert(.pinMessages) - } - case .changeInfo: - if self.participantRights.contains(.changeInfo) { - self.participantRights.remove(.changeInfo) - } else { - self.participantRights.insert(.changeInfo) - } + if !isEnabled { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.GroupPermission_PermissionDisabledByDefault, actions: [ + TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: { + }) + ]), in: .window(.root)) + } else { + self.isMediaSectionExpanded = !self.isMediaSectionExpanded + self.state?.updated(transition: .spring(duration: 0.35)) } - self.state?.updated(transition: .spring(duration: 0.35)) - } : nil - )), - action: ((isEnabled && configItem == .sendMedia) || !isEnabled) ? { [weak self] _ in - guard let self, let component = self.component else { - return - } - if !isEnabled { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.GroupPermission_PermissionDisabledByDefault, actions: [ - TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: { - }) - ]), in: .window(.root)) - } else { - self.isMediaSectionExpanded = !self.isMediaSectionExpanded - self.state?.updated(transition: .spring(duration: 0.35)) - } - } : nil, - highlighting: .disabled - )))) - - if isEnabled, case .sendMedia = configItem, self.isMediaSectionExpanded { - var mediaItems: [AnyComponentWithIdentity] = [] - mediaRightsLoop: for possibleMediaItem in allMediaRightItems { - if !self.allowedMediaRights.contains(possibleMediaItem) { - continue - } - - let mediaItemTitle: String - switch possibleMediaItem { - case .photos: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendPhoto - case .videos: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVideo - case .stickersAndGifs: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendStickersAndGifs - case .music: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendMusic - case .files: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendFile - case .voiceMessages: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVoiceMessage - case .videoMessages: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVideoMessage - case .links: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionEmbedLinks - case .polls: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendPolls - default: - continue mediaRightsLoop - } - - mediaItems.append(AnyComponentWithIdentity(id: possibleMediaItem, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: mediaItemTitle, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - ))), - ], alignment: .left, spacing: 2.0)), - leftIcon: .check(ListActionItemComponent.LeftIcon.Check( - isSelected: self.mediaRights.contains(possibleMediaItem), - toggle: { [weak self] in + } : nil, + highlighting: .disabled + )))) + + if isEnabled, case .sendMedia = configItem, self.isMediaSectionExpanded { + var mediaItems: [AnyComponentWithIdentity] = [] + mediaRightsLoop: for possibleMediaItem in allMediaRightItems { + if !self.allowedMediaRights.contains(possibleMediaItem) { + continue + } + + let mediaItemTitle: String + switch possibleMediaItem { + case .photos: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendPhoto + case .videos: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVideo + case .stickersAndGifs: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendStickersAndGifs + case .music: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendMusic + case .files: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendFile + case .voiceMessages: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVoiceMessage + case .videoMessages: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVideoMessage + case .links: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionEmbedLinks + case .polls: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendPolls + default: + continue mediaRightsLoop + } + + mediaItems.append(AnyComponentWithIdentity(id: possibleMediaItem, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: mediaItemTitle, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: self.mediaRights.contains(possibleMediaItem), + toggle: { [weak self] in + guard let self else { + return + } + + if self.mediaRights.contains(possibleMediaItem) { + self.mediaRights.remove(possibleMediaItem) + } else { + self.mediaRights.insert(possibleMediaItem) + } + + self.state?.updated(transition: .spring(duration: 0.35)) + } + )), + icon: .none, + accessory: .none, + action: { [weak self] _ in guard let self else { return } @@ -1258,31 +1276,16 @@ private final class AdminUserActionsSheetComponent: Component { } self.state?.updated(transition: .spring(duration: 0.35)) - } - )), - icon: .none, - accessory: .none, - action: { [weak self] _ in - guard let self else { - return - } - - if self.mediaRights.contains(possibleMediaItem) { - self.mediaRights.remove(possibleMediaItem) - } else { - self.mediaRights.insert(possibleMediaItem) - } - - self.state?.updated(transition: .spring(duration: 0.35)) - }, - highlighting: .disabled + }, + highlighting: .disabled + )))) + } + configSectionItems.append(AnyComponentWithIdentity(id: "media-sub", component: AnyComponent(ListSubSectionComponent( + theme: environment.theme, + leftInset: 0.0, + items: mediaItems )))) } - configSectionItems.append(AnyComponentWithIdentity(id: "media-sub", component: AnyComponent(ListSubSectionComponent( - theme: environment.theme, - leftInset: 0.0, - items: mediaItems - )))) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index 82f9898882..a37488d6c7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -1299,7 +1299,9 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE let starsString = presentationStringsFormattedNumber(Int32(amount), interfaceState.dateTimeFormat.groupingSeparator) let rawText: String - if self.isPremiumDisabled { + if let channel = interfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum { + rawText = interfaceState.strings.Chat_EmptyStateMonoforumPaid_Text(peerTitle, " $ \(starsString)").string + } else if self.isPremiumDisabled { rawText = interfaceState.strings.Chat_EmptyStatePaidMessagingDisabled_Text(peerTitle, " $ \(starsString)").string } else { rawText = interfaceState.strings.Chat_EmptyStatePaidMessaging_Text(peerTitle, " $ \(starsString)").string @@ -1867,7 +1869,11 @@ public final class ChatEmptyNode: ASDisplayNode { } } } else if let channel = peer as? TelegramChannel, channel.isMonoForum { - contentType = .starsRequired(interfaceState.sendPaidMessageStars?.value) + if let mainChannel = interfaceState.renderedPeer?.chatOrMonoforumMainPeer as? TelegramChannel, mainChannel.hasPermission(.sendSomething) { + contentType = .regular + } else { + contentType = .starsRequired(interfaceState.sendPaidMessageStars?.value) + } } else { contentType = .regular } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 5b65447c23..1019e22c1b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -864,15 +864,19 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if replyThreadMessage.peerId != item.context.account.peerId { if replyThreadMessage.peerId.isGroupOrChannel && item.message.author != nil { var isBroadcastChannel = false - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - isBroadcastChannel = true + var isMonoforum = false + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel { + if case .broadcast = peer.info { + isBroadcastChannel = true + } + isMonoforum = peer.isMonoForum } if replyThreadMessage.isChannelPost, replyThreadMessage.effectiveTopId == item.message.id { isBroadcastChannel = true } - if !isBroadcastChannel { + if !isBroadcastChannel && !isMonoforum { hasAvatar = true } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index e586681e6b..173aa77857 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -162,7 +162,9 @@ private final class ChatMessageDateSectionSeparatorNode: ASDisplayNode { private let controllerInteraction: ChatControllerInteraction? private let presentationData: ChatPresentationData - private let backgroundNode: NavigationBackgroundNode + public let backgroundNode: NavigationBackgroundNode + private var backgroundContent: WallpaperBubbleBackgroundNode? + private let patternLayer: SimpleShapeLayer init( @@ -172,6 +174,11 @@ private final class ChatMessageDateSectionSeparatorNode: ASDisplayNode { self.controllerInteraction = controllerInteraction self.presentationData = presentationData + if controllerInteraction?.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true, let backgroundContent = controllerInteraction?.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + self.backgroundContent = backgroundContent + } + self.backgroundNode = NavigationBackgroundNode(color: .clear) self.backgroundNode.isUserInteractionEnabled = false @@ -182,7 +189,13 @@ private final class ChatMessageDateSectionSeparatorNode: ASDisplayNode { self.backgroundColor = nil self.isOpaque = false - self.addSubnode(self.backgroundNode) + if let backgroundContent = self.backgroundContent { + self.addSubnode(backgroundContent) + backgroundContent.layer.mask = self.patternLayer + } else { + self.addSubnode(self.backgroundNode) + self.backgroundNode.layer.mask = self.patternLayer + } let fullTranslucency: Bool = self.controllerInteraction?.enableFullTranslucency ?? true @@ -196,8 +209,6 @@ private final class ChatMessageDateSectionSeparatorNode: ASDisplayNode { linePath.addLine(to: CGPoint(x: 10000.0, y: self.patternLayer.lineWidth * 0.5)) self.patternLayer.path = linePath self.patternLayer.lineDashPattern = [6.0 as NSNumber, 2.0 as NSNumber] as [NSNumber] - - self.backgroundNode.layer.mask = self.patternLayer } func update(size: CGSize, transition: ContainedViewLayoutTransition) { @@ -206,6 +217,21 @@ private final class ChatMessageDateSectionSeparatorNode: ASDisplayNode { self.backgroundNode.update(size: backgroundFrame.size, transition: transition) transition.updateFrame(layer: self.patternLayer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 1.66))) + + if let backgroundContent = self.backgroundContent { + backgroundContent.allowsGroupOpacity = true + self.backgroundNode.isHidden = true + + transition.updateFrame(node: backgroundContent, frame: self.backgroundNode.frame) + backgroundContent.cornerRadius = backgroundFrame.size.height / 2.0 + + /*if let (rect, containerSize) = self.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += containerSize.height - rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition) + }*/ + } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index cb1cce2204..b18c87080f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -485,15 +485,19 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { if replyThreadMessage.peerId != item.context.account.peerId { if replyThreadMessage.peerId.isGroupOrChannel && item.message.author != nil { var isBroadcastChannel = false - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - isBroadcastChannel = true + var isMonoforum = false + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel { + if case .broadcast = peer.info { + isBroadcastChannel = true + } + isMonoforum = peer.isMonoForum } if replyThreadMessage.isChannelPost, replyThreadMessage.effectiveTopId == item.message.id { isBroadcastChannel = true } - if !isBroadcastChannel { + if !isBroadcastChannel && !isMonoforum { hasAvatar = true } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index df1c5e8d4a..e7e6d16bc3 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1891,11 +1891,14 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - let canViewStats: Bool - if let messageReadStatsAreHidden = infoSummaryData.messageReadStatsAreHidden, !messageReadStatsAreHidden { + var canViewStats = false + var canViewAuthor = false + if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum { + if message.effectivelyIncoming(context.account.peerId) { + canViewAuthor = true + } + } else if let messageReadStatsAreHidden = infoSummaryData.messageReadStatsAreHidden, !messageReadStatsAreHidden { canViewStats = canViewReadStats(message: message, participantCount: infoSummaryData.participantCount, isMessageRead: isMessageRead, isPremium: isPremium, appConfig: appConfig) - } else { - canViewStats = false } var reactionCount = 0 @@ -1922,6 +1925,13 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, hasReadReports: false, isEdit: true, stats: MessageReadStats(reactionCount: 0, peers: [], readTimestamps: [:]), action: nil), false), at: 0) } + if canViewAuthor { + actions.insert(.custom(ChatMessageAuthorContextItem(context: context, message: message, action: { c, f, peer in + c.dismiss(completion: { + controllerInteraction.openPeer(peer, .default, nil, .default) + }) + }), false), at: 0) + } if let peer = message.peers[message.id.peerId], (canViewStats || reactionCount != 0) { var hasReadReports = false if let channel = peer as? TelegramChannel { @@ -2688,6 +2698,313 @@ private final class ChatDeleteMessageContextItemNode: ASDisplayNode, ContextMenu } } +final class ChatMessageAuthorContextItem: ContextMenuCustomItem { + fileprivate let context: AccountContext + fileprivate let message: Message + fileprivate let action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, EnginePeer) -> Void)? + + init(context: AccountContext, message: Message, action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, EnginePeer) -> Void)?) { + self.context = context + self.message = message + self.action = action + } + + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + return ChatMessageAuthorContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) + } +} + +private final class ChatMessageAuthorContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol { + private let item: ChatMessageAuthorContextItem + private var presentationData: PresentationData + private let getController: () -> ContextControllerProtocol? + private let actionSelected: (ContextMenuActionResult) -> Void + + private let backgroundNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let placeholderCalculationTextNode: ImmediateTextNode + private let textNode: ImmediateTextNode + private let shimmerNode: ShimmerEffectNode + + /*private let avatarsNode: AnimatedAvatarSetNode + private let avatarsContext: AnimatedAvatarSetContext + + private let placeholderAvatarsNode: AnimatedAvatarSetNode + private let placeholderAvatarsContext: AnimatedAvatarSetContext*/ + + private let buttonNode: HighlightTrackingButtonNode + + private var pointerInteraction: PointerInteraction? + + private var disposable: Disposable? + private var peer: EnginePeer? + + init(presentationData: PresentationData, item: ChatMessageAuthorContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + self.item = item + self.presentationData = presentationData + self.getController = getController + self.actionSelected = actionSelected + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isAccessibilityElement = false + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isAccessibilityElement = false + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + self.highlightedBackgroundNode.alpha = 0.0 + + self.placeholderCalculationTextNode = ImmediateTextNode() + self.placeholderCalculationTextNode.attributedText = NSAttributedString(string: presentationData.strings.Conversation_ContextMenuSeen(11), font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) + self.placeholderCalculationTextNode.maximumNumberOfLines = 1 + + self.textNode = ImmediateTextNode() + self.textNode.isAccessibilityElement = false + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) + self.textNode.maximumNumberOfLines = 1 + self.textNode.alpha = 0.0 + + self.shimmerNode = ShimmerEffectNode() + self.shimmerNode.clipsToBounds = true + + self.buttonNode = HighlightTrackingButtonNode() + self.buttonNode.isAccessibilityElement = true + self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording + + /*self.avatarsNode = AnimatedAvatarSetNode() + self.avatarsContext = AnimatedAvatarSetContext() + + self.placeholderAvatarsNode = AnimatedAvatarSetNode() + self.placeholderAvatarsContext = AnimatedAvatarSetContext()*/ + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.shimmerNode) + self.addSubnode(self.textNode) + /*self.addSubnode(self.avatarsNode) + self.addSubnode(self.placeholderAvatarsNode)*/ + self.addSubnode(self.buttonNode) + + self.buttonNode.highligthedChanged = { [weak self] highligted in + guard let strongSelf = self else { + return + } + if highligted { + strongSelf.highlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + + self.buttonNode.isUserInteractionEnabled = false + + self.disposable = (item.context.engine.messages.requestMessageAuthor(id: item.message.id) + |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + if let value { + self.updatePeer(peer: value, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + }) + } + + deinit { + self.disposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in + if let strongSelf = self { + strongSelf.highlightedBackgroundNode.alpha = 0.75 + } + }, willExit: { [weak self] in + if let strongSelf = self { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + } + }) + } + + private var validLayout: (calculatedWidth: CGFloat, size: CGSize)? + + func updatePeer(peer: EnginePeer, transition: ContainedViewLayoutTransition) { + self.buttonNode.isUserInteractionEnabled = true + + guard let (calculatedWidth, size) = self.validLayout else { + return + } + + self.peer = peer + + let (_, apply) = self.updateLayout(constrainedWidth: calculatedWidth, constrainedHeight: size.height) + apply(size, transition) + } + + func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { + let sideInset: CGFloat = 14.0 + let verticalInset: CGFloat + let rightTextInset: CGFloat + //let avatarsWidth: CGFloat = 32.0 + let avatarsWidth: CGFloat = 0 + + verticalInset = 12.0 + rightTextInset = sideInset + 36.0 + + let calculatedWidth = min(constrainedWidth, 250.0) + + let textFont = Font.regular(floor(13.0 * (self.presentationData.listsFontSize.baseDisplaySize / 17.0))) + let boldTextFont = Font.semibold(floor(13.0 * (self.presentationData.listsFontSize.baseDisplaySize / 17.0))) + + let animatePositions = true + + if let peer = self.peer { + let peerTitle = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) + let rawString = self.presentationData.strings.Chat_ContextMenu_AuthorInfo(peerTitle) + let string = NSMutableAttributedString(attributedString: NSAttributedString(string: rawString.string, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor)) + for range in rawString.ranges { + string.addAttribute(.foregroundColor, value: self.presentationData.theme.list.itemAccentColor, range: range.range) + string.addAttribute(.font, value: boldTextFont, range: range.range) + } + self.textNode.attributedText = string + } else { + self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + } + + let textSize = self.textNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset - avatarsWidth - 4.0, height: .greatestFiniteMagnitude)) + + let placeholderTextSize = self.placeholderCalculationTextNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset - avatarsWidth - 4.0, height: .greatestFiniteMagnitude)) + + 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 + avatarsWidth + 2.0, y: verticalOrigin), size: textSize) + + positionTransition.updateFrameAdditive(node: self.textNode, frame: textFrame) + transition.updateAlpha(node: self.textNode, alpha: self.peer == nil ? 0.0 : 1.0) + + let shimmerHeight: CGFloat = 8.0 + + self.shimmerNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: floor((size.height - shimmerHeight) / 2.0)), size: CGSize(width: placeholderTextSize.width, height: shimmerHeight)) + self.shimmerNode.cornerRadius = shimmerHeight / 2.0 + let shimmeringForegroundColor: UIColor + let shimmeringColor: UIColor + if self.presentationData.theme.overallDarkAppearance { + let backgroundColor = self.presentationData.theme.contextMenu.backgroundColor.blitOver(self.presentationData.theme.list.plainBackgroundColor, alpha: 1.0) + + shimmeringForegroundColor = self.presentationData.theme.contextMenu.primaryColor.blitOver(backgroundColor, alpha: 0.1) + shimmeringColor = self.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3) + } else { + let backgroundColor = self.presentationData.theme.contextMenu.backgroundColor.blitOver(self.presentationData.theme.list.plainBackgroundColor, alpha: 1.0) + + shimmeringForegroundColor = self.presentationData.theme.contextMenu.primaryColor.blitOver(backgroundColor, alpha: 0.15) + shimmeringColor = self.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3) + } + self.shimmerNode.update(backgroundColor: self.presentationData.theme.list.plainBackgroundColor, foregroundColor: shimmeringForegroundColor, shimmeringColor: shimmeringColor, shapes: [.rect(rect: self.shimmerNode.bounds)], horizontal: true, size: self.shimmerNode.bounds.size) + self.shimmerNode.updateAbsoluteRect(self.shimmerNode.frame, within: size) + transition.updateAlpha(node: self.shimmerNode, alpha: self.peer == nil ? 1.0 : 0.0) + + /*let avatarsContent: AnimatedAvatarSetContext.Content + let placeholderAvatarsContent: AnimatedAvatarSetContext.Content + + var avatarsPeers: [EnginePeer] = [] + if let peer = self.peer { + avatarsPeers = [peer] + } + avatarsContent = self.avatarsContext.update(peers: avatarsPeers, animated: false) + + placeholderAvatarsContent = self.avatarsContext.updatePlaceholder(color: shimmeringForegroundColor, count: 1, animated: false) + + let avatarsSize = self.avatarsNode.update(context: self.item.context, content: avatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true) + self.avatarsNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize) + transition.updateAlpha(node: self.avatarsNode, alpha: self.peer == nil ? 0.0 : 1.0) + + let placeholderAvatarsSize = self.placeholderAvatarsNode.update(context: self.item.context, content: placeholderAvatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true) + self.placeholderAvatarsNode.frame = CGRect(origin: CGPoint(x: self.avatarsNode.frame.minX, y: floor((size.height - placeholderAvatarsSize.height) / 2.0)), size: placeholderAvatarsSize) + transition.updateAlpha(node: self.placeholderAvatarsNode, alpha: self.peer == nil ? 1.0 : 0.0)*/ + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + }) + } + + func updateTheme(presentationData: PresentationData) { + self.presentationData = presentationData + + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) + + self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) + } + + @objc private func buttonPressed() { + self.performAction() + } + + private var actionTemporarilyDisabled: Bool = false + + func canBeHighlighted() -> Bool { + return self.isActionEnabled + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + + func performAction() { + if self.actionTemporarilyDisabled { + return + } + self.actionTemporarilyDisabled = true + Queue.mainQueue().async { [weak self] in + self?.actionTemporarilyDisabled = false + } + + guard let controller = self.getController() else { + return + } + if let peer = self.peer { + self.item.action?(controller, { [weak self] result in + self?.actionSelected(result) + }, peer) + } + } + + var isActionEnabled: Bool { + if self.item.action == nil { + return false + } + return self.peer != nil + } + + func setIsHighlighted(_ value: Bool) { + if value { + self.highlightedBackgroundNode.alpha = 1.0 + } else { + self.highlightedBackgroundNode.alpha = 0.0 + } + } + + func actionNode(at point: CGPoint) -> ContextActionNodeProtocol { + return self + } +} + final class ChatReadReportContextItem: ContextMenuCustomItem { fileprivate let context: AccountContext fileprivate let message: Message