[WIP] Topics

This commit is contained in:
Ali 2022-10-21 20:49:27 +04:00
parent 94d264900f
commit f3caab5096
26 changed files with 633 additions and 96 deletions

View File

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

View File

@ -232,7 +232,7 @@ func chatListViewForLocation(chatListLocation: ChatListControllerLocation, locat
pinnedIndex = .none
}
let readCounters = EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: 1, maxKnownId: 1, count: data.incomingUnreadCount, markedUnread: false))]), isMuted: false)
let readCounters = EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: data.maxOutgoingReadId, maxKnownId: 1, count: data.incomingUnreadCount, markedUnread: false))]), isMuted: false)
var draft: EngineChatList.Draft?
if let embeddedState = item.embeddedInterfaceState, let _ = embeddedState.overrideChatTimestamp {

View File

@ -204,6 +204,11 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV
fromEmptyView = true
}
if let fromView = fromView, !fromView.isLoading, toView.isLoading {
options.remove(.AnimateInsertion)
options.remove(.AnimateAlpha)
}
var adjustScrollToFirstItem = false
if !previewing && !searchMode && fromEmptyView && scrollToItem == nil && toView.filteredEntries.count >= 2 {
adjustScrollToFirstItem = true

View File

@ -30,3 +30,35 @@ public final class ChatListHolesView {
self.entries = mutableView.entries
}
}
public struct ForumTopicListHolesEntry: Hashable {
public let peerId: PeerId
public let index: StoredPeerThreadCombinedState.Index?
public init(peerId: PeerId, index: StoredPeerThreadCombinedState.Index?) {
self.peerId = peerId
self.index = index
}
}
final class MutableForumTopicListHolesView {
fileprivate var entries = Set<ForumTopicListHolesEntry>()
func update(holes: Set<ForumTopicListHolesEntry>) -> Bool {
if self.entries != holes {
self.entries = holes
return true
} else {
return false
}
}
}
public final class ForumTopicListHolesView {
public let entries: Set<ForumTopicListHolesEntry>
init(_ mutableView: MutableForumTopicListHolesView) {
self.entries = mutableView.entries
}
}

View File

