mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-26 15:31:12 +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
|
||||
|
||||
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
|
||||
@ -523,7 +674,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
|
||||
@ -731,6 +882,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
containerSize: avatarIconView.bounds.size
|
||||
)
|
||||
}
|
||||
self.authorNode.visibilityStatus = self.visibilityStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -766,9 +918,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
|
||||
@ -1045,7 +1196,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()
|
||||
@ -1286,6 +1437,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):
|
||||
@ -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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1582,14 +1731,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)
|
||||
@ -1776,7 +1925,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 {
|
||||
@ -1869,7 +2025,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
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, 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 = []
|
||||
} else {
|
||||
peerRevealOptions = []
|
||||
@ -2205,13 +2361,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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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)
|
||||
|
@ -872,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
|
@ -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
|
||||
@ -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)
|
||||
|> 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
|
||||
}
|
||||
|
||||
@ -4454,6 +4490,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 })
|
||||
@ -4467,7 +4504,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 {
|
||||
@ -4841,9 +4878,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?
|
||||
@ -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)
|
||||
|> map { peer 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,7 +234,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
|
||||
|
||||
@ -968,7 +968,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)
|
||||
}
|
||||
|
@ -525,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
|
||||
|
@ -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 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 {
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user