Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2022-10-22 00:28:54 +03:00
commit 520f7e0cba
52 changed files with 1103 additions and 276 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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<Empty>?
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<Empty>
if let current = self.titleTopicIconView {
titleTopicIconView = current
} else {
titleTopicIconView = ComponentHostView<Empty>()
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 {

View File

@ -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

View File

@ -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<ChatListNodePeerInputActivities?>(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 {

View File

@ -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,

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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<ForumTopicListHolesEntry>()
func update(holes: Set<ForumTopicListHolesEntry>) -> Bool {
if self.entries != holes {
self.entries = holes
return true
} else {
return false
}
}
}
public final class ForumTopicListHolesView {
public let entries: Set<ForumTopicListHolesEntry>
init(_ mutableView: MutableForumTopicListHolesView) {
self.entries = mutableView.entries
}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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"

View File

@ -3281,6 +3281,18 @@ final class PostboxImpl {
}
}
public func forumTopicListHolesView() -> Signal<ForumTopicListHolesView, NoError> {
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<UnsentMessageIdsView, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
@ -4195,6 +4207,18 @@ public class Postbox {
return disposable
}
}
public func forumTopicListHolesView() -> Signal<ForumTopicListHolesView, NoError> {
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<UnsentMessageIdsView, NoError> {
return Signal { subscriber in

View File

@ -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

View File

@ -25,6 +25,9 @@ final class ViewTracker {
private let chatListHolesView = MutableChatListHolesView()
private let chatListHolesViewSubscribers = Bag<ValuePipe<ChatListHolesView>>()
private let forumTopicListHolesView = MutableForumTopicListHolesView()
private let forumTopicListHolesViewSubscribers = Bag<ValuePipe<ForumTopicListHolesView>>()
private var unsentMessageView: UnsentMessageHistoryView
private let unsendMessageIdsViewSubscribers = Bag<ValuePipe<UnsentMessageIdsView>>()
@ -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<ForumTopicListHolesEntry>()
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<MessageHistoryHolesViewEntry>()
for (view, _) in self.messageHistoryViews.copyItems() {
@ -506,6 +531,30 @@ final class ViewTracker {
}
}
func forumTopicListHolesViewSignal() -> Signal<ForumTopicListHolesView, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.queue.async {
subscriber.putNext(ForumTopicListHolesView(self.forumTopicListHolesView))
let pipe = ValuePipe<ForumTopicListHolesView>()
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<UnsentMessageIdsView, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()

View File

@ -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<Never, LoadMessageHistoryThreadsError> {
let signal: Signal<Never, LoadMessageHistoryThreadsError> = account.postbox.transaction { transaction -> Api.InputChannel? in
func _internal_loadMessageHistoryThreads(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId, offsetIndex: StoredPeerThreadCombinedState.Index?, limit: Int) -> Signal<Never, LoadMessageHistoryThreadsError> {
let signal: Signal<Never, LoadMessageHistoryThreadsError> = 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<Never, LoadMessageHistoryThreadsError> = 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<Never, LoadMessageHistoryThreadsError> = 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<Never, LoadMessageHistoryThreadsError> 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())
}

View File

@ -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
}
}

View File

@ -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<Void, NoError> {
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()
}
}
}
}

View File

@ -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())

View File

@ -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<State, NoError> 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<Api.messages.SponsoredMessages?, NoError> 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<AdMessagesHistoryContextImpl>
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)
}

View File

@ -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)

View File

@ -95,6 +95,7 @@ public enum PresentationResourceKey: Int32 {
case chatListFakeServiceIcon
case chatListSecretIcon
case chatListStatusLockIcon
case chatListTopicArrowIcon
case chatListRecentStatusOnlineIcon
case chatListRecentStatusOnlineHighlightedIcon
case chatListRecentStatusOnlinePinnedIcon

View File

@ -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)
})
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -31,6 +31,7 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/Components/PagerComponent:PagerComponent",
"//submodules/PremiumUI",
"//submodules/ProgressNavigationButtonNode",
],
visibility = [
"//visibility:public",

View File

@ -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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "forumarrow.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(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<EngineMessageHistoryThread.Info?, NoError>
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<ChatPinnedMessage?, NoError> {
let topPinnedMessage: Signal<ChatPinnedMessage?, NoError>
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<ChatPinnedMessage?, NoError>
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<ChatLocationContextHolder?>(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<ChatLocationContextHolder?>(value: nil), scheduled: false, fixedCombinedReadStates: nil, tagMask: MessageTags.pinned, appendMessagesFromTheSameGroup: false, additionalData: [], orderStatistics: .combinedLocation)
|> castError(Bool.self)
|> mapToSignal { update -> Signal<ChatHistoryViewUpdate, Bool> 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

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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)
}

View File

@ -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) {

View File

@ -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)
}
}
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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 = {

View File

@ -27,6 +27,7 @@ swift_library(
"//submodules/AvatarNode:AvatarNode",
"//submodules/AccountContext:AccountContext",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
],
visibility = [
"//visibility:public",

View File

@ -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 {

View File

@ -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) {

View File

@ -1,5 +1,5 @@
{
"app": "9.0.1",
"app": "9.1.0",
"bazel": "5.3.1",
"xcode": "14.0"
}