@ -33,6 +33,7 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView {
fileprivate let summaryComponents: ChatListEntrySummaryComponents
fileprivate var peer: Peer?
fileprivate var items: [Item] = []
private var hole: ForumTopicListHolesEntry?
fileprivate var isLoading: Bool = false
init(postbox: PostboxImpl, peerId: PeerId, summaryComponents: ChatListEntrySummaryComponents) {
@ -50,6 +51,16 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView {
let validIndexBoundary = postbox.peerThreadCombinedStateTable.get(peerId: peerId)?.validIndexBoundary
self.isLoading = validIndexBoundary == nil
if let validIndexBoundary = validIndexBoundary {
if validIndexBoundary.messageId != 1 {
self.hole = ForumTopicListHolesEntry(peerId: self.peerId, index: validIndexBoundary)
} else {
self.hole = nil
}
} else {
self.hole = ForumTopicListHolesEntry(peerId: self.peerId, index: nil)
}
if !self.isLoading {
let pinnedThreadIds = postbox.messageHistoryThreadPinnedTable.get(peerId: self.peerId)
var nextPinnedIndex = 0
@ -124,9 +135,15 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView {
return updated
}
func topHole() -> ForumTopicListHolesEntry? {
return self.hole
}
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
return false
self.reload(postbox: postbox)
return true
}
func immutableView() -> PostboxView {

View File

@ -1,7 +1,7 @@
import Foundation
public struct StoredPeerThreadCombinedState: Equatable, Codable {
public struct Index: Equatable, Comparable, Codable {
public struct Index: Hashable, Comparable, Codable {
private enum CodingKeys: String, CodingKey {
case timestamp = "t"
case threadId = "i"

View File

@ -3281,6 +3281,18 @@ final class PostboxImpl {
}
}
public func forumTopicListHolesView() -> Signal<ForumTopicListHolesView, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.queue.async {
disposable.set(self.viewTracker.forumTopicListHolesViewSignal().start(next: { view in
subscriber.putNext(view)
}))
}
return disposable
}
}
public func unsentMessageIdsView() -> Signal<UnsentMessageIdsView, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
@ -4195,6 +4207,18 @@ public class Postbox {
return disposable
}
}
public func forumTopicListHolesView() -> Signal<ForumTopicListHolesView, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.forumTopicListHolesView().start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion))
}
return disposable
}
}
public func unsentMessageIdsView() -> Signal<UnsentMessageIdsView, NoError> {
return Signal { subscriber in

View File

@ -10,7 +10,7 @@ protocol MutablePostboxView {
}
final class CombinedMutableView {
private let views: [PostboxViewKey: MutablePostboxView]
let views: [PostboxViewKey: MutablePostboxView]
init(views: [PostboxViewKey: MutablePostboxView]) {
self.views = views

View File

@ -25,6 +25,9 @@ final class ViewTracker {
private let chatListHolesView = MutableChatListHolesView()
private let chatListHolesViewSubscribers = Bag<ValuePipe<ChatListHolesView>>()
private let forumTopicListHolesView = MutableForumTopicListHolesView()
private let forumTopicListHolesViewSubscribers = Bag<ValuePipe<ForumTopicListHolesView>>()
private var unsentMessageView: UnsentMessageHistoryView
private let unsendMessageIdsViewSubscribers = Bag<ValuePipe<UnsentMessageIdsView>>()
@ -407,6 +410,8 @@ final class ViewTracker {
pipe.putNext(view.immutableView())
}
}
self.updateTrackedForumTopicListHoles()
}
private func updateTrackedChatListHoles() {
@ -425,6 +430,26 @@ final class ViewTracker {
}
}
private func updateTrackedForumTopicListHoles() {
var firstHoles = Set<ForumTopicListHolesEntry>()
for (views) in self.combinedViews.copyItems() {
for (key, view) in views.0.views {
if case .messageHistoryThreadIndex = key, let view = view as? MutableMessageHistoryThreadIndexView {
if let hole = view.topHole() {
firstHoles.insert(hole)
}
}
}
}
if self.forumTopicListHolesView.update(holes: firstHoles) {
for pipe in self.forumTopicListHolesViewSubscribers.copyItems() {
pipe.putNext(ForumTopicListHolesView(self.forumTopicListHolesView))
}
}
}
private func updateTrackedHoles() {
var firstHolesAndTags = Set<MessageHistoryHolesViewEntry>()
for (view, _) in self.messageHistoryViews.copyItems() {
@ -506,6 +531,30 @@ final class ViewTracker {
}
}
func forumTopicListHolesViewSignal() -> Signal<ForumTopicListHolesView, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.queue.async {
subscriber.putNext(ForumTopicListHolesView(self.forumTopicListHolesView))
let pipe = ValuePipe<ForumTopicListHolesView>()
let index = self.forumTopicListHolesViewSubscribers.add(pipe)
let pipeDisposable = pipe.signal().start(next: { view in
subscriber.putNext(view)
})
disposable.set(ActionDisposable {
self.queue.async {
pipeDisposable.dispose()
self.forumTopicListHolesViewSubscribers.remove(index)
}
})
}
return disposable
}
}
func unsentMessageIdsViewSignal() -> Signal<UnsentMessageIdsView, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()

View File

@ -373,22 +373,6 @@ func _internal_setForumChannelTopicPinned(account: Account, id: EnginePeer.Id, t
account.stateManager.addUpdates(result)
return .complete()
/*return account.postbox.transaction { transaction -> Void in
if let initialData = transaction.getMessageHistoryThreadInfo(peerId: id, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
var data = initialData
data.isClosed = isClosed
if data != initialData {
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: id, threadId: threadId, info: entry)
}
}
}
}
|> castError(EditForumChannelTopicError.self)
|> ignoreValues*/
}
}
}
@ -421,8 +405,8 @@ enum LoadMessageHistoryThreadsError {
case generic
}
func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Signal<Never, LoadMessageHistoryThreadsError> {
let signal: Signal<Never, LoadMessageHistoryThreadsError> = account.postbox.transaction { transaction -> Api.InputChannel? in
func _internal_loadMessageHistoryThreads(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId, offsetIndex: StoredPeerThreadCombinedState.Index?, limit: Int) -> Signal<Never, LoadMessageHistoryThreadsError> {
let signal: Signal<Never, LoadMessageHistoryThreadsError> = postbox.transaction { transaction -> Api.InputChannel? in
return transaction.getPeer(peerId).flatMap(apiInputChannel)
}
|> castError(LoadMessageHistoryThreadsError.self)
@ -430,23 +414,32 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si
guard let inputChannel = inputChannel else {
return .fail(.generic)
}
let signal: Signal<Never, LoadMessageHistoryThreadsError> = account.network.request(Api.functions.channels.getForumTopics(
flags: 0,
let flags: Int32 = 0
var offsetDate: Int32 = 0
var offsetId: Int32 = 0
var offsetTopic: Int32 = 0
if let offsetIndex = offsetIndex {
offsetDate = offsetIndex.timestamp
offsetId = offsetIndex.messageId
offsetTopic = Int32(clamping: offsetIndex.threadId)
}
let signal: Signal<Never, LoadMessageHistoryThreadsError> = network.request(Api.functions.channels.getForumTopics(
flags: flags,
channel: inputChannel,
q: nil,
offsetDate: 0,
offsetId: 0,
offsetTopic: 0,
limit: 100
offsetDate: offsetDate,
offsetId: offsetId,
offsetTopic: offsetTopic,
limit: Int32(limit)
))
|> mapError { _ -> LoadMessageHistoryThreadsError in
return .generic
}
|> mapToSignal { result -> Signal<Never, LoadMessageHistoryThreadsError> in
return account.postbox.transaction { transaction -> Void in
return postbox.transaction { transaction -> Void in
var pinnedId: Int64?
switch result {
case let .forumTopics(flags, count, topics, messages, chats, users, pts):
case let .forumTopics(_, _, topics, messages, chats, users, pts):
var peers: [Peer] = []
var peerPresences: [PeerId: Api.User] = [:]
for chat in chats {
@ -463,19 +456,16 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si
return updated
})
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences)
let _ = InternalAccountState.addMessages(transaction: transaction, messages: messages.compactMap { message -> StoreMessage? in
let addedMessages = messages.compactMap { message -> StoreMessage? in
return StoreMessage(apiMessage: message)
}, location: .Random)
}
let _ = InternalAccountState.addMessages(transaction: transaction, messages: addedMessages, location: .Random)
let _ = flags
let _ = count
let _ = topics
let _ = messages
let _ = chats
let _ = users
let _ = pts
var minIndex: StoredPeerThreadCombinedState.Index?
for topic in topics {
switch topic {
@ -509,6 +499,22 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si
transaction.replaceMessageTagSummary(peerId: peerId, threadId: Int64(id), tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, count: unreadMentionsCount, maxId: topMessage)
transaction.replaceMessageTagSummary(peerId: peerId, threadId: Int64(id), tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud, count: unreadReactionsCount, maxId: topMessage)
var topTimestamp = date
for message in addedMessages {
if message.id.peerId == peerId && message.threadId == Int64(id) {
topTimestamp = max(topTimestamp, message.timestamp)
}
}
let topicIndex = StoredPeerThreadCombinedState.Index(timestamp: topTimestamp, threadId: Int64(id), messageId: topMessage)
if let minIndexValue = minIndex {
if topicIndex < minIndexValue {
minIndex = topicIndex
}
} else {
minIndex = topicIndex
}
case .forumTopicDeleted:
break
}
@ -520,9 +526,17 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si
transaction.setPeerPinnedThreads(peerId: peerId, threadIds: [])
}
if let entry = StoredPeerThreadCombinedState(PeerThreadCombinedState(
validIndexBoundary: StoredPeerThreadCombinedState.Index(timestamp: Int32.max, threadId: Int64(Int32.max), messageId: Int32.max)
)) {
var nextIndex: StoredPeerThreadCombinedState.Index
if topics.count != 0 {
nextIndex = minIndex ?? StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1)
} else {
nextIndex = StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1)
}
if let offsetIndex = offsetIndex, nextIndex == offsetIndex {
nextIndex = StoredPeerThreadCombinedState.Index(timestamp: 0, threadId: 0, messageId: 1)
}
if let entry = StoredPeerThreadCombinedState(PeerThreadCombinedState(validIndexBoundary: nextIndex)) {
transaction.setPeerThreadCombinedState(peerId: peerId, state: entry)
}
}
@ -641,7 +655,7 @@ public final class ForumChannelTopics {
self.account = account
self.peerId = peerId
let _ = _internal_loadMessageHistoryThreads(account: self.account, peerId: peerId).start()
//let _ = _internal_loadMessageHistoryThreads(account: self.account, peerId: peerId, offsetIndex: nil, limit: 100).start()
self.updateDisposable.set(account.viewTracker.polledChannel(peerId: peerId).start())
}

View File

@ -90,3 +90,70 @@ func managedChatListHoles(network: Network, postbox: Postbox, accountPeerId: Pee
}
}
}
private final class ManagedForumTopicListHolesState {
private var currentHoles: [ForumTopicListHolesEntry: Disposable] = [:]
func clearDisposables() -> [Disposable] {
let disposables = Array(self.currentHoles.values)
self.currentHoles.removeAll()
return disposables
}
func update(entries: [ForumTopicListHolesEntry]) -> (removed: [Disposable], added: [ForumTopicListHolesEntry: MetaDisposable]) {
var removed: [Disposable] = []
var added: [ForumTopicListHolesEntry: MetaDisposable] = [:]
for entry in entries {
if self.currentHoles[entry] == nil {
let disposable = MetaDisposable()
added[entry] = disposable
self.currentHoles[entry] = disposable
}
}
var removedKeys: [ForumTopicListHolesEntry] = []
for (entry, disposable) in self.currentHoles {
if !entries.contains(entry) {
removed.append(disposable)
removedKeys.append(entry)
}
}
for key in removedKeys {
self.currentHoles.removeValue(forKey: key)
}
return (removed, added)
}
}
func managedForumTopicListHoles(network: Network, postbox: Postbox, accountPeerId: PeerId) -> Signal<Void, NoError> {
return Signal { _ in
let state = Atomic(value: ManagedForumTopicListHolesState())
let disposable = postbox.forumTopicListHolesView().start(next: { view in
let entries = Array(view.entries)
let (removed, added) = state.with { state in
return state.update(entries: entries)
}
for disposable in removed {
disposable.dispose()
}
for (entry, disposable) in added {
disposable.set(_internal_loadMessageHistoryThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, peerId: entry.peerId, offsetIndex: entry.index, limit: 100).start())
}
})
return ActionDisposable {
disposable.dispose()
for disposable in state.with({ state -> [Disposable] in
state.clearDisposables()
}) {
disposable.dispose()
}
}
}
}

View File

@ -7,6 +7,7 @@ func managedServiceViews(accountPeerId: PeerId, network: Network, postbox: Postb
let disposable = DisposableSet()
disposable.add(managedMessageHistoryHoles(accountPeerId: accountPeerId, network: network, postbox: postbox).start())
disposable.add(managedChatListHoles(network: network, postbox: postbox, accountPeerId: accountPeerId).start())
disposable.add(managedForumTopicListHoles(network: network, postbox: postbox, accountPeerId: accountPeerId).start())
disposable.add(managedSynchronizePeerReadStates(network: network, postbox: postbox, stateManager: stateManager).start())
disposable.add(managedSynchronizeGroupMessageStats(network: network, postbox: postbox, stateManager: stateManager).start())

View File

@ -29,10 +29,14 @@ public final class EngineChatList: Equatable {
public struct ForumTopicData: Equatable {
public var title: String
public let iconFileId: Int64?
public let iconColor: Int32
public var maxOutgoingReadMessageId: EngineMessage.Id
public init(title: String, maxOutgoingReadMessageId: EngineMessage.Id) {
public init(title: String, iconFileId: Int64?, iconColor: Int32, maxOutgoingReadMessageId: EngineMessage.Id) {
self.title = title
self.iconFileId = iconFileId
self.iconColor = iconColor
self.maxOutgoingReadMessageId = maxOutgoingReadMessageId
}
}
@ -422,7 +426,7 @@ extension EngineChatList.Item {
var forumTopicDataValue: EngineChatList.ForumTopicData?
if let forumTopicData = forumTopicData?.data.get(MessageHistoryThreadData.self) {
forumTopicDataValue = EngineChatList.ForumTopicData(title: forumTopicData.info.title, maxOutgoingReadMessageId: MessageId(peerId: index.messageIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: forumTopicData.maxOutgoingReadId))
forumTopicDataValue = EngineChatList.ForumTopicData(title: forumTopicData.info.title, iconFileId: forumTopicData.info.icon, iconColor: forumTopicData.info.iconColor, maxOutgoingReadMessageId: MessageId(peerId: index.messageIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: forumTopicData.maxOutgoingReadId))
}
let readCounters = readState.flatMap(EnginePeerReadCounters.init)

View File

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

View File

@ -360,4 +360,10 @@ public struct PresentationResourcesChatList {
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/StatusLockIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor)
})
}
public static func topicArrowIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatListTopicArrowIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/TopicArrowIcon"), color: theme.chatList.titleColor)
})
}
}

