diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c06f6cb119..f15bc151bb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,7 +18,7 @@ internal: except: - tags script: - - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 + - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: internal @@ -37,7 +37,7 @@ appstore_development: except: - tags script: - - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 + - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appstore-development.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: appstore-development @@ -55,7 +55,7 @@ experimental_i: except: - tags script: - - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 + - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-experimental.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: experimental @@ -73,7 +73,7 @@ experimental: except: - tags script: - - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 + - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-experimental2.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: experimental-2 @@ -92,7 +92,7 @@ beta_testflight: except: - tags script: - - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=appstore --configuration=release_arm64 + - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=appstore --configuration=release_arm64 environment: name: testflight_llc artifacts: @@ -110,7 +110,7 @@ deploy_beta_testflight: except: - tags script: - - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-deploy-testflight --darwinContainersHost="http://host.docker.internal:8650" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" + - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-deploy-testflight --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: testflight_llc @@ -125,9 +125,9 @@ verifysanity_beta_testflight: - tags script: - rm -rf build/verify-input && mkdir -p build/verify-input && mv build/artifacts/Telegram.ipa build/verify-input/TelegramVerifySource.ipa - - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --codesigningInformationPath=build-system/fake-codesigning --configuration=release_arm64 - - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-ipa-diff --darwinContainersHost="http://host.docker.internal:8650" --ipa1="build/artifacts/Telegram.ipa" --ipa2="build/verify-input/TelegramVerifySource.ipa" - - if [ $? -ne 0 ]; then; echo "Verification failed"; mkdir -p build/verifysanity_artifacts; cp build/artifacts/Telegram.ipa build/verifysanity_artifacts/; exit 1; fi + - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --codesigningInformationPath=build-system/fake-codesigning --configuration=release_arm64 + - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-ipa-diff --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --ipa1="build/artifacts/Telegram.ipa" --ipa2="build/verify-input/TelegramVerifySource.ipa" + - if [ $? -ne 0 ]; then echo "Verification failed"; mkdir -p build/verifysanity_artifacts; cp build/artifacts/Telegram.ipa build/verifysanity_artifacts/; exit 1; fi environment: name: testflight_llc artifacts: @@ -147,9 +147,9 @@ verify_beta_testflight: - tags script: - rm -rf build/verify-input && mkdir -p build/verify-input && mv build/artifacts/Telegram.ipa build/verify-input/TelegramVerifySource.ipa - - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --configurationPath="build-system/appstore-configuration.json" --codesigningInformationPath=build-system/fake-codesigning --configuration=release_arm64 - - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-ipa-diff --darwinContainersHost="http://host.docker.internal:8650" --ipa1="build/artifacts/Telegram.ipa" --ipa2="build/verify-input/TelegramVerifySource.ipa" - - if [ $? -ne 0 ]; then; echo "Verification failed"; mkdir -p build/verify_artifacts; cp build/artifacts/Telegram.ipa build/verify_artifacts/; exit 1; fi + - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --configurationPath="build-system/appstore-configuration.json" --codesigningInformationPath=build-system/fake-codesigning --configuration=release_arm64 + - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-ipa-diff --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --ipa1="build/artifacts/Telegram.ipa" --ipa2="build/verify-input/TelegramVerifySource.ipa" + - if [ $? -ne 0 ]; then echo "Verification failed"; mkdir -p build/verify_artifacts; cp build/artifacts/Telegram.ipa build/verify_artifacts/; exit 1; fi environment: name: testflight_llc artifacts: diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 1c053c08f5..5415ebf1aa 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -509,12 +509,7 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: var items: [ContextMenuItem] = [] - var canManage: Bool = false if channel.hasPermission(.pinMessages) { - canManage = true - } - - if canManage { //TODO:localize items.append(.action(ContextMenuActionItem(text: isPinned ? "Unpin" : "Pin", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) @@ -714,7 +709,15 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: } }))) - if canManage || threadData.isOwnedByMe { + var canManage = false + if channel.flags.contains(.isCreator) { + canManage = true + } else if channel.adminRights != nil { + canManage = true + } else if threadData.isOwnedByMe { + canManage = true + } + if canManage { //TODO:localize items.append(.action(ContextMenuActionItem(text: threadData.isClosed ? "Restart" : "Close", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: threadData.isClosed ? "Chat/Context Menu/Play": "Chat/Context Menu/Pause"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index cfc6d15566..ae990d0b58 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1597,10 +1597,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create) controller.navigationPresentation = .modal - controller.completion = { title, fileId in + controller.completion = { [weak controller] title, fileId in + controller?.isInProgress = true + let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: ForumCreateTopicScreen.iconColors.randomElement()!, iconFileId: fileId) |> deliverOnMainQueue).start(next: { topicId in let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text).start() + }, error: { _ in + controller?.isInProgress = false }) } strongSelf.push(controller) @@ -2659,12 +2663,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create) controller.navigationPresentation = .modal - controller.completion = { title, fileId in + + controller.completion = { [weak controller] title, fileId in + controller?.isInProgress = true + let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: ForumCreateTopicScreen.iconColors.randomElement()!, iconFileId: fileId) |> deliverOnMainQueue).start(next: { topicId in if let navigationController = (sourceController.navigationController as? NavigationController) { let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text).start() } + }, error: { _ in + controller?.isInProgress = false }) } sourceController.push(controller) @@ -3735,8 +3744,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController func deletePeerThread(peerId: EnginePeer.Id, threadId: Int64) { let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] - + //TODO:localize + items.append(ActionSheetTextItem(title: "This will delete the topic with all its messages", parseMarkdown: true)) items.append(ActionSheetButtonItem(title: "Delete", color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() self?.commitDeletePeerThread(peerId: peerId, threadId: threadId) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 554d9b5970..84a57a1c38 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -396,6 +396,8 @@ private final class ChatListContainerItemNode: ASDisplayNode { emptyNodeTransition.updateAlpha(node: emptyShimmerEffectNode, alpha: 0.0, completion: { [weak emptyShimmerEffectNode] _ in emptyShimmerEffectNode?.removeFromSupernode() }) + strongSelf.listNode.alpha = 0.0 + emptyNodeTransition.updateAlpha(node: strongSelf.listNode, alpha: 1.0) } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index d236d68a55..2ecb6ae5b4 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -330,7 +330,7 @@ private func groupReferenceRevealOptions(strings: PresentationStrings, theme: Pr return options } -private func forumRevealOptions(strings: PresentationStrings, theme: PresentationTheme, isMuted: Bool?, isClosed: Bool, isPinned: Bool, isEditing: Bool, canManage: Bool) -> [ItemListRevealOption] { +private func forumRevealOptions(strings: PresentationStrings, theme: PresentationTheme, isMuted: Bool?, isClosed: Bool, isPinned: Bool, isEditing: Bool, canPin: Bool, canManage: Bool) -> [ItemListRevealOption] { var options: [ItemListRevealOption] = [] if !isEditing { if let isMuted = isMuted { @@ -501,6 +501,157 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { private let maxVideoLoopCount = 3 class ChatListItemNode: ItemListRevealOptionsItemNode { + final class AuthorNode: ASDisplayNode { + let authorNode: TextNode + var titleTopicArrowNode: ASImageNode? + var topicTitleNode: TextNode? + var titleTopicIconView: ComponentHostView? + var titleTopicIconComponent: EmojiStatusComponent? + + var visibilityStatus: Bool = false { + didSet { + if self.visibilityStatus != oldValue { + if let titleTopicIconView = self.titleTopicIconView, let titleTopicIconComponent = self.titleTopicIconComponent { + let _ = titleTopicIconView.update( + transition: .immediate, + component: AnyComponent(titleTopicIconComponent.withVisibleForAnimations(self.visibilityStatus)), + environment: {}, + containerSize: titleTopicIconView.bounds.size + ) + } + } + } + } + + override init() { + self.authorNode = TextNode() + self.authorNode.displaysAsynchronously = true + + super.init() + + self.addSubnode(self.authorNode) + } + + func asyncLayout() -> (_ context: AccountContext, _ constrainedWidth: CGFloat, _ theme: PresentationTheme, _ authorTitle: NSAttributedString?, _ topic: (title: NSAttributedString, iconId: Int64?, iconColor: Int32)?) -> (CGSize, () -> Void) { + let makeAuthorLayout = TextNode.asyncLayout(self.authorNode) + let makeTopicTitleLayout = TextNode.asyncLayout(self.topicTitleNode) + + return { [weak self] context, constrainedWidth, theme, authorTitle, topic in + var maxTitleWidth = constrainedWidth + if let _ = topic { + maxTitleWidth = floor(constrainedWidth * 0.7) + } + + let authorTitleLayout = makeAuthorLayout(TextNodeLayoutArguments(attributedString: authorTitle, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + + var remainingWidth = constrainedWidth - authorTitleLayout.0.size.width + + var topicTitleArguments: TextNodeLayoutArguments? + var arrowIconImage: UIImage? + if let topic = topic { + remainingWidth -= 22.0 + 2.0 + + arrowIconImage = PresentationResourcesChatList.topicArrowIcon(theme) + if let arrowIconImage = arrowIconImage { + remainingWidth -= arrowIconImage.size.width + 6.0 * 2.0 + } + + topicTitleArguments = TextNodeLayoutArguments(attributedString: topic.title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: remainingWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)) + } + + let topicTitleLayout = topicTitleArguments.flatMap(makeTopicTitleLayout) + + var size = authorTitleLayout.0.size + if let topicTitleLayout = topicTitleLayout { + size.width += 10.0 + topicTitleLayout.0.size.width + } + + return (size, { + guard let self else { + return + } + + let _ = authorTitleLayout.1() + let authorFrame = CGRect(origin: CGPoint(), size: authorTitleLayout.0.size) + self.authorNode.frame = authorFrame + + var nextX = authorFrame.maxX - 1.0 + if let arrowIconImage = arrowIconImage, let topic = topic { + let titleTopicArrowNode: ASImageNode + if let current = self.titleTopicArrowNode { + titleTopicArrowNode = current + } else { + titleTopicArrowNode = ASImageNode() + self.titleTopicArrowNode = titleTopicArrowNode + self.addSubnode(titleTopicArrowNode) + } + titleTopicArrowNode.image = arrowIconImage + nextX += 6.0 + titleTopicArrowNode.frame = CGRect(origin: CGPoint(x: nextX, y: 5.0), size: arrowIconImage.size) + nextX += arrowIconImage.size.width + 6.0 + + let titleTopicIconView: ComponentHostView + if let current = self.titleTopicIconView { + titleTopicIconView = current + } else { + titleTopicIconView = ComponentHostView() + self.titleTopicIconView = titleTopicIconView + self.view.addSubview(titleTopicIconView) + } + + let titleTopicIconContent: EmojiStatusComponent.Content + if let fileId = topic.iconId, fileId != 0 { + titleTopicIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 36.0, height: 36.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(2)) + } else { + titleTopicIconContent = .topic(title: String(topic.title.string.prefix(1)), color: topic.iconColor, size: CGSize(width: 22.0, height: 22.0)) + } + + let titleTopicIconComponent = EmojiStatusComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + content: titleTopicIconContent, + isVisibleForAnimations: self.visibilityStatus, + action: nil + ) + self.titleTopicIconComponent = titleTopicIconComponent + + let iconSize = titleTopicIconView.update( + transition: .immediate, + component: AnyComponent(titleTopicIconComponent), + environment: {}, + containerSize: CGSize(width: 22.0, height: 22.0) + ) + titleTopicIconView.frame = CGRect(origin: CGPoint(x: nextX, y: UIScreenPixel), size: iconSize) + nextX += iconSize.width + 2.0 + } else { + if let titleTopicArrowNode = self.titleTopicArrowNode { + self.titleTopicArrowNode = nil + titleTopicArrowNode.removeFromSupernode() + } + if let titleTopicIconView = self.titleTopicIconView { + self.titleTopicIconView = nil + titleTopicIconView.removeFromSuperview() + } + } + + if let topicTitleLayout = topicTitleLayout { + let topicTitleNode = topicTitleLayout.1() + if topicTitleNode.supernode == nil { + self.addSubnode(topicTitleNode) + self.topicTitleNode = topicTitleNode + } + + topicTitleNode.frame = CGRect(origin: CGPoint(x: nextX - 1.0, y: 0.0), size: topicTitleLayout.0.size) + } else if let topicTitleNode = self.topicTitleNode { + self.topicTitleNode = nil + topicTitleNode.removeFromSupernode() + } + }) + } + } + } + var item: ChatListItem? private let backgroundNode: ASDisplayNode @@ -517,7 +668,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var videoLoopCount = 0 let titleNode: TextNode - let authorNode: TextNode + let authorNode: AuthorNode let measureNode: TextNode private var currentItemHeight: CGFloat? let textNode: TextNodeWithEntities @@ -725,6 +876,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { containerSize: avatarIconView.bounds.size ) } + self.authorNode.visibilityStatus = self.visibilityStatus } } } @@ -760,9 +912,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = true - self.authorNode = TextNode() + self.authorNode = AuthorNode() self.authorNode.isUserInteractionEnabled = false - self.authorNode.displaysAsynchronously = true self.textNode = TextNodeWithEntities() self.textNode.textNode.isUserInteractionEnabled = false @@ -1037,7 +1188,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let dateLayout = TextNode.asyncLayout(self.dateNode) let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) let titleLayout = TextNode.asyncLayout(self.titleNode) - let authorLayout = TextNode.asyncLayout(self.authorNode) + let authorLayout = self.authorNode.asyncLayout() let makeMeasureLayout = TextNode.asyncLayout(self.measureNode) let inputActivitiesLayout = self.inputActivitiesNode.asyncLayout() let badgeLayout = self.badgeNode.asyncLayout() @@ -1278,6 +1429,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let contentImageSpacing: CGFloat = 2.0 let contentImageTrailingSpace: CGFloat = 5.0 var contentImageSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = [] + var forumThread: (title: String, iconId: Int64?, iconColor: Int32)? switch contentData { case let .chat(itemPeer, _, _, _, text, spoilers, customEmojiRanges): @@ -1302,14 +1454,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - if let peerTextValue = peerText, case let .channel(channel) = itemPeer.chatMainPeer, channel.flags.contains(.isForum), threadInfo == nil { + if let _ = peerText, case let .channel(channel) = itemPeer.chatMainPeer, channel.flags.contains(.isForum), threadInfo == nil { if let forumTopicData = forumTopicData { - peerText = "\(peerTextValue) → \(forumTopicData.title)" + forumThread = (forumTopicData.title, forumTopicData.iconFileId, forumTopicData.iconColor) } else if let threadInfo = threadInfo?.info { - peerText = "\(peerTextValue) → \(threadInfo.title)" - } else { - //TODO:localize - peerText = "\(peerTextValue) → General" + forumThread = (threadInfo.title, threadInfo.icon, threadInfo.iconColor) } } @@ -1574,14 +1723,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if hasFailedMessages { statusState = .failed(item.presentationData.theme.chatList.failedFillColor, item.presentationData.theme.chatList.failedForegroundColor) } else { - if case .chatList = item.chatListLocation { - if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) { + if let forumTopicData = forumTopicData { + if message.id.namespace == forumTopicData.maxOutgoingReadMessageId.namespace, message.id.id >= forumTopicData.maxOutgoingReadMessageId.id { statusState = .read(item.presentationData.theme.chatList.checkmarkColor) } else { statusState = .delivered(item.presentationData.theme.chatList.checkmarkColor) } - } else if case .forum = item.chatListLocation { - if let forumTopicData = forumTopicData, message.id.namespace == forumTopicData.maxOutgoingReadMessageId.namespace, message.id.id >= forumTopicData.maxOutgoingReadMessageId.id { + } else { + if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) { statusState = .read(item.presentationData.theme.chatList.checkmarkColor) } else { statusState = .delivered(item.presentationData.theme.chatList.checkmarkColor) @@ -1768,7 +1917,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } badgeSize = max(badgeSize, reorderInset) - let (authorLayout, authorApply) = authorLayout(TextNodeLayoutArguments(attributedString: (hideAuthor && !hasDraft) ? nil : authorAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + let authorTitle = (hideAuthor && !hasDraft) ? nil : authorAttributedString + + var forumThreadTitle: (title: NSAttributedString, iconId: Int64?, iconColor: Int32)? + if let _ = authorTitle, let forumThread { + forumThreadTitle = (NSAttributedString(string: forumThread.title, font: textFont, textColor: theme.authorNameColor), forumThread.iconId, forumThread.iconColor) + } + + let (authorLayout, authorApply) = authorLayout(item.context, rawContentWidth - badgeSize, item.presentationData.theme, authorTitle, forumThreadTitle) var textCutout: TextNodeCutout? if !textLeftCutout.isZero { @@ -1789,8 +1945,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var inputActivitiesSize: CGSize? var inputActivitiesApply: (() -> Void)? - if let inputActivities = inputActivities, !inputActivities.isEmpty, case let .chatList(index) = item.index { - let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentWidth - badgeSize, height: 40.0), item.presentationData, item.presentationData.theme.chatList.messageTextColor, index.messageIndex.id.peerId, inputActivities) + var chatPeerId: EnginePeer.Id? + if case let .chatList(index) = item.index { + chatPeerId = index.messageIndex.id.peerId + } else if case let .forum(peerId) = item.chatListLocation { + chatPeerId = peerId + } + if let inputActivities = inputActivities, !inputActivities.isEmpty, let chatPeerId { + let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentWidth - badgeSize, height: 40.0), item.presentationData, item.presentationData.theme.chatList.messageTextColor, chatPeerId, inputActivities) inputActivitiesSize = size inputActivitiesApply = apply } else { @@ -1844,16 +2006,18 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if case .forum = item.chatListLocation { if case let .chat(itemPeer) = contentPeer, case let .channel(channel) = itemPeer.peer { var canManage = false - if channel.hasPermission(.pinMessages) { + if channel.flags.contains(.isCreator) { + canManage = true + } else if channel.adminRights != nil { + canManage = true + } else if let threadInfo = threadInfo, threadInfo.isOwner { canManage = true - } else if let threadInfo { - canManage = threadInfo.isOwner } var isClosed = false if let threadInfo { isClosed = threadInfo.isClosed } - peerRevealOptions = forumRevealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isMuted: (currentMutedIconImage != nil), isClosed: isClosed, isPinned: isPinned, isEditing: item.editing, canManage: canManage) + peerRevealOptions = forumRevealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isMuted: (currentMutedIconImage != nil), isClosed: isClosed, isPinned: isPinned, isEditing: item.editing, canPin: channel.flags.contains(.isCreator) || channel.adminRights != nil, canManage: canManage) peerLeftRevealOptions = [] } else { peerRevealOptions = [] @@ -2189,13 +2353,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.secretIconNode = nil secretIconNode.removeFromSupernode() } - + let contentDelta = CGPoint(x: contentRect.origin.x - (strongSelf.titleNode.frame.minX - titleOffset), y: contentRect.origin.y - (strongSelf.titleNode.frame.minY - UIScreenPixel)) let titleFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame - let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height), size: authorLayout.size) + let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height), size: authorLayout) strongSelf.authorNode.frame = authorNodeFrame - let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.size.height.isZero ? 0.0 : (authorLayout.size.height - 3.0))), size: textLayout.size) + let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.height.isZero ? 0.0 : (authorLayout.height - 3.0))), size: textLayout.size) strongSelf.textNode.textNode.frame = textNodeFrame if !textLayout.spoilers.isEmpty { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index 8ed87f6c69..61e8fa1ba9 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -270,7 +270,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: } } default: - hideAuthor = true + switch action.action { + case .topicCreated, .topicEdited: + hideAuthor = false + default: + hideAuthor = true + } if let (text, textSpoilers, customEmojiRangesValue) = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) { messageText = text spoilers = textSpoilers diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 761189a595..de8ee202df 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -145,9 +145,19 @@ public final class ChatListNodeInteraction { } public final class ChatListNodePeerInputActivities { - public let activities: [EnginePeer.Id: [(EnginePeer, PeerInputActivity)]] + public struct ItemId: Hashable { + public var peerId: EnginePeer.Id + public var threadId: Int64? + + public init(peerId: EnginePeer.Id, threadId: Int64?) { + self.peerId = peerId + self.threadId = threadId + } + } - public init(activities: [EnginePeer.Id: [(EnginePeer, PeerInputActivity)]]) { + public let activities: [ItemId: [(EnginePeer, PeerInputActivity)]] + + public init(activities: [ItemId: [(EnginePeer, PeerInputActivity)]]) { self.activities = activities } } @@ -1155,7 +1165,7 @@ public final class ChatListNode: ListView { let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) - let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, mode: mode) + let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, mode: mode, chatListLocation: location) let entries = rawEntries.filter { entry in switch entry { case let .PeerEntry(_, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _, _, _, _): @@ -1488,7 +1498,7 @@ public final class ChatListNode: ListView { let previousPeerCache = Atomic<[EnginePeer.Id: EnginePeer]>(value: [:]) let previousActivities = Atomic(value: nil) self.activityStatusesDisposable = (context.account.allPeerInputActivities() - |> mapToSignal { activitiesByPeerId -> Signal<[EnginePeer.Id: [(EnginePeer, PeerInputActivity)]], NoError> in + |> mapToSignal { activitiesByPeerId -> Signal<[ChatListNodePeerInputActivities.ItemId: [(EnginePeer, PeerInputActivity)]], NoError> in var activitiesByPeerId = activitiesByPeerId for key in activitiesByPeerId.keys { activitiesByPeerId[key]?.removeAll(where: { _, activity in @@ -1504,11 +1514,23 @@ public final class ChatListNode: ListView { } var foundAllPeers = true - var cachedResult: [EnginePeer.Id: [(EnginePeer, PeerInputActivity)]] = [:] + var cachedResult: [ChatListNodePeerInputActivities.ItemId: [(EnginePeer, PeerInputActivity)]] = [:] previousPeerCache.with { dict -> Void in for (chatPeerId, activities) in activitiesByPeerId { - guard case .global = chatPeerId.category else { - continue + var threadId: Int64? + switch location { + case .chatList: + guard case .global = chatPeerId.category else { + continue + } + case let .forum(peerId): + if chatPeerId.peerId != peerId { + continue + } + guard case let .thread(threadIdValue) = chatPeerId.category else { + continue + } + threadId = threadIdValue } var cachedChatResult: [(EnginePeer, PeerInputActivity)] = [] for (peerId, activity) in activities { @@ -1518,31 +1540,46 @@ public final class ChatListNode: ListView { foundAllPeers = false break } - cachedResult[chatPeerId.peerId] = cachedChatResult + cachedResult[ChatListNodePeerInputActivities.ItemId(peerId: chatPeerId.peerId, threadId: threadId)] = cachedChatResult } } } if foundAllPeers { return .single(cachedResult) } else { + var dataKeys: [EnginePeer.Id] = [] + for (peerId, activities) in activitiesByPeerId { + dataKeys.append(peerId.peerId) + for activity in activities { + dataKeys.append(activity.0) + } + } return engine.data.get(EngineDataMap( - activitiesByPeerId.keys.filter { key in - if case .global = key.category { - return true - } else { - return false - } - }.map { key in - return TelegramEngine.EngineData.Item.Peer.Peer(id: key.peerId) + Set(dataKeys).map { + TelegramEngine.EngineData.Item.Peer.Peer(id: $0) } )) - |> map { peerMap -> [EnginePeer.Id: [(EnginePeer, PeerInputActivity)]] in - var result: [EnginePeer.Id: [(EnginePeer, PeerInputActivity)]] = [:] + |> map { peerMap -> [ChatListNodePeerInputActivities.ItemId: [(EnginePeer, PeerInputActivity)]] in + var result: [ChatListNodePeerInputActivities.ItemId: [(EnginePeer, PeerInputActivity)]] = [:] var peerCache: [EnginePeer.Id: EnginePeer] = [:] for (chatPeerId, activities) in activitiesByPeerId { - guard case .global = chatPeerId.category else { - continue + let itemId: ChatListNodePeerInputActivities.ItemId + switch location { + case .chatList: + guard case .global = chatPeerId.category else { + continue + } + itemId = ChatListNodePeerInputActivities.ItemId(peerId: chatPeerId.peerId, threadId: nil) + case let .forum(peerId): + if chatPeerId.peerId != peerId { + continue + } + guard case let .thread(threadIdValue) = chatPeerId.category else { + continue + } + itemId = ChatListNodePeerInputActivities.ItemId(peerId: chatPeerId.peerId, threadId: threadIdValue) } + var chatResult: [(EnginePeer, PeerInputActivity)] = [] for (peerId, activity) in activities { @@ -1552,7 +1589,7 @@ public final class ChatListNode: ListView { } } - result[chatPeerId.peerId] = chatResult + result[itemId] = chatResult } let _ = previousPeerCache.swap(peerCache) return result @@ -1562,7 +1599,7 @@ public final class ChatListNode: ListView { |> map { activities -> ChatListNodePeerInputActivities? in return previousActivities.modify { current in var updated = false - let currentList: [EnginePeer.Id: [(EnginePeer, PeerInputActivity)]] = current?.activities ?? [:] + let currentList: [ChatListNodePeerInputActivities.ItemId: [(EnginePeer, PeerInputActivity)]] = current?.activities ?? [:] if currentList.count != activities.count { updated = true } else { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index ce291f0afc..7034036428 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -316,7 +316,7 @@ private func offsetPinnedIndex(_ index: EngineChatList.Item.Index, offset: UInt1 } } -func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, mode: ChatListNodeMode) -> (entries: [ChatListNodeEntry], loading: Bool) { +func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation) -> (entries: [ChatListNodeEntry], loading: Bool) { var result: [ChatListNodeEntry] = [] var pinnedIndexOffset: UInt16 = 0 @@ -343,8 +343,13 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState } loop: for entry in view.items { var peerId: EnginePeer.Id? + var activityItemId: ChatListNodePeerInputActivities.ItemId? if case let .chatList(index) = entry.index { peerId = index.messageIndex.id.peerId + activityItemId = ChatListNodePeerInputActivities.ItemId(peerId: index.messageIndex.id.peerId, threadId: nil) + } else if case let .forum(_, _, threadId, _, _) = entry.index, case let .forum(peerIdValue) = chatListLocation { + peerId = peerIdValue + activityItemId = ChatListNodePeerInputActivities.ItemId(peerId: peerIdValue, threadId: threadId) } if let savedMessagesPeer = savedMessagesPeer, let peerId = peerId, savedMessagesPeer.id == peerId || foundPeerIds.contains(peerId) { @@ -370,8 +375,8 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState hasActiveRevealControls = peerId == state.peerIdWithRevealedOptions } var inputActivities: [(EnginePeer, PeerInputActivity)]? - if let peerId { - inputActivities = state.peerInputActivities?.activities[peerId] + if let activityItemId { + inputActivities = state.peerInputActivities?.activities[activityItemId] } var threadId: Int64 = 0 @@ -474,7 +479,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState editing: state.editing, hasActiveRevealControls: peerId == state.peerIdWithRevealedOptions, selected: isSelected, - inputActivities: state.peerInputActivities?.activities[peerId], + inputActivities: state.peerInputActivities?.activities[ChatListNodePeerInputActivities.ItemId(peerId: peerId, threadId: nil)], promoInfo: promoInfo, hasFailedMessages: item.item.hasFailed, isContact: item.item.isContact, diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift index a0181137f6..e7a1b197cf 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift @@ -232,7 +232,7 @@ func chatListViewForLocation(chatListLocation: ChatListControllerLocation, locat pinnedIndex = .none } - let readCounters = EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: 1, maxKnownId: 1, count: data.incomingUnreadCount, markedUnread: false))]), isMuted: false) + let readCounters = EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: data.maxOutgoingReadId, maxKnownId: 1, count: data.incomingUnreadCount, markedUnread: false))]), isMuted: false) var draft: EngineChatList.Draft? if let embeddedState = item.embeddedInterfaceState, let _ = embeddedState.overrideChatTimestamp { diff --git a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift index 97be3219c5..e50289ef00 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift @@ -204,6 +204,11 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV fromEmptyView = true } + if let fromView = fromView, !fromView.isLoading, toView.isLoading { + options.remove(.AnimateInsertion) + options.remove(.AnimateAlpha) + } + var adjustScrollToFirstItem = false if !previewing && !searchMode && fromEmptyView && scrollToItem == nil && toView.filteredEntries.count >= 2 { adjustScrollToFirstItem = true diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index e41c9e0309..9872517b61 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -337,7 +337,6 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture open func animateIn(animateContent: Bool, useSimpleAnimation: Bool) { let duration: Double = animateContent ? 0.2 : 0.3 - let fadeDuration: Double = 0.2 let backgroundColor = self.backgroundNode.backgroundColor ?? .black @@ -346,9 +345,9 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture self.footerNode.alpha = 0.0 self.currentThumbnailContainerNode?.alpha = 0.0 - self.backgroundNode.layer.animate(from: backgroundColor.withAlphaComponent(0.0).cgColor, to: backgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: fadeDuration) + self.backgroundNode.layer.animate(from: backgroundColor.withAlphaComponent(0.0).cgColor, to: backgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.15) - UIView.animate(withDuration: fadeDuration, delay: 0.0, options: [.curveLinear], animations: { + UIView.animate(withDuration: 0.15, delay: 0.0, options: [.curveLinear], animations: { if !self.areControlsHidden { self.statusBar?.alpha = 1.0 self.navigationBar?.alpha = 1.0 diff --git a/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift b/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift index 94a0c87568..a381a3c3a5 100644 --- a/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift @@ -173,6 +173,14 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, UIScrollViewDel self.scrollNode.view.contentSize = contentSize updated = true } + + var progress = progress ?? 0.0 + if centralIndex == 0 && progress < 0.0 { + progress = 0.0 + } else if centralIndex == self.itemNodes.count - 1 && progress > 0.0 { + progress = 0.0 + } + if updated || !self.isPanning { transition.animateView { @@ -180,7 +188,6 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, UIScrollViewDel } } - let progress = progress ?? 0.0 var itemFrames: [CGRect] = [] var lastTrailingSpacing: CGFloat = 0.0 var xOffset: CGFloat = -itemBaseSize.width / 2.0 diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 37c850d737..11142b4271 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1419,7 +1419,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if isAnimated || disablePlayerControls { strongSelf.footerContentNode.content = .info - } else if isPaused && !strongSelf.ignorePauseStatus { + } else if isPaused && !strongSelf.ignorePauseStatus && strongSelf.isCentral == true { if hasStarted || strongSelf.didPause { strongSelf.footerContentNode.content = .playback(paused: true, seekable: seekable) } else if let fetchStatus = fetchStatus, !strongSelf.requiresDownload { diff --git a/submodules/Postbox/Sources/ChatListHolesView.swift b/submodules/Postbox/Sources/ChatListHolesView.swift index c1c1d103db..89bbc5d4a5 100644 --- a/submodules/Postbox/Sources/ChatListHolesView.swift +++ b/submodules/Postbox/Sources/ChatListHolesView.swift @@ -30,3 +30,35 @@ public final class ChatListHolesView { self.entries = mutableView.entries } } + +public struct ForumTopicListHolesEntry: Hashable { + public let peerId: PeerId + public let index: StoredPeerThreadCombinedState.Index? + + public init(peerId: PeerId, index: StoredPeerThreadCombinedState.Index?) { + self.peerId = peerId + self.index = index + } +} + +final class MutableForumTopicListHolesView { + fileprivate var entries = Set() + + func update(holes: Set) -> Bool { + if self.entries != holes { + self.entries = holes + return true + } else { + return false + } + } +} + +public final class ForumTopicListHolesView { + public let entries: Set + + init(_ mutableView: MutableForumTopicListHolesView) { + self.entries = mutableView.entries + } +} + diff --git a/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift b/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift index 1e2517bb89..ec03eeae0a 100644 --- a/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift @@ -81,7 +81,7 @@ final class MessageHistoryMetadataTable: Table { private func peerThreadHoleIndexInitializedKey(peerId: PeerId, threadId: Int64) -> ValueBoxKey { self.sharedPeerThreadHoleIndexInitializedKey.setInt64(0, value: peerId.toInt64()) self.sharedPeerThreadHoleIndexInitializedKey.setInt8(8, value: MetadataPrefix.PeerHistoryThreadHoleIndexInitialized.rawValue) - self.sharedPeerThreadHoleIndexInitializedKey.setInt64(0, value: threadId) + self.sharedPeerThreadHoleIndexInitializedKey.setInt64(8 + 1, value: threadId) return self.sharedPeerThreadHoleIndexInitializedKey } diff --git a/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift b/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift index b685eaa561..8d7f30da7a 100644 --- a/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift +++ b/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift @@ -33,6 +33,7 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView { fileprivate let summaryComponents: ChatListEntrySummaryComponents fileprivate var peer: Peer? fileprivate var items: [Item] = [] + private var hole: ForumTopicListHolesEntry? fileprivate var isLoading: Bool = false init(postbox: PostboxImpl, peerId: PeerId, summaryComponents: ChatListEntrySummaryComponents) { @@ -50,6 +51,16 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView { let validIndexBoundary = postbox.peerThreadCombinedStateTable.get(peerId: peerId)?.validIndexBoundary self.isLoading = validIndexBoundary == nil + if let validIndexBoundary = validIndexBoundary { + if validIndexBoundary.messageId != 1 { + self.hole = ForumTopicListHolesEntry(peerId: self.peerId, index: validIndexBoundary) + } else { + self.hole = nil + } + } else { + self.hole = ForumTopicListHolesEntry(peerId: self.peerId, index: nil) + } + if !self.isLoading { let pinnedThreadIds = postbox.messageHistoryThreadPinnedTable.get(peerId: self.peerId) var nextPinnedIndex = 0 @@ -124,9 +135,15 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView { return updated } + + func topHole() -> ForumTopicListHolesEntry? { + return self.hole + } func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { - return false + self.reload(postbox: postbox) + + return true } func immutableView() -> PostboxView { diff --git a/submodules/Postbox/Sources/PeerThreadCombinedStateTable.swift b/submodules/Postbox/Sources/PeerThreadCombinedStateTable.swift index 81a3b1739d..2109cf602d 100644 --- a/submodules/Postbox/Sources/PeerThreadCombinedStateTable.swift +++ b/submodules/Postbox/Sources/PeerThreadCombinedStateTable.swift @@ -1,7 +1,7 @@ import Foundation public struct StoredPeerThreadCombinedState: Equatable, Codable { - public struct Index: Equatable, Comparable, Codable { + public struct Index: Hashable, Comparable, Codable { private enum CodingKeys: String, CodingKey { case timestamp = "t" case threadId = "i" diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index ca5add8f93..615e476ff1 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -3281,6 +3281,18 @@ final class PostboxImpl { } } + public func forumTopicListHolesView() -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.queue.async { + disposable.set(self.viewTracker.forumTopicListHolesViewSignal().start(next: { view in + subscriber.putNext(view) + })) + } + return disposable + } + } + public func unsentMessageIdsView() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() @@ -4195,6 +4207,18 @@ public class Postbox { return disposable } } + + public func forumTopicListHolesView() -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.forumTopicListHolesView().start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)) + } + + return disposable + } + } public func unsentMessageIdsView() -> Signal { return Signal { subscriber in diff --git a/submodules/Postbox/Sources/PostboxView.swift b/submodules/Postbox/Sources/PostboxView.swift index 710fb0b21a..7cf89152ae 100644 --- a/submodules/Postbox/Sources/PostboxView.swift +++ b/submodules/Postbox/Sources/PostboxView.swift @@ -10,7 +10,7 @@ protocol MutablePostboxView { } final class CombinedMutableView { - private let views: [PostboxViewKey: MutablePostboxView] + let views: [PostboxViewKey: MutablePostboxView] init(views: [PostboxViewKey: MutablePostboxView]) { self.views = views diff --git a/submodules/Postbox/Sources/ViewTracker.swift b/submodules/Postbox/Sources/ViewTracker.swift index fa275fc344..e544bb82fe 100644 --- a/submodules/Postbox/Sources/ViewTracker.swift +++ b/submodules/Postbox/Sources/ViewTracker.swift @@ -25,6 +25,9 @@ final class ViewTracker { private let chatListHolesView = MutableChatListHolesView() private let chatListHolesViewSubscribers = Bag>() + private let forumTopicListHolesView = MutableForumTopicListHolesView() + private let forumTopicListHolesViewSubscribers = Bag>() + private var unsentMessageView: UnsentMessageHistoryView private let unsendMessageIdsViewSubscribers = Bag>() @@ -407,6 +410,8 @@ final class ViewTracker { pipe.putNext(view.immutableView()) } } + + self.updateTrackedForumTopicListHoles() } private func updateTrackedChatListHoles() { @@ -425,6 +430,26 @@ final class ViewTracker { } } + private func updateTrackedForumTopicListHoles() { + var firstHoles = Set() + + for (views) in self.combinedViews.copyItems() { + for (key, view) in views.0.views { + if case .messageHistoryThreadIndex = key, let view = view as? MutableMessageHistoryThreadIndexView { + if let hole = view.topHole() { + firstHoles.insert(hole) + } + } + } + } + + if self.forumTopicListHolesView.update(holes: firstHoles) { + for pipe in self.forumTopicListHolesViewSubscribers.copyItems() { + pipe.putNext(ForumTopicListHolesView(self.forumTopicListHolesView)) + } + } + } + private func updateTrackedHoles() { var firstHolesAndTags = Set() for (view, _) in self.messageHistoryViews.copyItems() { @@ -506,6 +531,30 @@ final class ViewTracker { } } + func forumTopicListHolesViewSignal() -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.queue.async { + subscriber.putNext(ForumTopicListHolesView(self.forumTopicListHolesView)) + + let pipe = ValuePipe() + let index = self.forumTopicListHolesViewSubscribers.add(pipe) + + let pipeDisposable = pipe.signal().start(next: { view in + subscriber.putNext(view) + }) + + disposable.set(ActionDisposable { + self.queue.async { + pipeDisposable.dispose() + self.forumTopicListHolesViewSubscribers.remove(index) + } + }) + } + return disposable + } + } + func unsentMessageIdsViewSignal() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() diff --git a/submodules/TelegramCore/Sources/ForumChannels.swift b/submodules/TelegramCore/Sources/ForumChannels.swift index 8faa7edc77..a0c2d8ce23 100644 --- a/submodules/TelegramCore/Sources/ForumChannels.swift +++ b/submodules/TelegramCore/Sources/ForumChannels.swift @@ -226,7 +226,7 @@ func _internal_createForumChannelTopic(account: Account, peerId: PeerId, title: } if let topicId = topicId { - return resolveForumThreads(postbox: account.postbox, network: account.network, ids: []) + return resolveForumThreads(postbox: account.postbox, network: account.network, ids: [MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: topicId))]) |> castError(CreateForumChannelTopicError.self) |> map { _ -> Int64 in return topicId @@ -373,22 +373,6 @@ func _internal_setForumChannelTopicPinned(account: Account, id: EnginePeer.Id, t account.stateManager.addUpdates(result) return .complete() - - /*return account.postbox.transaction { transaction -> Void in - if let initialData = transaction.getMessageHistoryThreadInfo(peerId: id, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { - var data = initialData - - data.isClosed = isClosed - - if data != initialData { - if let entry = StoredMessageHistoryThreadInfo(data) { - transaction.setMessageHistoryThreadInfo(peerId: id, threadId: threadId, info: entry) - } - } - } - } - |> castError(EditForumChannelTopicError.self) - |> ignoreValues*/ } } } @@ -421,8 +405,8 @@ enum LoadMessageHistoryThreadsError { case generic } -func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Signal { - let signal: Signal = account.postbox.transaction { transaction -> Api.InputChannel? in +func _internal_loadMessageHistoryThreads(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId, offsetIndex: StoredPeerThreadCombinedState.Index?, limit: Int) -> Signal { + let signal: Signal = postbox.transaction { transaction -> Api.InputChannel? in return transaction.getPeer(peerId).flatMap(apiInputChannel) } |> castError(LoadMessageHistoryThreadsError.self) @@ -430,23 +414,32 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si guard let inputChannel = inputChannel else { return .fail(.generic) } - let signal: Signal = account.network.request(Api.functions.channels.getForumTopics( - flags: 0, + let flags: Int32 = 0 + var offsetDate: Int32 = 0 + var offsetId: Int32 = 0 + var offsetTopic: Int32 = 0 + if let offsetIndex = offsetIndex { + offsetDate = offsetIndex.timestamp + offsetId = offsetIndex.messageId + offsetTopic = Int32(clamping: offsetIndex.threadId) + } + let signal: Signal = network.request(Api.functions.channels.getForumTopics( + flags: flags, channel: inputChannel, q: nil, - offsetDate: 0, - offsetId: 0, - offsetTopic: 0, - limit: 100 + offsetDate: offsetDate, + offsetId: offsetId, + offsetTopic: offsetTopic, + limit: Int32(limit) )) |> mapError { _ -> LoadMessageHistoryThreadsError in return .generic } |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> Void in + return postbox.transaction { transaction -> Void in var pinnedId: Int64? switch result { - case let .forumTopics(flags, count, topics, messages, chats, users, pts): + case let .forumTopics(_, _, topics, messages, chats, users, pts): var peers: [Peer] = [] var peerPresences: [PeerId: Api.User] = [:] for chat in chats { @@ -463,19 +456,16 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si return updated }) - updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) + updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) - let _ = InternalAccountState.addMessages(transaction: transaction, messages: messages.compactMap { message -> StoreMessage? in + let addedMessages = messages.compactMap { message -> StoreMessage? in return StoreMessage(apiMessage: message) - }, location: .Random) + } + + let _ = InternalAccountState.addMessages(transaction: transaction, messages: addedMessages, location: .Random) - let _ = flags - let _ = count - let _ = topics - let _ = messages - let _ = chats - let _ = users let _ = pts + var minIndex: StoredPeerThreadCombinedState.Index? for topic in topics { switch topic { @@ -509,6 +499,22 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si transaction.replaceMessageTagSummary(peerId: peerId, threadId: Int64(id), tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, count: unreadMentionsCount, maxId: topMessage) transaction.replaceMessageTagSummary(peerId: peerId, threadId: Int64(id), tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud, count: unreadReactionsCount, maxId: topMessage) + + var topTimestamp = date + for message in addedMessages { + if message.id.peerId == peerId && message.threadId == Int64(id) { + topTimestamp = max(topTimestamp, message.timestamp) + } + } + + let topicIndex = StoredPeerThreadCombinedState.Index(timestamp: topTimestamp, threadId: Int64(id), messageId: topMessage) + if let minIndexValue = minIndex { + if topicIndex < minIndexValue { + minIndex = topicIndex + } + } else { + minIndex = topicIndex + } case .forumTopicDeleted: break } @@ -520,9 +526,17 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si transaction.setPeerPinnedThreads(peerId: peerId, threadIds: []) } - if let entry = StoredPeerThreadCombinedState(PeerThreadCombinedState( - validIndexBoundary: StoredPeerThreadCombinedState.Index(timestamp: Int32.max, threadId: Int64(Int32.max), messageId: Int32.max) - )) { + var nextIndex: StoredPeerThreadCombinedState.Index + if topics.count != 0 { + nextIndex = minIndex ?? StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1) + } else { + nextIndex = StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1) + } + if let offsetIndex = offsetIndex, nextIndex == offsetIndex { + nextIndex = StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1) + } + + if let entry = StoredPeerThreadCombinedState(PeerThreadCombinedState(validIndexBoundary: nextIndex)) { transaction.setPeerThreadCombinedState(peerId: peerId, state: entry) } } @@ -641,7 +655,7 @@ public final class ForumChannelTopics { self.account = account self.peerId = peerId - let _ = _internal_loadMessageHistoryThreads(account: self.account, peerId: peerId).start() + //let _ = _internal_loadMessageHistoryThreads(account: self.account, peerId: peerId, offsetIndex: nil, limit: 100).start() self.updateDisposable.set(account.viewTracker.polledChannel(peerId: peerId).start()) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 0f1d35fb49..44894d7877 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -1328,6 +1328,49 @@ public final class AccountStateManager { } } +func resolveNotificationSettings(list: [TelegramPeerNotificationSettings], defaultSettings: MessageNotificationSettings) -> (sound: PeerMessageSound, notify: Bool, displayContents: Bool) { + var sound: PeerMessageSound = defaultSettings.sound + + var notify = defaultSettings.enabled + var displayContents = defaultSettings.displayPreviews + + for item in list.reversed() { + if case .default = item.messageSound { + } else { + sound = item.messageSound + } + + switch item.displayPreviews { + case .default: + break + case .show: + displayContents = true + case .hide: + displayContents = false + } + + switch item.muteState { + case .default: + break + case .unmuted: + notify = true + case let .muted(deadline): + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + if deadline > timestamp { + notify = false + } else { + notify = true + } + } + } + + if case .default = sound { + sound = defaultCloudPeerNotificationSound + } + + return (sound, notify, displayContents) +} + public func messagesForNotification(transaction: Transaction, id: MessageId, alwaysReturnMessage: Bool) -> (messages: [Message], notify: Bool, sound: PeerMessageSound, displayContents: Bool, threadData: MessageHistoryThreadData?) { guard let message = transaction.getMessage(id) else { Logger.shared.log("AccountStateManager", "notification message doesn't exist") @@ -1335,7 +1378,6 @@ public func messagesForNotification(transaction: Transaction, id: MessageId, alw } var notify = true - var sound: PeerMessageSound = defaultCloudPeerNotificationSound var muted = false var displayContents = true var threadData: MessageHistoryThreadData? @@ -1365,8 +1407,6 @@ public func messagesForNotification(transaction: Transaction, id: MessageId, alw } } - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - var notificationPeerId = id.peerId let peer = transaction.getPeer(id.peerId) if let peer = peer, let associatedPeerId = peer.associatedPeerId { @@ -1376,47 +1416,38 @@ public func messagesForNotification(transaction: Transaction, id: MessageId, alw notificationPeerId = author.id } + var notificationSettingsStack: [TelegramPeerNotificationSettings] = [] + + if let threadId = message.threadId, let threadData = transaction.getMessageHistoryThreadInfo(peerId: message.id.peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { + notificationSettingsStack.append(threadData.notificationSettings) + } + if let notificationSettings = transaction.getPeerNotificationSettings(id: notificationPeerId) as? TelegramPeerNotificationSettings { - var defaultSound: PeerMessageSound = defaultCloudPeerNotificationSound - var defaultNotify: Bool = true - if let globalNotificationSettings = transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications)?.get(GlobalNotificationSettings.self) { - if id.peerId.namespace == Namespaces.Peer.CloudUser { - defaultNotify = globalNotificationSettings.effective.privateChats.enabled - defaultSound = globalNotificationSettings.effective.privateChats.sound - displayContents = globalNotificationSettings.effective.privateChats.displayPreviews - } else if id.peerId.namespace == Namespaces.Peer.SecretChat { - defaultNotify = globalNotificationSettings.effective.privateChats.enabled - defaultSound = globalNotificationSettings.effective.privateChats.sound - displayContents = false - } else if id.peerId.namespace == Namespaces.Peer.CloudChannel, let peer = peer as? TelegramChannel, case .broadcast = peer.info { - defaultNotify = globalNotificationSettings.effective.channels.enabled - defaultSound = globalNotificationSettings.effective.channels.sound - displayContents = globalNotificationSettings.effective.channels.displayPreviews - } else { - defaultNotify = globalNotificationSettings.effective.groupChats.enabled - defaultSound = globalNotificationSettings.effective.groupChats.sound - displayContents = globalNotificationSettings.effective.groupChats.displayPreviews - } - } - switch notificationSettings.muteState { - case .default: - if !defaultNotify { - notify = false - } - case let .muted(until): - if until >= timestamp { - notify = false - } - case .unmuted: - break - } - if case .default = notificationSettings.messageSound { - sound = defaultSound - } else { - sound = notificationSettings.messageSound - } + notificationSettingsStack.append(notificationSettings) + } + + let globalNotificationSettings = transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications)?.get(GlobalNotificationSettings.self) ?? GlobalNotificationSettings.defaultSettings + + let defaultNotificationSettings: MessageNotificationSettings + if id.peerId.namespace == Namespaces.Peer.CloudUser { + defaultNotificationSettings = globalNotificationSettings.effective.privateChats + } else if id.peerId.namespace == Namespaces.Peer.SecretChat { + defaultNotificationSettings = globalNotificationSettings.effective.privateChats + displayContents = false + } else if id.peerId.namespace == Namespaces.Peer.CloudChannel, let peer = peer as? TelegramChannel, case .broadcast = peer.info { + defaultNotificationSettings = globalNotificationSettings.effective.channels } else { - Logger.shared.log("AccountStateManager", "notification settings for \(notificationPeerId) are undefined") + defaultNotificationSettings = globalNotificationSettings.effective.groupChats + } + + let (resolvedSound, resolvedNotify, resolvedDisplayContents) = resolveNotificationSettings(list: notificationSettingsStack, defaultSettings: defaultNotificationSettings) + + var sound = resolvedSound + if !resolvedNotify { + notify = false + } + if !resolvedDisplayContents { + displayContents = false } if muted { @@ -1425,10 +1456,10 @@ public func messagesForNotification(transaction: Transaction, id: MessageId, alw if let channel = message.peers[message.id.peerId] as? TelegramChannel { switch channel.participationStatus { - case .kicked, .left: - return ([], false, sound, false, threadData) - case .member: - break + case .kicked, .left: + return ([], false, sound, false, threadData) + case .member: + break } } diff --git a/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift b/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift index 920f6040ac..d452e38546 100644 --- a/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift +++ b/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift @@ -90,3 +90,70 @@ func managedChatListHoles(network: Network, postbox: Postbox, accountPeerId: Pee } } } + +private final class ManagedForumTopicListHolesState { + private var currentHoles: [ForumTopicListHolesEntry: Disposable] = [:] + + func clearDisposables() -> [Disposable] { + let disposables = Array(self.currentHoles.values) + self.currentHoles.removeAll() + return disposables + } + + func update(entries: [ForumTopicListHolesEntry]) -> (removed: [Disposable], added: [ForumTopicListHolesEntry: MetaDisposable]) { + var removed: [Disposable] = [] + var added: [ForumTopicListHolesEntry: MetaDisposable] = [:] + + for entry in entries { + if self.currentHoles[entry] == nil { + let disposable = MetaDisposable() + added[entry] = disposable + self.currentHoles[entry] = disposable + } + } + + var removedKeys: [ForumTopicListHolesEntry] = [] + for (entry, disposable) in self.currentHoles { + if !entries.contains(entry) { + removed.append(disposable) + removedKeys.append(entry) + } + } + for key in removedKeys { + self.currentHoles.removeValue(forKey: key) + } + + return (removed, added) + } +} + +func managedForumTopicListHoles(network: Network, postbox: Postbox, accountPeerId: PeerId) -> Signal { + return Signal { _ in + let state = Atomic(value: ManagedForumTopicListHolesState()) + + let disposable = postbox.forumTopicListHolesView().start(next: { view in + let entries = Array(view.entries) + + let (removed, added) = state.with { state in + return state.update(entries: entries) + } + + for disposable in removed { + disposable.dispose() + } + + for (entry, disposable) in added { + disposable.set(_internal_loadMessageHistoryThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, peerId: entry.peerId, offsetIndex: entry.index, limit: 100).start()) + } + }) + + return ActionDisposable { + disposable.dispose() + for disposable in state.with({ state -> [Disposable] in + state.clearDisposables() + }) { + disposable.dispose() + } + } + } +} diff --git a/submodules/TelegramCore/Sources/State/ManagedServiceViews.swift b/submodules/TelegramCore/Sources/State/ManagedServiceViews.swift index 03eaf49c90..1286e780d4 100644 --- a/submodules/TelegramCore/Sources/State/ManagedServiceViews.swift +++ b/submodules/TelegramCore/Sources/State/ManagedServiceViews.swift @@ -7,6 +7,7 @@ func managedServiceViews(accountPeerId: PeerId, network: Network, postbox: Postb let disposable = DisposableSet() disposable.add(managedMessageHistoryHoles(accountPeerId: accountPeerId, network: network, postbox: postbox).start()) disposable.add(managedChatListHoles(network: network, postbox: postbox, accountPeerId: accountPeerId).start()) + disposable.add(managedForumTopicListHoles(network: network, postbox: postbox, accountPeerId: accountPeerId).start()) disposable.add(managedSynchronizePeerReadStates(network: network, postbox: postbox, stateManager: stateManager).start()) disposable.add(managedSynchronizeGroupMessageStats(network: network, postbox: postbox, stateManager: stateManager).start()) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 96b3f5321a..e81997b2ab 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -270,14 +270,17 @@ private class AdMessagesHistoryContextImpl { struct CachedState: Codable, PostboxCoding { enum CodingKeys: String, CodingKey { case timestamp + case interPostInterval case messages } var timestamp: Int32 + var interPostInterval: Int32? var messages: [CachedMessage] - init(timestamp: Int32, messages: [CachedMessage]) { + init(timestamp: Int32, interPostInterval: Int32?, messages: [CachedMessage]) { self.timestamp = timestamp + self.interPostInterval = interPostInterval self.messages = messages } @@ -285,6 +288,7 @@ private class AdMessagesHistoryContextImpl { let container = try decoder.container(keyedBy: CodingKeys.self) self.timestamp = try container.decode(Int32.self, forKey: .timestamp) + self.interPostInterval = try container.decodeIfPresent(Int32.self, forKey: .interPostInterval) self.messages = try container.decode([CachedMessage].self, forKey: .messages) } @@ -292,11 +296,13 @@ private class AdMessagesHistoryContextImpl { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.timestamp, forKey: .timestamp) + try container.encodeIfPresent(self.interPostInterval, forKey: .interPostInterval) try container.encode(self.messages, forKey: .messages) } init(decoder: PostboxDecoder) { self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0) + self.interPostInterval = decoder.decodeOptionalInt32ForKey("interPostInterval") if let messagesData = decoder.decodeOptionalDataArrayForKey("messages") { self.messages = messagesData.compactMap { data -> CachedMessage? in return try? AdaptedPostboxDecoder().decode(CachedMessage.self, from: data) @@ -308,6 +314,11 @@ private class AdMessagesHistoryContextImpl { func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.timestamp, forKey: "timestamp") + if let interPostInterval = self.interPostInterval { + encoder.encodeInt32(interPostInterval, forKey: "interPostInterval") + } else { + encoder.encodeNil(forKey: "interPostInterval") + } encoder.encodeDataArray(self.messages.compactMap { message -> Data? in return try? AdaptedPostboxEncoder().encode(message) }, forKey: "messages") @@ -338,9 +349,13 @@ private class AdMessagesHistoryContextImpl { } struct State: Equatable { + var interPostInterval: Int32? var messages: [Message] static func ==(lhs: State, rhs: State) -> Bool { + if lhs.interPostInterval != rhs.interPostInterval { + return false + } if lhs.messages.count != rhs.messages.count { return false } @@ -372,43 +387,41 @@ private class AdMessagesHistoryContextImpl { self.account = account self.peerId = peerId - self.stateValue = State(messages: []) + self.stateValue = State(interPostInterval: nil, messages: []) self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId) |> mapToSignal { cachedState -> Signal in if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 { return account.postbox.transaction { transaction -> State in - return State(messages: cachedState.messages.compactMap { message -> Message? in + return State(interPostInterval: cachedState.interPostInterval, messages: cachedState.messages.compactMap { message -> Message? in return message.toMessage(peerId: peerId, transaction: transaction) }) } } else { - return .single(State(messages: [])) + return .single(State(interPostInterval: nil, messages: [])) } }) - let signal: Signal<[Message], NoError> = account.postbox.transaction { transaction -> Api.InputChannel? in + let signal: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputChannel? in return transaction.getPeer(peerId).flatMap(apiInputChannel) } - |> mapToSignal { inputChannel -> Signal<[Message], NoError> in + |> mapToSignal { inputChannel -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in guard let inputChannel = inputChannel else { - return .single([]) + return .single((nil, [])) } return account.network.request(Api.functions.channels.getSponsoredMessages(channel: inputChannel)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal<[Message], NoError> in + |> mapToSignal { result -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in guard let result = result else { - return .single([]) + return .single((nil, [])) } - return account.postbox.transaction { transaction -> [Message] in + return account.postbox.transaction { transaction -> (interPostInterval: Int32?, messages: [Message]) in switch result { case let .sponsoredMessages(_, postsBetween, messages, chats, users): - let _ = postsBetween - var peers: [Peer] = [] var peerPresences: [PeerId: Api.User] = [:] @@ -501,24 +514,24 @@ private class AdMessagesHistoryContextImpl { } } - CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), messages: parsedMessages)) + CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), interPostInterval: postsBetween, messages: parsedMessages)) - return parsedMessages.compactMap { message -> Message? in + return (postsBetween, parsedMessages.compactMap { message -> Message? in return message.toMessage(peerId: peerId, transaction: transaction) - } + }) case .sponsoredMessagesEmpty: - return [] + return (nil, []) } } } } self.disposable.set((signal - |> deliverOn(self.queue)).start(next: { [weak self] messages in + |> deliverOn(self.queue)).start(next: { [weak self] interPostInterval, messages in guard let strongSelf = self else { return } - strongSelf.stateValue = State(messages: messages) + strongSelf.stateValue = State(interPostInterval: interPostInterval, messages: messages) })) } @@ -549,13 +562,13 @@ public class AdMessagesHistoryContext { private let queue = Queue() private let impl: QueueLocalObject - public var state: Signal<[Message], NoError> { + public var state: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in let stateDisposable = impl.state.get().start(next: { state in - subscriber.putNext(state.messages) + subscriber.putNext((state.interPostInterval, state.messages)) }) disposable.set(stateDisposable) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift index 646dd30a09..38e66f82e8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift @@ -29,10 +29,14 @@ public final class EngineChatList: Equatable { public struct ForumTopicData: Equatable { public var title: String + public let iconFileId: Int64? + public let iconColor: Int32 public var maxOutgoingReadMessageId: EngineMessage.Id - public init(title: String, maxOutgoingReadMessageId: EngineMessage.Id) { + public init(title: String, iconFileId: Int64?, iconColor: Int32, maxOutgoingReadMessageId: EngineMessage.Id) { self.title = title + self.iconFileId = iconFileId + self.iconColor = iconColor self.maxOutgoingReadMessageId = maxOutgoingReadMessageId } } @@ -422,7 +426,7 @@ extension EngineChatList.Item { var forumTopicDataValue: EngineChatList.ForumTopicData? if let forumTopicData = forumTopicData?.data.get(MessageHistoryThreadData.self) { - forumTopicDataValue = EngineChatList.ForumTopicData(title: forumTopicData.info.title, maxOutgoingReadMessageId: MessageId(peerId: index.messageIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: forumTopicData.maxOutgoingReadId)) + forumTopicDataValue = EngineChatList.ForumTopicData(title: forumTopicData.info.title, iconFileId: forumTopicData.info.icon, iconColor: forumTopicData.info.iconColor, maxOutgoingReadMessageId: MessageId(peerId: index.messageIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: forumTopicData.maxOutgoingReadId)) } let readCounters = readState.flatMap(EnginePeerReadCounters.init) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index a0bf7e87ec..8c7c01f4e4 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -95,6 +95,7 @@ public enum PresentationResourceKey: Int32 { case chatListFakeServiceIcon case chatListSecretIcon case chatListStatusLockIcon + case chatListTopicArrowIcon case chatListRecentStatusOnlineIcon case chatListRecentStatusOnlineHighlightedIcon case chatListRecentStatusOnlinePinnedIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift index e1e42135a3..6f5bf37308 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift @@ -360,4 +360,10 @@ public struct PresentationResourcesChatList { return generateTintedImage(image: UIImage(bundleImageName: "Chat List/StatusLockIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor) }) } + + public static func topicArrowIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListTopicArrowIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/TopicArrowIcon"), color: theme.chatList.titleColor) + }) + } } diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index ee3b91af17..71ce323559 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -420,7 +420,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } else { if let titleContent = self.titleContent { switch titleContent { - case let .peer(peerView, _, onlineMemberCount, isScheduledMessages, _): + case let .peer(peerView, customTitle, onlineMemberCount, isScheduledMessages, _): if let peer = peerViewMainPeer(peerView) { let servicePeer = isServicePeer(peer) if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isReplies { @@ -485,7 +485,10 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { state = .info(string, .generic) } } else if let channel = peer as? TelegramChannel { - if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { + if channel.flags.contains(.isForum), customTitle != nil { + let string = NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + state = .info(string, .generic) + } else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { if memberCount == 0 { let string: NSAttributedString if case .group = channel.info { diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index 684f9ad4ea..aa6d315c0a 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -223,16 +223,19 @@ public final class EmojiStatusComponent: Component { } else { iconImage = nil } - case let .topic(title, color, size): + case let .topic(title, color, realSize): func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor]) -> UIImage? { - return generateImage(size, rotatedContext: { size, context in - context.clear(CGRect(origin: .zero, size: size)) + return generateImage(realSize, rotatedContext: { realSize, context in + context.clear(CGRect(origin: .zero, size: realSize)) context.saveGState() - let scale: CGFloat = size.width / 32.0 - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + let size = CGSize(width: 32.0, height: 32.0) + + let scale: CGFloat = realSize.width / size.width context.scaleBy(x: scale, y: scale) + + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.translateBy(x: -14.0 - UIScreenPixel, y: -14.0 - UIScreenPixel) let _ = try? drawSvgPath(context, path: "M24.1835,4.71703 C21.7304,2.42169 18.2984,0.995605 14.5,0.995605 C7.04416,0.995605 1.0,6.49029 1.0,13.2683 C1.0,17.1341 2.80572,20.3028 5.87839,22.5523 C6.27132,22.84 6.63324,24.4385 5.75738,25.7811 C5.39922,26.3301 5.00492,26.7573 4.70138,27.0861 C4.26262,27.5614 4.01347,27.8313 4.33716,27.967 C4.67478,28.1086 6.66968,28.1787 8.10952,27.3712 C9.23649,26.7392 9.91903,26.1087 10.3787,25.6842 C10.7588,25.3331 10.9864,25.1228 11.187,25.1688 C11.9059,25.3337 12.6478,25.4461 13.4075,25.5015 C13.4178,25.5022 13.4282,25.503 13.4386,25.5037 C13.7888,25.5284 14.1428,25.5411 14.5,25.5411 C21.9558,25.5411 28.0,20.0464 28.0,13.2683 C28.0,9.94336 26.5455,6.92722 24.1835,4.71703 ") @@ -269,13 +272,12 @@ public final class EmojiStatusComponent: Component { let line = CTLineCreateWithAttributedString(attributedString) let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + let lineOffset = CGPoint(x: title == "B" ? 1.0 : 0.0, y: floorToScreenPixels(realSize.height * 0.05)) + let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (realSize.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (realSize.height - lineBounds.size.height) / 2.0) + lineOffset.y) - let lineOffset = CGPoint(x: title == "B" ? 1.0 : 0.0, y: floorToScreenPixels(0.67 * scale)) - let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0) + lineOffset.y) - - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.translateBy(x: realSize.width / 2.0, y: realSize.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.translateBy(x: -realSize.width / 2.0, y: -realSize.height / 2.0) context.translateBy(x: lineOrigin.x, y: lineOrigin.y) CTLineDraw(line, context) diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/BUILD b/submodules/TelegramUI/Components/ForumCreateTopicScreen/BUILD index 7db4992f3d..d00b21fc02 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/BUILD +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/Components/PagerComponent:PagerComponent", "//submodules/PremiumUI", + "//submodules/ProgressNavigationButtonNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index e776083d51..740622106e 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -15,6 +15,7 @@ import MultilineTextComponent import EmojiStatusComponent import Postbox import PremiumUI +import ProgressNavigationButtonNode private final class TitleFieldComponent: Component { typealias EnvironmentType = Empty @@ -162,6 +163,8 @@ private final class TitleFieldComponent: Component { placeholderComponentView.frame = CGRect(origin: CGPoint(x: 62.0, y: floorToScreenPixels((availableSize.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize) } + self.placeholderView.view?.isHidden = !component.text.isEmpty + let iconSize = self.iconView.update( transition: .easeInOut(duration: 0.2), component: AnyComponent(EmojiStatusComponent( @@ -782,10 +785,32 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { case edit(topic: EngineMessageHistoryThread.Info) } + private let context: AccountContext + private let mode: Mode + + private var doneBarItem: UIBarButtonItem? + private var state: (String, Int64?) = ("", nil) public var completion: (String, Int64?) -> Void = { _, _ in } + public var isInProgress: Bool = false { + didSet { + if self.isInProgress != oldValue { + if self.isInProgress { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: presentationData.theme.rootController.navigationBar.accentTextColor)) + } else { + //TODO:localize + self.navigationItem.rightBarButtonItem = self.doneBarItem + } + } + } + } + public init(context: AccountContext, peerId: EnginePeer.Id, mode: ForumCreateTopicScreen.Mode) { + self.context = context + self.mode = mode + var titleUpdatedImpl: ((String) -> Void)? var iconUpdatedImpl: ((Int64?) -> Void)? var openPremiumImpl: (() -> Void)? @@ -802,12 +827,14 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { let title: String let doneTitle: String switch mode { - case .create: - title = "New Topic" - doneTitle = "Create" - case .edit: - title = "Edit Topic" - doneTitle = "Done" + case .create: + title = "New Topic" + doneTitle = "Create" + case let .edit(topic): + title = "Edit Topic" + doneTitle = "Done" + + self.state = (topic.title, topic.icon) } self.title = title @@ -815,20 +842,21 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: doneTitle, style: .done, target: self, action: #selector(self.createPressed)) - self.navigationItem.rightBarButtonItem?.isEnabled = false + self.doneBarItem = UIBarButtonItem(title: doneTitle, style: .done, target: self, action: #selector(self.createPressed)) + self.navigationItem.rightBarButtonItem = self.doneBarItem + self.doneBarItem?.isEnabled = false if case .edit = mode { - self.navigationItem.rightBarButtonItem?.isEnabled = true + self.doneBarItem?.isEnabled = true } titleUpdatedImpl = { [weak self] title in - guard let strongSelf = self else { + guard let self else { return } - strongSelf.navigationItem.rightBarButtonItem?.isEnabled = !title.isEmpty + self.doneBarItem?.isEnabled = !title.isEmpty - strongSelf.state = (title, strongSelf.state.1) + self.state = (title, self.state.1) } iconUpdatedImpl = { [weak self] fileId in @@ -844,8 +872,8 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { return } var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumDemoScreen(context: context, subject: .uniqueReactions, action: { - let controller = PremiumIntroScreen(context: context, source: .reactions) + let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: { + let controller = PremiumIntroScreen(context: context, source: .animatedEmoji) replaceImpl?(controller) }) replaceImpl = { [weak controller] c in diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/Contents.json new file mode 100644 index 0000000000..adf6728753 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "forumarrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/forumarrow.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/forumarrow.pdf new file mode 100644 index 0000000000..59b91d97df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/forumarrow.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.000000 -0.350586 cm +0.000000 0.000000 0.000000 scn +0.468521 11.725403 m +0.261516 11.984160 -0.116060 12.026113 -0.374817 11.819107 c +-0.633574 11.612102 -0.675527 11.234526 -0.468521 10.975769 c +0.468521 11.725403 l +h +4.000000 6.350586 m +4.468521 5.975769 l +4.643826 6.194900 4.643826 6.506272 4.468521 6.725403 c +4.000000 6.350586 l +h +-0.468521 1.725403 m +-0.675527 1.466646 -0.633574 1.089070 -0.374817 0.882065 c +-0.116060 0.675059 0.261516 0.717011 0.468521 0.975769 c +-0.468521 1.725403 l +h +-0.468521 10.975769 m +3.531479 5.975769 l +4.468521 6.725403 l +0.468521 11.725403 l +-0.468521 10.975769 l +h +3.531479 6.725403 m +-0.468521 1.725403 l +0.468521 0.975769 l +4.468521 5.975769 l +3.531479 6.725403 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 781 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 6.000000 12.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000871 00000 n +0000000893 00000 n +0000001065 00000 n +0000001139 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1198 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 2e5c8cd99e..6e22927650 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -324,7 +324,7 @@ final class AuthorizedApplicationContext { var chatIsVisible = false if let topController = strongSelf.rootController.topViewController as? ChatControllerImpl, topController.traceVisibility() { - if topController.chatLocation.peerId == firstMessage.id.peerId { + if topController.chatLocation.peerId == firstMessage.id.peerId, (topController.chatLocation.threadId == nil || topController.chatLocation.threadId == firstMessage.threadId) { chatIsVisible = true } } @@ -335,7 +335,7 @@ final class AuthorizedApplicationContext { if !chatIsVisible { strongSelf.mainWindow.forEachViewController({ controller in - if let controller = controller as? ChatControllerImpl, controller.chatLocation.peerId == chatLocation.peerId, controller.chatLocation.threadId == chatLocation.threadId { + if let controller = controller as? ChatControllerImpl, controller.chatLocation.peerId == chatLocation.peerId, (chatLocation.threadId == nil || chatLocation.threadId == controller.chatLocation.threadId) { chatIsVisible = true return false } @@ -415,14 +415,14 @@ final class AuthorizedApplicationContext { return true } - if let topController = strongSelf.rootController.topViewController as? ChatControllerImpl, topController.chatLocation.peerId == chatLocation.peerId, topController.chatLocation.threadId == chatLocation.threadId { + if let topController = strongSelf.rootController.topViewController as? ChatControllerImpl, topController.chatLocation.peerId == chatLocation.peerId, (topController.chatLocation.threadId == nil || topController.chatLocation.threadId == chatLocation.threadId) { strongSelf.notificationController.removeItemsWithGroupingKey(firstMessage.id.peerId) return false } for controller in strongSelf.rootController.viewControllers { - if let controller = controller as? ChatControllerImpl, controller.chatLocation.peerId == chatLocation.peerId, controller.chatLocation.threadId == chatLocation.threadId { + if let controller = controller as? ChatControllerImpl, controller.chatLocation.peerId == chatLocation.peerId, (controller.chatLocation.threadId == nil || controller.chatLocation.threadId == chatLocation.threadId) { return true } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ee9a545b31..f775a01ac9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -265,6 +265,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var moreInfoNavigationButton: ChatNavigationButton? private var peerView: PeerView? + private var threadInfo: EngineMessageHistoryThread.Info? private var historyStateDisposable: Disposable? @@ -520,6 +521,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var inviteRequestsContext: PeerInvitationImportersContext? private var inviteRequestsDisposable = MetaDisposable() + private var overlayTitle: String? { + var title: String? + if let threadInfo = self.threadInfo { + title = threadInfo.title + } else if let peerView = self.peerView { + if let peer = peerViewMainPeer(peerView) { + title = EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) + } + } + return title + } + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [PeerId] = []) { let _ = ChatControllerCount.modify { value in return value + 1 @@ -3018,6 +3031,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation, replyThreadMessage.messageId == message.id { return .none } + if case .peer = strongSelf.chatLocation, let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { + if message.threadId == nil { + return .none + } + } if canReplyInChat(strongSelf.presentationInterfaceState) { return .reply @@ -3038,7 +3056,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } case let .replyThread(replyThreadMessage): let peerId = replyThreadMessage.messageId.peerId - strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, animated: true, completion: nil) + strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, forceInCurrentChat: true, animated: true, completion: nil) case .feed: //TODO:implement break @@ -4410,10 +4428,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })) - self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, hasScheduledMessages, self.reportIrrelvantGeoNoticePromise.get(), displayedCountSignal) - |> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount in + let threadInfo: Signal + if let threadId = self.chatLocation.threadId { + let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: threadId) + threadInfo = context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> EngineMessageHistoryThread.Info? in + guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else { + return nil + } + guard let data = view.info?.data.get(MessageHistoryThreadData.self) else { + return nil + } + return data.info + } + |> distinctUntilChanged + } else { + threadInfo = .single(nil) + } + + self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, hasScheduledMessages, self.reportIrrelvantGeoNoticePromise.get(), displayedCountSignal, threadInfo) + |> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo in if let strongSelf = self { - if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages { + if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo { return } @@ -4456,6 +4492,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let firstTime = strongSelf.peerView == nil strongSelf.peerView = peerView + strongSelf.threadInfo = threadInfo if wasGroupChannel != isGroupChannel { if let isGroupChannel = isGroupChannel, isGroupChannel { let (recentDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.recent(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) @@ -4469,7 +4506,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } if strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.peerView = peerView + strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle } var peerIsMuted = false if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { @@ -4843,9 +4880,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let firstTime = strongSelf.peerView == nil strongSelf.peerView = peerView + strongSelf.threadInfo = messageAndTopic.threadData?.info if strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.peerView = peerView + strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle } var peerDiscussionId: PeerId? @@ -5567,9 +5605,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func topPinnedMessageSignal(latest: Bool) -> Signal { - let topPinnedMessage: Signal + var pinnedPeerId: EnginePeer.Id? + let threadId = self.chatLocation.threadId + switch self.chatLocation { - case let .peer(peerId): + case let .peer(id): + pinnedPeerId = id + case let .replyThread(message): + if message.isForumPost { + pinnedPeerId = self.chatLocation.peerId + } + default: + break + } + + if let peerId = pinnedPeerId { + let topPinnedMessage: Signal + struct ReferenceMessage { var id: MessageId var isScrolled: Bool @@ -5613,7 +5665,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G location = .Initial(count: count) } - return (chatHistoryViewForLocation(ChatHistoryLocationInput(content: location, id: 0), ignoreMessagesInTimestampRange: nil, context: context, chatLocation: .peer(id: peerId), chatLocationContextHolder: Atomic(value: nil), scheduled: false, fixedCombinedReadStates: nil, tagMask: MessageTags.pinned, appendMessagesFromTheSameGroup: false, additionalData: [], orderStatistics: .combinedLocation) + let chatLocation: ChatLocation + if let threadId { + chatLocation = .replyThread(message: ChatReplyThreadMessage(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + } else { + chatLocation = .peer(id: peerId) + } + + return (chatHistoryViewForLocation(ChatHistoryLocationInput(content: location, id: 0), ignoreMessagesInTimestampRange: nil, context: context, chatLocation: chatLocation, chatLocationContextHolder: Atomic(value: nil), scheduled: false, fixedCombinedReadStates: nil, tagMask: MessageTags.pinned, appendMessagesFromTheSameGroup: false, additionalData: [], orderStatistics: .combinedLocation) |> castError(Bool.self) |> mapToSignal { update -> Signal in switch update { @@ -5833,10 +5892,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return message } |> distinctUntilChanged - case .replyThread, .feed: + + return topPinnedMessage + } else { return .single(nil) } - return topPinnedMessage } override public func loadDisplayNode() { @@ -5902,7 +5962,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - self.chatDisplayNode.peerView = self.peerView + self.chatDisplayNode.overlayTitle = self.overlayTitle let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) |> map { peer in @@ -6243,7 +6303,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch strongSelf.chatLocation { case let .replyThread(replyThreadMessage): if isForum { - pinnedMessageId = nil + pinnedMessageId = topPinnedMessage?.message.id + pinnedMessage = topPinnedMessage } else { if isTopReplyThreadMessageShown { pinnedMessageId = nil @@ -8327,7 +8388,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, unblockPeer: { [weak self] in self?.unblockPeer() }, pinMessage: { [weak self] messageId, contextController in - if let strongSelf = self, case let .peer(currentPeerId) = strongSelf.chatLocation { + if let strongSelf = self, let currentPeerId = strongSelf.chatLocation.peerId { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { if strongSelf.canManagePin() { let pinAction: (Bool, Bool) -> Void = { notify, forThisPeerOnlyIfPossible in diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index bf8d93bd69..edae928325 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -79,9 +79,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var containerNode: ASDisplayNode? private var overlayNavigationBar: ChatOverlayNavigationBar? - var peerView: PeerView? { + var overlayTitle: String? { didSet { - self.overlayNavigationBar?.peerView = self.peerView + self.overlayNavigationBar?.title = self.overlayTitle } } @@ -246,7 +246,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { loadingPlaceholderNode = ChatLoadingPlaceholderNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners, backgroundNode: self.backgroundNode) loadingPlaceholderNode.updatePresentationInterfaceState(self.chatPresentationInterfaceState) - self.contentContainerNode.insertSubnode(loadingPlaceholderNode, aboveSubnode: self.backgroundNode) + self.backgroundNode.supernode?.insertSubnode(loadingPlaceholderNode, aboveSubnode: self.backgroundNode) self.loadingPlaceholderNode = loadingPlaceholderNode @@ -980,7 +980,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { }, close: { [weak self] in self?.dismissAsOverlay() }) - overlayNavigationBar.peerView = self.peerView + overlayNavigationBar.title = self.overlayTitle self.overlayNavigationBar = overlayNavigationBar self.containerNode?.addSubnode(overlayNavigationBar) } @@ -1064,13 +1064,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var extraTransition = transition if let titleAccessoryPanelNode = titlePanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.titleAccessoryPanelNode, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction) { if self.titleAccessoryPanelNode != titleAccessoryPanelNode { - dismissedTitleAccessoryPanelNode = self.titleAccessoryPanelNode + dismissedTitleAccessoryPanelNode = self.titleAccessoryPanelNode self.titleAccessoryPanelNode = titleAccessoryPanelNode immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance = true self.titleAccessoryPanelContainer.addSubnode(titleAccessoryPanelNode) titleAccessoryPanelNode.clipsToBounds = true - extraTransition = .animated(duration: 0.2, curve: .easeInOut) + if transition.isAnimated { + extraTransition = .animated(duration: 0.2, curve: .easeInOut) + } } let layoutResult = titleAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 39b48591c4..2df95d5687 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -25,7 +25,7 @@ func chatHistoryEntriesForView( customChannelDiscussionReadState: MessageId?, customThreadOutgoingReadState: MessageId?, cachedData: CachedPeerData?, - adMessages: [Message] + adMessages: (interPostInterval: Int32?, messages: [Message]) ) -> [ChatHistoryEntry] { if historyAppearsCleared { return [] @@ -329,9 +329,9 @@ func chatHistoryEntriesForView( } if view.laterId == nil && !view.isLoading { - if !entries.isEmpty, case let .MessageEntry(lastMessage, _, _, _, _, _) = entries[entries.count - 1], !adMessages.isEmpty { + if !entries.isEmpty, case let .MessageEntry(lastMessage, _, _, _, _, _) = entries[entries.count - 1], !adMessages.messages.isEmpty { var nextAdMessageId: Int32 = 1 - for message in adMessages { + if let message = adMessages.messages.first { let updatedMessage = Message( stableId: UInt32.max - 1 - UInt32(nextAdMessageId), stableVersion: message.stableVersion, diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 198fe473b6..61596d917d 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -641,14 +641,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { default: break } - var adMessages: Signal<[Message], NoError> + var adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> if case .bubbles = mode, let peerId = displayAdPeer { let adMessagesContext = context.engine.messages.adMessages(peerId: peerId) self.adMessagesContext = adMessagesContext adMessages = adMessagesContext.state } else { self.adMessagesContext = nil - adMessages = .single([]) + adMessages = .single((nil, [])) } /*if case .bubbles = mode, let peerId = sparseScrollPeerId { @@ -664,7 +664,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { super.init() adMessages = adMessages - |> afterNext { [weak self] messages in + |> afterNext { [weak self] interPostInterval, messages in Queue.mainQueue().async { guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index cdcd4e4a5b..c227ba7d60 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -94,6 +94,10 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData) if preloaded { + if tagMask == nil && view.entries.isEmpty { + print("") + } + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, flashIndicators: false, originalScrollPosition: nil, initialData: combinedInitialData, id: location.id) } else { if view.isLoading { @@ -102,7 +106,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess var scrollPosition: ChatHistoryViewScrollPosition? let canScrollToRead: Bool - if case .replyThread = chatLocation { + if case let .replyThread(message) = chatLocation, !message.isForumPost { canScrollToRead = true } else if view.isAddedToChatList { canScrollToRead = true @@ -114,7 +118,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess let aroundIndex = maxReadIndex scrollPosition = .unread(index: maxReadIndex) - if case .peer = chatLocation { + if let _ = chatLocation.peerId { var targetIndex = 0 for i in 0 ..< view.entries.count { if view.entries[i].index >= aroundIndex { @@ -152,7 +156,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess } else if view.isAddedToChatList, tagMask == nil, let historyScrollState = (initialData?.storedInterfaceState).flatMap(_internal_decodeStoredChatInterfaceState).flatMap(ChatInterfaceState.parse)?.historyScrollState { scrollPosition = .positionRestoration(index: historyScrollState.messageIndex, relativeOffset: CGFloat(historyScrollState.relativeOffset)) } else { - if case .peer = chatLocation, !view.isAddedToChatList { + if let _ = chatLocation.peerId, !view.isAddedToChatList { if view.holeEarlier && view.entries.count <= 2 { fadeIn = true return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) @@ -164,6 +168,10 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess } } + if tagMask == nil && view.entries.isEmpty { + print("") + } + preloaded = true return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index b9aa0152da..4b1b624900 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -262,7 +262,9 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS if let threadData = chatPresentationInterfaceState.threadData { if threadData.isClosed { var canManage = false - if channel.hasPermission(.pinMessages) { + if channel.flags.contains(.isCreator) { + canManage = true + } else if channel.adminRights != nil { canManage = true } else if threadData.isOwn { canManage = true @@ -523,6 +525,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let message = messages[0] + if case .peer = chatPresentationInterfaceState.chatLocation, let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { + if message.threadId == nil { + canReply = false + } + } + if Namespaces.Message.allScheduled.contains(message.id.namespace) || message.id.peerId.isReplies { canReply = false canPin = false @@ -1248,7 +1256,17 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - if data.canPin && !isMigrated, case .peer = chatPresentationInterfaceState.chatLocation { + var canPin = data.canPin + if case let .replyThread(message) = chatPresentationInterfaceState.chatLocation { + if !message.isForumPost { + canPin = false + } + } + if isMigrated { + canPin = false + } + + if canPin { var pinnedSelectedMessageId: MessageId? for message in messages { if message.tags.contains(.pinned) { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 4b2e0628ad..40806f2152 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -162,7 +162,9 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if let threadData = chatPresentationInterfaceState.threadData { if threadData.isClosed { var canManage = false - if channel.hasPermission(.pinMessages) { + if channel.flags.contains(.isCreator) { + canManage = true + } else if channel.adminRights != nil { canManage = true } else if threadData.isOwn { canManage = true @@ -179,6 +181,17 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } } + } else { + if chatPresentationInterfaceState.interfaceState.replyMessageId == nil { + if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + return (currentPanel, nil) + } else { + let panel = ChatRestrictedInputPanelNode() + panel.context = context + panel.interfaceInteraction = interfaceInteraction + return (panel, nil) + } + } } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index 20cbf824ef..fd66a5fd07 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -61,7 +61,9 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat if let threadData = chatPresentationInterfaceState.threadData { if threadData.isClosed { var canManage = false - if channel.hasPermission(.pinMessages) { + if channel.flags.contains(.isCreator) { + canManage = true + } else if channel.adminRights != nil { canManage = true } else if threadData.isOwn { canManage = true diff --git a/submodules/TelegramUI/Sources/ChatOverlayNavigationBar.swift b/submodules/TelegramUI/Sources/ChatOverlayNavigationBar.swift index 983e777a69..4aa1bf13af 100644 --- a/submodules/TelegramUI/Sources/ChatOverlayNavigationBar.swift +++ b/submodules/TelegramUI/Sources/ChatOverlayNavigationBar.swift @@ -22,15 +22,10 @@ final class ChatOverlayNavigationBar: ASDisplayNode { private var validLayout: CGSize? - private var peerTitle = "" - var peerView: PeerView? { + private var peerTitle: String = "" + var title: String? { didSet { - var title = "" - if let peerView = self.peerView { - if let peer = peerViewMainPeer(peerView) { - title = EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder) - } - } + let title = self.title ?? "" if self.peerTitle != title { self.peerTitle = title if let size = self.validLayout { diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index f79dc939f1..5bf860e352 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -102,7 +102,9 @@ private func peerButtons(_ state: ChatPresentationInterfaceState) -> [ChatReport if let threadData = state.threadData { if threadData.isClosed { var canManage = false - if channel.hasPermission(.pinMessages) { + if channel.flags.contains(.isCreator) { + canManage = true + } else if channel.adminRights != nil { canManage = true } else if threadData.isOwn { canManage = true @@ -597,7 +599,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { if let renderedPeer = interfaceState.renderedPeer { chatPeer = renderedPeer.peers[renderedPeer.peerId] } - if let chatPeer = chatPeer, let invitedBy = interfaceState.contactStatus?.invitedBy { + if let chatPeer = chatPeer, (updatedButtons.contains(.block) || updatedButtons.contains(.reportSpam) || updatedButtons.contains(.reportUserSpam)), let invitedBy = interfaceState.contactStatus?.invitedBy { var inviteInfoTransition = transition let inviteInfoNode: ChatInfoTitlePanelInviteInfoNode if let current = self.inviteInfoNode { diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 6a764539db..0709a95b20 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -49,6 +49,8 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { //TODO:localize iconImage = PresentationResourcesChat.chatPanelLockIcon(interfaceState.theme) self.textNode.attributedText = NSAttributedString(string: "The topic is closed by admin", font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) + } else if let channel = interfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum), case .peer = interfaceState.chatLocation { + self.textNode.attributedText = NSAttributedString(string: "Swipe left on a message to reply", font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } else if let (untilDate, personal) = bannedPermission { if personal && untilDate != 0 && untilDate != Int32.max { self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RestrictedTextTimed(stringForFullDate(timestamp: untilDate, strings: interfaceState.strings, dateTimeFormat: interfaceState.dateTimeFormat)).string, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index a169440598..72de6b8718 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -1189,7 +1189,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro return result } -func peerInfoCanEdit(peer: Peer?, cachedData: CachedPeerData?, isContact: Bool?) -> Bool { +func peerInfoCanEdit(peer: Peer?, threadData: MessageHistoryThreadData?, cachedData: CachedPeerData?, isContact: Bool?) -> Bool { if let user = peer as? TelegramUser { if user.isDeleted { return false @@ -1199,14 +1199,26 @@ func peerInfoCanEdit(peer: Peer?, cachedData: CachedPeerData?, isContact: Bool?) } return true } else if let peer = peer as? TelegramChannel { - if peer.flags.contains(.isCreator) { - return true - } else if peer.hasPermission(.changeInfo) { - return true - } else if let _ = peer.adminRights { - return true + if peer.flags.contains(.isForum) { + if peer.flags.contains(.isCreator) { + return true + } else if let threadData = threadData, threadData.isOwnedByMe { + return true + } else if let _ = peer.adminRights { + return true + } else { + return false + } + } else { + if peer.flags.contains(.isCreator) { + return true + } else if peer.hasPermission(.changeInfo) { + return true + } else if let _ = peer.adminRights { + return true + } + return false } - return false } else if let peer = peer as? TelegramGroup { if case .creator = peer.role { return true diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 006fc13be3..400e137ca2 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -8197,7 +8197,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .qrCode, isForExpandedView: false)) rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false)) rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .search, isForExpandedView: true)) - } else if peerInfoCanEdit(peer: self.data?.peer, cachedData: self.data?.cachedData, isContact: self.data?.isContact) { + } else if peerInfoCanEdit(peer: self.data?.peer, threadData: self.data?.threadData, cachedData: self.data?.cachedData, isContact: self.data?.isContact) { rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false)) } if self.state.selectedMessageIds == nil { @@ -8259,7 +8259,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate isLandscape = true } if offsetY <= -32.0 && scrollView.isDragging && scrollView.isTracking { - if let peer = self.data?.peer, peer.smallProfileImage != nil && self.state.updatingAvatar == nil && !isLandscape { + if let peer = self.data?.peer, self.chatLocation.threadId == nil, peer.smallProfileImage != nil && self.state.updatingAvatar == nil && !isLandscape { shouldBeExpanded = true if self.canOpenAvatarByDragging && self.headerNode.isAvatarExpanded && offsetY <= -32.0 { @@ -9616,6 +9616,31 @@ func presentAddMembersImpl(context: AccountContext, updatedPresentationData: (in contactsController?.dismiss() }, completed: { contactsController?.dismiss() + + let mappedPeerIds: [EnginePeer.Id] = peers.compactMap { peer -> EnginePeer.Id? in + switch peer { + case let .peer(id): + return id + default: + return nil + } + } + if !mappedPeerIds.isEmpty { + let _ = (context.engine.data.get(EngineDataMap(mappedPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))) + |> deliverOnMainQueue).start(next: { maybePeers in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let peers = maybePeers.compactMap { $0.value } + + //TODO:localize + let text: String + if peers.count == 1 { + text = "**\(peers[0].displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))** added to the group." + } else { + text = "**\(peers.count)** members added to the group." + } + parentController?.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: context, peers: peers, title: nil, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }) + } })) })) contactsController.dismissed = { diff --git a/submodules/UndoUI/BUILD b/submodules/UndoUI/BUILD index 35e2b4eeba..311179b505 100644 --- a/submodules/UndoUI/BUILD +++ b/submodules/UndoUI/BUILD @@ -27,6 +27,7 @@ swift_library( "//submodules/AvatarNode:AvatarNode", "//submodules/AccountContext:AccountContext", "//submodules/ComponentFlow:ComponentFlow", + "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", ], visibility = [ "//visibility:public", diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index eae12ddccf..ed961f9b17 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -42,6 +42,7 @@ public enum UndoOverlayContent { case image(image: UIImage, text: String) case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String, customUndoText: String?) + case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?) } public enum UndoOverlayAction { diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 60b0490bf2..eeb3f9ddd0 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -17,6 +17,7 @@ import AnimationUI import StickerResources import AvatarNode import AccountContext +import AnimatedAvatarSetNode final class UndoOverlayControllerNode: ViewControllerTracingNode { private let elevatedLayout: Bool @@ -25,6 +26,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private let timerTextNode: ImmediateTextNode private let avatarNode: AvatarNode? private let iconNode: ASImageNode? + private var multiAvatarsNode: AnimatedAvatarSetNode? + private var multiAvatarsSize: CGSize? private var iconImageSize: CGSize? private let iconCheckNode: RadialStatusNode? private let animationNode: AnimationNode? @@ -872,6 +875,44 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) displayUndo = true self.originalRemainingSeconds = 5 + case let .peers(context, peers, title, text, customUndoText): + self.avatarNode = nil + let multiAvatarsNode = AnimatedAvatarSetNode() + self.multiAvatarsNode = multiAvatarsNode + let avatarsContext = AnimatedAvatarSetContext() + self.multiAvatarsSize = multiAvatarsNode.update(context: context, content: avatarsContext.update(peers: peers, animated: false), itemSize: CGSize(width: 28.0, height: 28.0), animated: false, synchronousLoad: false) + + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = nil + self.animatedStickerNode = nil + if let title = title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } else { + self.titleNode.attributedText = nil + } + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) + self.textNode.attributedText = attributedText + + if text.contains("](") { + isUserInteractionEnabled = true + } + self.originalRemainingSeconds = isUserInteractionEnabled ? 5 : 3 + + self.textNode.maximumNumberOfLines = 5 + + if let customUndoText = customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } } self.remainingSeconds = self.originalRemainingSeconds @@ -900,7 +941,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { switch content { case .removedChat: self.panelWrapperNode.addSubnode(self.timerTextNode) - case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal: + case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .peers: if self.textNode.tapAttributeAction != nil || displayUndo { self.isUserInteractionEnabled = true } else { @@ -927,6 +968,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.animationNode?.isUserInteractionEnabled = false self.iconCheckNode?.isUserInteractionEnabled = false self.avatarNode?.isUserInteractionEnabled = false + self.multiAvatarsNode?.isUserInteractionEnabled = false self.slotMachineNode?.isUserInteractionEnabled = false self.animatedStickerNode?.isUserInteractionEnabled = false @@ -938,6 +980,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.animatedStickerNode.flatMap(self.panelWrapperNode.addSubnode) self.slotMachineNode.flatMap(self.panelWrapperNode.addSubnode) self.avatarNode.flatMap(self.panelWrapperNode.addSubnode) + self.multiAvatarsNode.flatMap(self.panelWrapperNode.addSubnode) self.panelWrapperNode.addSubnode(self.buttonNode) self.panelWrapperNode.addSubnode(self.titleNode) self.panelWrapperNode.addSubnode(self.textNode) @@ -1088,7 +1131,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { if iconSize.width > leftInset { leftInset = iconSize.width - 8.0 } + } else if let multiAvatarsSize = self.multiAvatarsSize { + leftInset = 13.0 + multiAvatarsSize.width + 20.0 } + let rightInset: CGFloat = 16.0 var contentHeight: CGFloat = 20.0 @@ -1228,6 +1274,11 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let avatarSize: CGFloat = 30.0 transition.updateFrame(node: avatarNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - avatarSize) / 2.0), y: floor((contentHeight - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))) } + + if let multiAvatarsNode = self.multiAvatarsNode, let multiAvatarsSize = self.multiAvatarsSize { + let avatarsFrame = CGRect(origin: CGPoint(x: 13.0, y: floor((contentHeight - multiAvatarsSize.height) / 2.0) + verticalOffset), size: multiAvatarsSize) + transition.updateFrame(node: multiAvatarsNode, frame: avatarsFrame) + } } func animateIn(asReplacement: Bool) { diff --git a/versions.json b/versions.json index a2ca02c677..137a9ecf35 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "9.0.1", + "app": "9.1.0", "bazel": "5.3.1", "xcode": "14.0" }