mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-31 09:52:04 +00:00
[WIP] Topics
This commit is contained in:
parent
94d264900f
commit
f3caab5096
@ -507,6 +507,157 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
|
|||||||
private let maxVideoLoopCount = 3
|
private let maxVideoLoopCount = 3
|
||||||
|
|
||||||
class ChatListItemNode: ItemListRevealOptionsItemNode {
|
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?
|
var item: ChatListItem?
|
||||||
|
|
||||||
private let backgroundNode: ASDisplayNode
|
private let backgroundNode: ASDisplayNode
|
||||||
@ -523,7 +674,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
private var videoLoopCount = 0
|
private var videoLoopCount = 0
|
||||||
|
|
||||||
let titleNode: TextNode
|
let titleNode: TextNode
|
||||||
let authorNode: TextNode
|
let authorNode: AuthorNode
|
||||||
let measureNode: TextNode
|
let measureNode: TextNode
|
||||||
private var currentItemHeight: CGFloat?
|
private var currentItemHeight: CGFloat?
|
||||||
let textNode: TextNodeWithEntities
|
let textNode: TextNodeWithEntities
|
||||||
@ -731,6 +882,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
containerSize: avatarIconView.bounds.size
|
containerSize: avatarIconView.bounds.size
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
self.authorNode.visibilityStatus = self.visibilityStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -766,9 +918,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
self.titleNode.isUserInteractionEnabled = false
|
self.titleNode.isUserInteractionEnabled = false
|
||||||
self.titleNode.displaysAsynchronously = true
|
self.titleNode.displaysAsynchronously = true
|
||||||
|
|
||||||
self.authorNode = TextNode()
|
self.authorNode = AuthorNode()
|
||||||
self.authorNode.isUserInteractionEnabled = false
|
self.authorNode.isUserInteractionEnabled = false
|
||||||
self.authorNode.displaysAsynchronously = true
|
|
||||||
|
|
||||||
self.textNode = TextNodeWithEntities()
|
self.textNode = TextNodeWithEntities()
|
||||||
self.textNode.textNode.isUserInteractionEnabled = false
|
self.textNode.textNode.isUserInteractionEnabled = false
|
||||||
@ -1045,7 +1196,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
let dateLayout = TextNode.asyncLayout(self.dateNode)
|
let dateLayout = TextNode.asyncLayout(self.dateNode)
|
||||||
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||||
let titleLayout = TextNode.asyncLayout(self.titleNode)
|
let titleLayout = TextNode.asyncLayout(self.titleNode)
|
||||||
let authorLayout = TextNode.asyncLayout(self.authorNode)
|
let authorLayout = self.authorNode.asyncLayout()
|
||||||
let makeMeasureLayout = TextNode.asyncLayout(self.measureNode)
|
let makeMeasureLayout = TextNode.asyncLayout(self.measureNode)
|
||||||
let inputActivitiesLayout = self.inputActivitiesNode.asyncLayout()
|
let inputActivitiesLayout = self.inputActivitiesNode.asyncLayout()
|
||||||
let badgeLayout = self.badgeNode.asyncLayout()
|
let badgeLayout = self.badgeNode.asyncLayout()
|
||||||
@ -1286,6 +1437,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
let contentImageSpacing: CGFloat = 2.0
|
let contentImageSpacing: CGFloat = 2.0
|
||||||
let contentImageTrailingSpace: CGFloat = 5.0
|
let contentImageTrailingSpace: CGFloat = 5.0
|
||||||
var contentImageSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = []
|
var contentImageSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = []
|
||||||
|
var forumThread: (title: String, iconId: Int64?, iconColor: Int32)?
|
||||||
|
|
||||||
switch contentData {
|
switch contentData {
|
||||||
case let .chat(itemPeer, _, _, _, text, spoilers, customEmojiRanges):
|
case let .chat(itemPeer, _, _, _, text, spoilers, customEmojiRanges):
|
||||||
@ -1310,14 +1462,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 {
|
if let forumTopicData = forumTopicData {
|
||||||
peerText = "\(peerTextValue) → \(forumTopicData.title)"
|
forumThread = (forumTopicData.title, forumTopicData.iconFileId, forumTopicData.iconColor)
|
||||||
} else if let threadInfo = threadInfo?.info {
|
} else if let threadInfo = threadInfo?.info {
|
||||||
peerText = "\(peerTextValue) → \(threadInfo.title)"
|
forumThread = (threadInfo.title, threadInfo.icon, threadInfo.iconColor)
|
||||||
} else {
|
|
||||||
//TODO:localize
|
|
||||||
peerText = "\(peerTextValue) → General"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1582,14 +1731,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
if hasFailedMessages {
|
if hasFailedMessages {
|
||||||
statusState = .failed(item.presentationData.theme.chatList.failedFillColor, item.presentationData.theme.chatList.failedForegroundColor)
|
statusState = .failed(item.presentationData.theme.chatList.failedFillColor, item.presentationData.theme.chatList.failedForegroundColor)
|
||||||
} else {
|
} else {
|
||||||
if case .chatList = item.chatListLocation {
|
if let forumTopicData = forumTopicData {
|
||||||
if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) {
|
if message.id.namespace == forumTopicData.maxOutgoingReadMessageId.namespace, message.id.id >= forumTopicData.maxOutgoingReadMessageId.id {
|
||||||
statusState = .read(item.presentationData.theme.chatList.checkmarkColor)
|
statusState = .read(item.presentationData.theme.chatList.checkmarkColor)
|
||||||
} else {
|
} else {
|
||||||
statusState = .delivered(item.presentationData.theme.chatList.checkmarkColor)
|
statusState = .delivered(item.presentationData.theme.chatList.checkmarkColor)
|
||||||
}
|
}
|
||||||
} else if case .forum = item.chatListLocation {
|
} else {
|
||||||
if let forumTopicData = forumTopicData, message.id.namespace == forumTopicData.maxOutgoingReadMessageId.namespace, message.id.id >= forumTopicData.maxOutgoingReadMessageId.id {
|
if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) {
|
||||||
statusState = .read(item.presentationData.theme.chatList.checkmarkColor)
|
statusState = .read(item.presentationData.theme.chatList.checkmarkColor)
|
||||||
} else {
|
} else {
|
||||||
statusState = .delivered(item.presentationData.theme.chatList.checkmarkColor)
|
statusState = .delivered(item.presentationData.theme.chatList.checkmarkColor)
|
||||||
@ -1776,7 +1925,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
badgeSize = max(badgeSize, reorderInset)
|
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?
|
var textCutout: TextNodeCutout?
|
||||||
if !textLeftCutout.isZero {
|
if !textLeftCutout.isZero {
|
||||||
@ -1869,7 +2025,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
if let threadInfo {
|
if let threadInfo {
|
||||||
isClosed = threadInfo.isClosed
|
isClosed = threadInfo.isClosed
|
||||||
}
|
}
|
||||||
peerRevealOptions = forumRevealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isMuted: (currentMutedIconImage != nil), isClosed: isClosed, isPinned: isPinned, isEditing: item.editing, canPin: channel.hasPermission(.pinMessages), 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 = []
|
peerLeftRevealOptions = []
|
||||||
} else {
|
} else {
|
||||||
peerRevealOptions = []
|
peerRevealOptions = []
|
||||||
@ -2209,9 +2365,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
let contentDelta = CGPoint(x: contentRect.origin.x - (strongSelf.titleNode.frame.minX - titleOffset), y: contentRect.origin.y - (strongSelf.titleNode.frame.minY - UIScreenPixel))
|
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)
|
let titleFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size)
|
||||||
strongSelf.titleNode.frame = titleFrame
|
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
|
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
|
strongSelf.textNode.textNode.frame = textNodeFrame
|
||||||
|
|
||||||
if !textLayout.spoilers.isEmpty {
|
if !textLayout.spoilers.isEmpty {
|
||||||
|
@ -232,7 +232,7 @@ func chatListViewForLocation(chatListLocation: ChatListControllerLocation, locat
|
|||||||
pinnedIndex = .none
|
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?
|
var draft: EngineChatList.Draft?
|
||||||
if let embeddedState = item.embeddedInterfaceState, let _ = embeddedState.overrideChatTimestamp {
|
if let embeddedState = item.embeddedInterfaceState, let _ = embeddedState.overrideChatTimestamp {
|
||||||
|
@ -204,6 +204,11 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV
|
|||||||
fromEmptyView = true
|
fromEmptyView = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let fromView = fromView, !fromView.isLoading, toView.isLoading {
|
||||||
|
options.remove(.AnimateInsertion)
|
||||||
|
options.remove(.AnimateAlpha)
|
||||||
|
}
|
||||||
|
|
||||||
var adjustScrollToFirstItem = false
|
var adjustScrollToFirstItem = false
|
||||||
if !previewing && !searchMode && fromEmptyView && scrollToItem == nil && toView.filteredEntries.count >= 2 {
|
if !previewing && !searchMode && fromEmptyView && scrollToItem == nil && toView.filteredEntries.count >= 2 {
|
||||||
adjustScrollToFirstItem = true
|
adjustScrollToFirstItem = true
|
||||||
|
@ -30,3 +30,35 @@ public final class ChatListHolesView {
|
|||||||
self.entries = mutableView.entries
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView {
|
|||||||
fileprivate let summaryComponents: ChatListEntrySummaryComponents
|
fileprivate let summaryComponents: ChatListEntrySummaryComponents
|
||||||
fileprivate var peer: Peer?
|
fileprivate var peer: Peer?
|
||||||
fileprivate var items: [Item] = []
|
fileprivate var items: [Item] = []
|
||||||
|
private var hole: ForumTopicListHolesEntry?
|
||||||
fileprivate var isLoading: Bool = false
|
fileprivate var isLoading: Bool = false
|
||||||
|
|
||||||
init(postbox: PostboxImpl, peerId: PeerId, summaryComponents: ChatListEntrySummaryComponents) {
|
init(postbox: PostboxImpl, peerId: PeerId, summaryComponents: ChatListEntrySummaryComponents) {
|
||||||
@ -50,6 +51,16 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView {
|
|||||||
let validIndexBoundary = postbox.peerThreadCombinedStateTable.get(peerId: peerId)?.validIndexBoundary
|
let validIndexBoundary = postbox.peerThreadCombinedStateTable.get(peerId: peerId)?.validIndexBoundary
|
||||||
self.isLoading = validIndexBoundary == nil
|
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 {
|
if !self.isLoading {
|
||||||
let pinnedThreadIds = postbox.messageHistoryThreadPinnedTable.get(peerId: self.peerId)
|
let pinnedThreadIds = postbox.messageHistoryThreadPinnedTable.get(peerId: self.peerId)
|
||||||
var nextPinnedIndex = 0
|
var nextPinnedIndex = 0
|
||||||
@ -125,8 +136,14 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView {
|
|||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func topHole() -> ForumTopicListHolesEntry? {
|
||||||
|
return self.hole
|
||||||
|
}
|
||||||
|
|
||||||
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
|
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
|
||||||
return false
|
self.reload(postbox: postbox)
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func immutableView() -> PostboxView {
|
func immutableView() -> PostboxView {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct StoredPeerThreadCombinedState: Equatable, Codable {
|
public struct StoredPeerThreadCombinedState: Equatable, Codable {
|
||||||
public struct Index: Equatable, Comparable, Codable {
|
public struct Index: Hashable, Comparable, Codable {
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case timestamp = "t"
|
case timestamp = "t"
|
||||||
case threadId = "i"
|
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> {
|
public func unsentMessageIdsView() -> Signal<UnsentMessageIdsView, NoError> {
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
@ -4196,6 +4208,18 @@ public class Postbox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
public func unsentMessageIdsView() -> Signal<UnsentMessageIdsView, NoError> {
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
|
@ -10,7 +10,7 @@ protocol MutablePostboxView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class CombinedMutableView {
|
final class CombinedMutableView {
|
||||||
private let views: [PostboxViewKey: MutablePostboxView]
|
let views: [PostboxViewKey: MutablePostboxView]
|
||||||
|
|
||||||
init(views: [PostboxViewKey: MutablePostboxView]) {
|
init(views: [PostboxViewKey: MutablePostboxView]) {
|
||||||
self.views = views
|
self.views = views
|
||||||
|
@ -25,6 +25,9 @@ final class ViewTracker {
|
|||||||
private let chatListHolesView = MutableChatListHolesView()
|
private let chatListHolesView = MutableChatListHolesView()
|
||||||
private let chatListHolesViewSubscribers = Bag<ValuePipe<ChatListHolesView>>()
|
private let chatListHolesViewSubscribers = Bag<ValuePipe<ChatListHolesView>>()
|
||||||
|
|
||||||
|
private let forumTopicListHolesView = MutableForumTopicListHolesView()
|
||||||
|
private let forumTopicListHolesViewSubscribers = Bag<ValuePipe<ForumTopicListHolesView>>()
|
||||||
|
|
||||||
private var unsentMessageView: UnsentMessageHistoryView
|
private var unsentMessageView: UnsentMessageHistoryView
|
||||||
private let unsendMessageIdsViewSubscribers = Bag<ValuePipe<UnsentMessageIdsView>>()
|
private let unsendMessageIdsViewSubscribers = Bag<ValuePipe<UnsentMessageIdsView>>()
|
||||||
|
|
||||||
@ -407,6 +410,8 @@ final class ViewTracker {
|
|||||||
pipe.putNext(view.immutableView())
|
pipe.putNext(view.immutableView())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.updateTrackedForumTopicListHoles()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateTrackedChatListHoles() {
|
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() {
|
private func updateTrackedHoles() {
|
||||||
var firstHolesAndTags = Set<MessageHistoryHolesViewEntry>()
|
var firstHolesAndTags = Set<MessageHistoryHolesViewEntry>()
|
||||||
for (view, _) in self.messageHistoryViews.copyItems() {
|
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> {
|
func unsentMessageIdsViewSignal() -> Signal<UnsentMessageIdsView, NoError> {
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
|
@ -373,22 +373,6 @@ func _internal_setForumChannelTopicPinned(account: Account, id: EnginePeer.Id, t
|
|||||||
account.stateManager.addUpdates(result)
|
account.stateManager.addUpdates(result)
|
||||||
|
|
||||||
return .complete()
|
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
|
case generic
|
||||||
}
|
}
|
||||||
|
|
||||||
func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Signal<Never, LoadMessageHistoryThreadsError> {
|
func _internal_loadMessageHistoryThreads(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId, offsetIndex: StoredPeerThreadCombinedState.Index?, limit: Int) -> Signal<Never, LoadMessageHistoryThreadsError> {
|
||||||
let signal: Signal<Never, LoadMessageHistoryThreadsError> = account.postbox.transaction { transaction -> Api.InputChannel? in
|
let signal: Signal<Never, LoadMessageHistoryThreadsError> = postbox.transaction { transaction -> Api.InputChannel? in
|
||||||
return transaction.getPeer(peerId).flatMap(apiInputChannel)
|
return transaction.getPeer(peerId).flatMap(apiInputChannel)
|
||||||
}
|
}
|
||||||
|> castError(LoadMessageHistoryThreadsError.self)
|
|> castError(LoadMessageHistoryThreadsError.self)
|
||||||
@ -430,23 +414,32 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si
|
|||||||
guard let inputChannel = inputChannel else {
|
guard let inputChannel = inputChannel else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
}
|
}
|
||||||
let signal: Signal<Never, LoadMessageHistoryThreadsError> = account.network.request(Api.functions.channels.getForumTopics(
|
let flags: Int32 = 0
|
||||||
flags: 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,
|
channel: inputChannel,
|
||||||
q: nil,
|
q: nil,
|
||||||
offsetDate: 0,
|
offsetDate: offsetDate,
|
||||||
offsetId: 0,
|
offsetId: offsetId,
|
||||||
offsetTopic: 0,
|
offsetTopic: offsetTopic,
|
||||||
limit: 100
|
limit: Int32(limit)
|
||||||
))
|
))
|
||||||
|> mapError { _ -> LoadMessageHistoryThreadsError in
|
|> mapError { _ -> LoadMessageHistoryThreadsError in
|
||||||
return .generic
|
return .generic
|
||||||
}
|
}
|
||||||
|> mapToSignal { result -> Signal<Never, LoadMessageHistoryThreadsError> in
|
|> mapToSignal { result -> Signal<Never, LoadMessageHistoryThreadsError> in
|
||||||
return account.postbox.transaction { transaction -> Void in
|
return postbox.transaction { transaction -> Void in
|
||||||
var pinnedId: Int64?
|
var pinnedId: Int64?
|
||||||
switch result {
|
switch result {
|
||||||
case let .forumTopics(flags, count, topics, messages, chats, users, pts):
|
case let .forumTopics(_, _, topics, messages, chats, users, pts):
|
||||||
var peers: [Peer] = []
|
var peers: [Peer] = []
|
||||||
var peerPresences: [PeerId: Api.User] = [:]
|
var peerPresences: [PeerId: Api.User] = [:]
|
||||||
for chat in chats {
|
for chat in chats {
|
||||||
@ -463,19 +456,16 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si
|
|||||||
return updated
|
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)
|
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
|
let _ = pts
|
||||||
|
var minIndex: StoredPeerThreadCombinedState.Index?
|
||||||
|
|
||||||
for topic in topics {
|
for topic in topics {
|
||||||
switch topic {
|
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: .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)
|
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:
|
case .forumTopicDeleted:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -520,9 +526,17 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si
|
|||||||
transaction.setPeerPinnedThreads(peerId: peerId, threadIds: [])
|
transaction.setPeerPinnedThreads(peerId: peerId, threadIds: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
if let entry = StoredPeerThreadCombinedState(PeerThreadCombinedState(
|
var nextIndex: StoredPeerThreadCombinedState.Index
|
||||||
validIndexBoundary: StoredPeerThreadCombinedState.Index(timestamp: Int32.max, threadId: Int64(Int32.max), messageId: Int32.max)
|
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)
|
transaction.setPeerThreadCombinedState(peerId: peerId, state: entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -641,7 +655,7 @@ public final class ForumChannelTopics {
|
|||||||
self.account = account
|
self.account = account
|
||||||
self.peerId = peerId
|
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())
|
self.updateDisposable.set(account.viewTracker.polledChannel(peerId: peerId).start())
|
||||||
}
|
}
|
||||||
|
@ -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()
|
let disposable = DisposableSet()
|
||||||
disposable.add(managedMessageHistoryHoles(accountPeerId: accountPeerId, network: network, postbox: postbox).start())
|
disposable.add(managedMessageHistoryHoles(accountPeerId: accountPeerId, network: network, postbox: postbox).start())
|
||||||
disposable.add(managedChatListHoles(network: network, postbox: postbox, accountPeerId: accountPeerId).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(managedSynchronizePeerReadStates(network: network, postbox: postbox, stateManager: stateManager).start())
|
||||||
disposable.add(managedSynchronizeGroupMessageStats(network: network, postbox: postbox, stateManager: stateManager).start())
|
disposable.add(managedSynchronizeGroupMessageStats(network: network, postbox: postbox, stateManager: stateManager).start())
|
||||||
|
|
||||||
|
@ -29,10 +29,14 @@ public final class EngineChatList: Equatable {
|
|||||||
|
|
||||||
public struct ForumTopicData: Equatable {
|
public struct ForumTopicData: Equatable {
|
||||||
public var title: String
|
public var title: String
|
||||||
|
public let iconFileId: Int64?
|
||||||
|
public let iconColor: Int32
|
||||||
public var maxOutgoingReadMessageId: EngineMessage.Id
|
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.title = title
|
||||||
|
self.iconFileId = iconFileId
|
||||||
|
self.iconColor = iconColor
|
||||||
self.maxOutgoingReadMessageId = maxOutgoingReadMessageId
|
self.maxOutgoingReadMessageId = maxOutgoingReadMessageId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -422,7 +426,7 @@ extension EngineChatList.Item {
|
|||||||
|
|
||||||
var forumTopicDataValue: EngineChatList.ForumTopicData?
|
var forumTopicDataValue: EngineChatList.ForumTopicData?
|
||||||
if let forumTopicData = forumTopicData?.data.get(MessageHistoryThreadData.self) {
|
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)
|
let readCounters = readState.flatMap(EnginePeerReadCounters.init)
|
||||||
|
@ -95,6 +95,7 @@ public enum PresentationResourceKey: Int32 {
|
|||||||
case chatListFakeServiceIcon
|
case chatListFakeServiceIcon
|
||||||
case chatListSecretIcon
|
case chatListSecretIcon
|
||||||
case chatListStatusLockIcon
|
case chatListStatusLockIcon
|
||||||
|
case chatListTopicArrowIcon
|
||||||
case chatListRecentStatusOnlineIcon
|
case chatListRecentStatusOnlineIcon
|
||||||
case chatListRecentStatusOnlineHighlightedIcon
|
case chatListRecentStatusOnlineHighlightedIcon
|
||||||
case chatListRecentStatusOnlinePinnedIcon
|
case chatListRecentStatusOnlinePinnedIcon
|
||||||
|
@ -360,4 +360,10 @@ public struct PresentationResourcesChatList {
|
|||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/StatusLockIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor)
|
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 {
|
} else {
|
||||||
if let titleContent = self.titleContent {
|
if let titleContent = self.titleContent {
|
||||||
switch titleContent {
|
switch titleContent {
|
||||||
case let .peer(peerView, _, onlineMemberCount, isScheduledMessages, _):
|
case let .peer(peerView, customTitle, onlineMemberCount, isScheduledMessages, _):
|
||||||
if let peer = peerViewMainPeer(peerView) {
|
if let peer = peerViewMainPeer(peerView) {
|
||||||
let servicePeer = isServicePeer(peer)
|
let servicePeer = isServicePeer(peer)
|
||||||
if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isReplies {
|
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)
|
state = .info(string, .generic)
|
||||||
}
|
}
|
||||||
} else if let channel = peer as? TelegramChannel {
|
} 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 {
|
if memberCount == 0 {
|
||||||
let string: NSAttributedString
|
let string: NSAttributedString
|
||||||
if case .group = channel.info {
|
if case .group = channel.info {
|
||||||
|
@ -223,16 +223,19 @@ public final class EmojiStatusComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
iconImage = nil
|
iconImage = nil
|
||||||
}
|
}
|
||||||
case let .topic(title, color, size):
|
case let .topic(title, color, realSize):
|
||||||
func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor]) -> UIImage? {
|
func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor]) -> UIImage? {
|
||||||
return generateImage(size, rotatedContext: { size, context in
|
return generateImage(realSize, rotatedContext: { realSize, context in
|
||||||
context.clear(CGRect(origin: .zero, size: size))
|
context.clear(CGRect(origin: .zero, size: realSize))
|
||||||
|
|
||||||
context.saveGState()
|
context.saveGState()
|
||||||
|
|
||||||
let scale: CGFloat = size.width / 32.0
|
let size = CGSize(width: 32.0, height: 32.0)
|
||||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
||||||
|
let scale: CGFloat = realSize.width / size.width
|
||||||
context.scaleBy(x: scale, y: scale)
|
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)
|
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 ")
|
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 line = CTLineCreateWithAttributedString(attributedString)
|
||||||
let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
|
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))
|
context.translateBy(x: realSize.width / 2.0, y: realSize.height / 2.0)
|
||||||
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.scaleBy(x: 1.0, y: -1.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)
|
context.translateBy(x: lineOrigin.x, y: lineOrigin.y)
|
||||||
CTLineDraw(line, context)
|
CTLineDraw(line, context)
|
||||||
|
@ -872,8 +872,8 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var replaceImpl: ((ViewController) -> Void)?
|
var replaceImpl: ((ViewController) -> Void)?
|
||||||
let controller = PremiumDemoScreen(context: context, subject: .uniqueReactions, action: {
|
let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: {
|
||||||
let controller = PremiumIntroScreen(context: context, source: .reactions)
|
let controller = PremiumIntroScreen(context: context, source: .animatedEmoji)
|
||||||
replaceImpl?(controller)
|
replaceImpl?(controller)
|
||||||
})
|
})
|
||||||
replaceImpl = { [weak controller] c in
|
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
|
@ -265,6 +265,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
private var moreInfoNavigationButton: ChatNavigationButton?
|
private var moreInfoNavigationButton: ChatNavigationButton?
|
||||||
|
|
||||||
private var peerView: PeerView?
|
private var peerView: PeerView?
|
||||||
|
private var threadInfo: EngineMessageHistoryThread.Info?
|
||||||
|
|
||||||
private var historyStateDisposable: Disposable?
|
private var historyStateDisposable: Disposable?
|
||||||
|
|
||||||
@ -520,6 +521,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
private var inviteRequestsContext: PeerInvitationImportersContext?
|
private var inviteRequestsContext: PeerInvitationImportersContext?
|
||||||
private var inviteRequestsDisposable = MetaDisposable()
|
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] = []) {
|
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
|
let _ = ChatControllerCount.modify { value in
|
||||||
return value + 1
|
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 {
|
if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation, replyThreadMessage.messageId == message.id {
|
||||||
return .none
|
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) {
|
if canReplyInChat(strongSelf.presentationInterfaceState) {
|
||||||
return .reply
|
return .reply
|
||||||
@ -4408,10 +4426,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, hasScheduledMessages, self.reportIrrelvantGeoNoticePromise.get(), displayedCountSignal)
|
let threadInfo: Signal<EngineMessageHistoryThread.Info?, NoError>
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount in
|
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 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4454,6 +4490,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
let firstTime = strongSelf.peerView == nil
|
let firstTime = strongSelf.peerView == nil
|
||||||
strongSelf.peerView = peerView
|
strongSelf.peerView = peerView
|
||||||
|
strongSelf.threadInfo = threadInfo
|
||||||
if wasGroupChannel != isGroupChannel {
|
if wasGroupChannel != isGroupChannel {
|
||||||
if let isGroupChannel = isGroupChannel, 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 })
|
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 })
|
||||||
@ -4467,7 +4504,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if strongSelf.isNodeLoaded {
|
if strongSelf.isNodeLoaded {
|
||||||
strongSelf.chatDisplayNode.peerView = peerView
|
strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle
|
||||||
}
|
}
|
||||||
var peerIsMuted = false
|
var peerIsMuted = false
|
||||||
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
|
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
|
||||||
@ -4841,9 +4878,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
|
|
||||||
let firstTime = strongSelf.peerView == nil
|
let firstTime = strongSelf.peerView == nil
|
||||||
strongSelf.peerView = peerView
|
strongSelf.peerView = peerView
|
||||||
|
strongSelf.threadInfo = messageAndTopic.threadData?.info
|
||||||
|
|
||||||
if strongSelf.isNodeLoaded {
|
if strongSelf.isNodeLoaded {
|
||||||
strongSelf.chatDisplayNode.peerView = peerView
|
strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
var peerDiscussionId: PeerId?
|
var peerDiscussionId: PeerId?
|
||||||
@ -5922,7 +5960,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)
|
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|
||||||
|> map { peer in
|
|> map { peer in
|
||||||
|
@ -79,9 +79,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
private var containerNode: ASDisplayNode?
|
private var containerNode: ASDisplayNode?
|
||||||
private var overlayNavigationBar: ChatOverlayNavigationBar?
|
private var overlayNavigationBar: ChatOverlayNavigationBar?
|
||||||
|
|
||||||
var peerView: PeerView? {
|
var overlayTitle: String? {
|
||||||
didSet {
|
didSet {
|
||||||
self.overlayNavigationBar?.peerView = self.peerView
|
self.overlayNavigationBar?.title = self.overlayTitle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,7 +234,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
} else {
|
} else {
|
||||||
loadingPlaceholderNode = ChatLoadingPlaceholderNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners, backgroundNode: self.backgroundNode)
|
loadingPlaceholderNode = ChatLoadingPlaceholderNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners, backgroundNode: self.backgroundNode)
|
||||||
loadingPlaceholderNode.updatePresentationInterfaceState(self.chatPresentationInterfaceState)
|
loadingPlaceholderNode.updatePresentationInterfaceState(self.chatPresentationInterfaceState)
|
||||||
self.contentContainerNode.insertSubnode(loadingPlaceholderNode, aboveSubnode: self.backgroundNode)
|
self.backgroundNode.supernode?.insertSubnode(loadingPlaceholderNode, aboveSubnode: self.backgroundNode)
|
||||||
|
|
||||||
self.loadingPlaceholderNode = loadingPlaceholderNode
|
self.loadingPlaceholderNode = loadingPlaceholderNode
|
||||||
|
|
||||||
@ -968,7 +968,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}, close: { [weak self] in
|
}, close: { [weak self] in
|
||||||
self?.dismissAsOverlay()
|
self?.dismissAsOverlay()
|
||||||
})
|
})
|
||||||
overlayNavigationBar.peerView = self.peerView
|
overlayNavigationBar.title = self.overlayTitle
|
||||||
self.overlayNavigationBar = overlayNavigationBar
|
self.overlayNavigationBar = overlayNavigationBar
|
||||||
self.containerNode?.addSubnode(overlayNavigationBar)
|
self.containerNode?.addSubnode(overlayNavigationBar)
|
||||||
}
|
}
|
||||||
|
@ -525,6 +525,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
|
|
||||||
let message = messages[0]
|
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 {
|
if Namespaces.Message.allScheduled.contains(message.id.namespace) || message.id.peerId.isReplies {
|
||||||
canReply = false
|
canReply = false
|
||||||
canPin = false
|
canPin = false
|
||||||
|
@ -181,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,15 +22,10 @@ final class ChatOverlayNavigationBar: ASDisplayNode {
|
|||||||
|
|
||||||
private var validLayout: CGSize?
|
private var validLayout: CGSize?
|
||||||
|
|
||||||
private var peerTitle = ""
|
private var peerTitle: String = ""
|
||||||
var peerView: PeerView? {
|
var title: String? {
|
||||||
didSet {
|
didSet {
|
||||||
var title = ""
|
let title = self.title ?? ""
|
||||||
if let peerView = self.peerView {
|
|
||||||
if let peer = peerViewMainPeer(peerView) {
|
|
||||||
title = EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.peerTitle != title {
|
if self.peerTitle != title {
|
||||||
self.peerTitle = title
|
self.peerTitle = title
|
||||||
if let size = self.validLayout {
|
if let size = self.validLayout {
|
||||||
|
@ -49,6 +49,8 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode {
|
|||||||
//TODO:localize
|
//TODO:localize
|
||||||
iconImage = PresentationResourcesChat.chatPanelLockIcon(interfaceState.theme)
|
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)
|
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 {
|
} else if let (untilDate, personal) = bannedPermission {
|
||||||
if personal && untilDate != 0 && untilDate != Int32.max {
|
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)
|
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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user