View File

@ -420,7 +420,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
} else {
if let titleContent = self.titleContent {
switch titleContent {
case let .peer(peerView, _, onlineMemberCount, isScheduledMessages, _):
case let .peer(peerView, customTitle, onlineMemberCount, isScheduledMessages, _):
if let peer = peerViewMainPeer(peerView) {
let servicePeer = isServicePeer(peer)
if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isReplies {
@ -485,7 +485,10 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
state = .info(string, .generic)
}
} else if let channel = peer as? TelegramChannel {
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
if channel.flags.contains(.isForum), customTitle != nil {
let string = NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
state = .info(string, .generic)
} else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
if memberCount == 0 {
let string: NSAttributedString
if case .group = channel.info {

View File

@ -223,16 +223,19 @@ public final class EmojiStatusComponent: Component {
} else {
iconImage = nil
}
case let .topic(title, color, size):
case let .topic(title, color, realSize):
func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor]) -> UIImage? {
return generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
return generateImage(realSize, rotatedContext: { realSize, context in
context.clear(CGRect(origin: .zero, size: realSize))
context.saveGState()
let scale: CGFloat = size.width / 32.0
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
let size = CGSize(width: 32.0, height: 32.0)
let scale: CGFloat = realSize.width / size.width
context.scaleBy(x: scale, y: scale)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.translateBy(x: -14.0 - UIScreenPixel, y: -14.0 - UIScreenPixel)
let _ = try? drawSvgPath(context, path: "M24.1835,4.71703 C21.7304,2.42169 18.2984,0.995605 14.5,0.995605 C7.04416,0.995605 1.0,6.49029 1.0,13.2683 C1.0,17.1341 2.80572,20.3028 5.87839,22.5523 C6.27132,22.84 6.63324,24.4385 5.75738,25.7811 C5.39922,26.3301 5.00492,26.7573 4.70138,27.0861 C4.26262,27.5614 4.01347,27.8313 4.33716,27.967 C4.67478,28.1086 6.66968,28.1787 8.10952,27.3712 C9.23649,26.7392 9.91903,26.1087 10.3787,25.6842 C10.7588,25.3331 10.9864,25.1228 11.187,25.1688 C11.9059,25.3337 12.6478,25.4461 13.4075,25.5015 C13.4178,25.5022 13.4282,25.503 13.4386,25.5037 C13.7888,25.5284 14.1428,25.5411 14.5,25.5411 C21.9558,25.5411 28.0,20.0464 28.0,13.2683 C28.0,9.94336 26.5455,6.92722 24.1835,4.71703 ")
@ -269,13 +272,12 @@ public final class EmojiStatusComponent: Component {
let line = CTLineCreateWithAttributedString(attributedString)
let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
let lineOffset = CGPoint(x: title == "B" ? 1.0 : 0.0, y: floorToScreenPixels(realSize.height * 0.05))
let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (realSize.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (realSize.height - lineBounds.size.height) / 2.0) + lineOffset.y)
let lineOffset = CGPoint(x: title == "B" ? 1.0 : 0.0, y: floorToScreenPixels(0.67 * scale))
let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0) + lineOffset.y)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.translateBy(x: realSize.width / 2.0, y: realSize.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.translateBy(x: -realSize.width / 2.0, y: -realSize.height / 2.0)
context.translateBy(x: lineOrigin.x, y: lineOrigin.y)
CTLineDraw(line, context)

View File

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

View File

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

View File

@ -0,0 +1,92 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.000000 -0.350586 cm
0.000000 0.000000 0.000000 scn
0.468521 11.725403 m
0.261516 11.984160 -0.116060 12.026113 -0.374817 11.819107 c
-0.633574 11.612102 -0.675527 11.234526 -0.468521 10.975769 c
0.468521 11.725403 l
h
4.000000 6.350586 m
4.468521 5.975769 l
4.643826 6.194900 4.643826 6.506272 4.468521 6.725403 c
4.000000 6.350586 l
h
-0.468521 1.725403 m
-0.675527 1.466646 -0.633574 1.089070 -0.374817 0.882065 c
-0.116060 0.675059 0.261516 0.717011 0.468521 0.975769 c
-0.468521 1.725403 l
h
-0.468521 10.975769 m
3.531479 5.975769 l
4.468521 6.725403 l
0.468521 11.725403 l
-0.468521 10.975769 l
h
3.531479 6.725403 m
-0.468521 1.725403 l
0.468521 0.975769 l
4.468521 5.975769 l
3.531479 6.725403 l
h
f
n
Q
endstream
endobj
3 0 obj
781
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 6.000000 12.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000871 00000 n
0000000893 00000 n
0000001065 00000 n
0000001139 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1198
%%EOF

View File

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

View File

@ -79,9 +79,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
private var containerNode: ASDisplayNode?
private var overlayNavigationBar: ChatOverlayNavigationBar?
var peerView: PeerView? {
var overlayTitle: String? {
didSet {
self.overlayNavigationBar?.peerView = self.peerView
self.overlayNavigationBar?.title = self.overlayTitle
}
}
@ -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)
}

View File

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

View File

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

View File

@ -22,15 +22,10 @@ final class ChatOverlayNavigationBar: ASDisplayNode {
private var validLayout: CGSize?
private var peerTitle = ""
var peerView: PeerView? {
private var peerTitle: String = ""
var title: String? {
didSet {
var title = ""
if let peerView = self.peerView {
if let peer = peerViewMainPeer(peerView) {
title = EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder)
}
}
let title = self.title ?? ""
if self.peerTitle != title {
self.peerTitle = title
if let size = self.validLayout {

View File

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