mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
520f7e0cba
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -95,6 +95,7 @@ public enum PresentationResourceKey: Int32 {
|
||||
case chatListFakeServiceIcon
|
||||
case chatListSecretIcon
|
||||
case chatListStatusLockIcon
|
||||
case chatListTopicArrowIcon
|
||||
case chatListRecentStatusOnlineIcon
|
||||
case chatListRecentStatusOnlineHighlightedIcon
|
||||
case chatListRecentStatusOnlinePinnedIcon
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -31,6 +31,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/Components/PagerComponent:PagerComponent",
|
||||
"//submodules/PremiumUI",
|
||||
"//submodules/ProgressNavigationButtonNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "forumarrow.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
92
submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/forumarrow.pdf
vendored
Normal file
92
submodules/TelegramUI/Images.xcassets/Chat List/TopicArrowIcon.imageset/forumarrow.pdf
vendored
Normal 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
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
|
@ -27,6 +27,7 @@ swift_library(
|
||||
"//submodules/AvatarNode:AvatarNode",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"app": "9.0.1",
|
||||
"app": "9.1.0",
|
||||
"bazel": "5.3.1",
|
||||
"xcode": "14.0"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user