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

This commit is contained in:
Ilya Laktyushin 2025-05-24 16:38:45 +02:00
commit c4c05851a7
29 changed files with 400 additions and 244 deletions

View File

@ -1067,6 +1067,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if case let .channel(channel) = peer, channel.flags.contains(.isMonoforum) { if case let .channel(channel) = peer, channel.flags.contains(.isMonoforum) {
openAsInlineForum = false openAsInlineForum = false
} else if case let .channel(channel) = peer, channel.flags.contains(.displayForumAsTabs) {
openAsInlineForum = false
} else { } else {
if let cachedData = cachedDataView.cachedPeerData as? CachedChannelData, case let .known(viewForumAsMessages) = cachedData.viewForumAsMessages, viewForumAsMessages { if let cachedData = cachedDataView.cachedPeerData as? CachedChannelData, case let .known(viewForumAsMessages) = cachedData.viewForumAsMessages, viewForumAsMessages {
openAsInlineForum = false openAsInlineForum = false
@ -1519,7 +1521,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let threadId = threadId { if let threadId = threadId {
let source: ContextContentSource let source: ContextContentSource
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .replyThread(message: ChatReplyThreadMessage( let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .replyThread(message: ChatReplyThreadMessage(
peerId: peer.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: !channel.isMonoForum, isMonoforumPost: channel.isMonoForum, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false peerId: peer.peerId, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: channel.isForumOrMonoForum, isMonoforumPost: channel.isMonoForum, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false
)), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) )), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil)
chatController.canReadHistory.set(false) chatController.canReadHistory.set(false)
source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))

View File

@ -463,6 +463,7 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour {
public static let toggleUnread = Actions(rawValue: 1 << 0) public static let toggleUnread = Actions(rawValue: 1 << 0)
public static let delete = Actions(rawValue: 1 << 1) public static let delete = Actions(rawValue: 1 << 1)
public static let togglePinned = Actions(rawValue: 1 << 2)
} }
case custom(Actions) case custom(Actions)
@ -994,6 +995,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
final class TopicItemNode: ASDisplayNode { final class TopicItemNode: ASDisplayNode {
let topicTitleNode: TextNode let topicTitleNode: TextNode
let titleTopicIconView: ComponentHostView<Empty>? let titleTopicIconView: ComponentHostView<Empty>?
var titleTopicAvatarNode: AvatarNode?
var titleTopicIconComponent: EmojiStatusComponent? var titleTopicIconComponent: EmojiStatusComponent?
var visibilityStatus: Bool = false { var visibilityStatus: Bool = false {
@ -1011,30 +1013,34 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} }
} }
private init(topicTitleNode: TextNode, titleTopicIconView: ComponentHostView<Empty>?, titleTopicIconComponent: EmojiStatusComponent?) { private init(topicTitleNode: TextNode, titleTopicIconView: ComponentHostView<Empty>?, titleTopicAvatarNode: AvatarNode?, titleTopicIconComponent: EmojiStatusComponent?) {
self.topicTitleNode = topicTitleNode self.topicTitleNode = topicTitleNode
self.titleTopicIconView = titleTopicIconView self.titleTopicIconView = titleTopicIconView
self.titleTopicAvatarNode = titleTopicAvatarNode
self.titleTopicIconComponent = titleTopicIconComponent self.titleTopicIconComponent = titleTopicIconComponent
super.init() super.init()
self.addSubnode(self.topicTitleNode) self.addSubnode(self.topicTitleNode)
if let titleTopicAvatarNode = self.titleTopicAvatarNode {
self.view.addSubview(titleTopicAvatarNode.view)
}
if let titleTopicIconView = self.titleTopicIconView { if let titleTopicIconView = self.titleTopicIconView {
self.view.addSubview(titleTopicIconView) self.view.addSubview(titleTopicIconView)
} }
} }
static func asyncLayout(_ currentNode: TopicItemNode?) -> (_ constrainedWidth: CGFloat, _ context: AccountContext, _ theme: PresentationTheme, _ threadId: Int64, _ title: NSAttributedString, _ iconId: Int64?, _ iconColor: Int32?) -> (CGSize, () -> TopicItemNode) { static func asyncLayout(_ currentNode: TopicItemNode?) -> (_ constrainedWidth: CGFloat, _ context: AccountContext, _ theme: PresentationTheme, _ threadId: Int64, _ threadPeer: EnginePeer?, _ title: NSAttributedString, _ iconId: Int64?, _ iconColor: Int32?) -> (CGSize, () -> TopicItemNode) {
let makeTopicTitleLayout = TextNode.asyncLayout(currentNode?.topicTitleNode) let makeTopicTitleLayout = TextNode.asyncLayout(currentNode?.topicTitleNode)
return { constrainedWidth, context, theme, threadId, title, iconId, iconColor in return { constrainedWidth, context, theme, threadId, threadPeer, title, iconId, iconColor in
let remainingWidth = max(1.0, constrainedWidth - (((iconId == nil && iconColor == nil) ? 1.0 : 18.0) + 2.0)) let remainingWidth = max(1.0, constrainedWidth - (((iconId == nil && iconColor == nil && threadPeer == nil) ? 1.0 : 18.0) + 2.0))
let topicTitleArguments = TextNodeLayoutArguments(attributedString: 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 topicTitleArguments = TextNodeLayoutArguments(attributedString: 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 = makeTopicTitleLayout(topicTitleArguments) let topicTitleLayout = makeTopicTitleLayout(topicTitleArguments)
return (CGSize(width: ((iconId == nil && iconColor == nil) ? 1.0 : 18.0) + 2.0 + topicTitleLayout.0.size.width, height: topicTitleLayout.0.size.height), { return (CGSize(width: ((iconId == nil && iconColor == nil && threadPeer == nil) ? 1.0 : 18.0) + 2.0 + topicTitleLayout.0.size.width, height: topicTitleLayout.0.size.height), {
let topicTitleNode = topicTitleLayout.1() let topicTitleNode = topicTitleLayout.1()
let titleTopicIconContent: EmojiStatusComponent.Content? let titleTopicIconContent: EmojiStatusComponent.Content?
@ -1068,7 +1074,16 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} }
} }
let targetNode = currentNode ?? TopicItemNode(topicTitleNode: topicTitleNode, titleTopicIconView: titleTopicIconView, titleTopicIconComponent: titleTopicIconComponent) var titleTopicAvatarNode: AvatarNode?
if let _ = threadPeer {
if let current = currentNode?.titleTopicAvatarNode {
titleTopicAvatarNode = current
} else {
titleTopicAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
}
}
let targetNode = currentNode ?? TopicItemNode(topicTitleNode: topicTitleNode, titleTopicIconView: titleTopicIconView, titleTopicAvatarNode: titleTopicAvatarNode, titleTopicIconComponent: titleTopicIconComponent)
targetNode.titleTopicIconComponent = titleTopicIconComponent targetNode.titleTopicIconComponent = titleTopicIconComponent
@ -1081,6 +1096,18 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
) )
titleTopicIconView.frame = CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: iconSize) titleTopicIconView.frame = CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: iconSize)
topicTitleNode.frame = CGRect(origin: CGPoint(x: 18.0 + 2.0, y: 0.0), size: topicTitleLayout.0.size)
} else if let titleTopicAvatarNode, let threadPeer {
let iconSize = CGSize(width: 18.0, height: 18.0)
titleTopicAvatarNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: iconSize)
titleTopicAvatarNode.updateSize(size: iconSize)
if threadPeer.smallProfileImage != nil {
titleTopicAvatarNode.setPeerV2(context: context, theme: theme, peer: threadPeer, overrideImage: nil, emptyColor: theme.list.mediaPlaceholderColor, clipStyle: .round, synchronousLoad: false, displayDimensions: iconSize)
} else {
titleTopicAvatarNode.setPeer(context: context, theme: theme, peer: threadPeer, overrideImage: nil, emptyColor: theme.list.mediaPlaceholderColor, clipStyle: .round, synchronousLoad: false, displayDimensions: iconSize)
}
topicTitleNode.frame = CGRect(origin: CGPoint(x: 18.0 + 2.0, y: 0.0), size: topicTitleLayout.0.size) topicTitleNode.frame = CGRect(origin: CGPoint(x: 18.0 + 2.0, y: 0.0), size: topicTitleLayout.0.size)
} else { } else {
topicTitleNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: topicTitleLayout.0.size) topicTitleNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: topicTitleLayout.0.size)
@ -1144,9 +1171,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} }
} }
func asyncLayout() -> (_ context: AccountContext, _ constrainedWidth: CGFloat, _ theme: PresentationTheme, _ authorTitle: NSAttributedString?, _ topics: [(id: Int64, title: NSAttributedString, iconId: Int64?, iconColor: Int32?)]) -> (CGSize, () -> CGRect?) { func asyncLayout() -> (_ context: AccountContext, _ constrainedWidth: CGFloat, _ theme: PresentationTheme, _ authorTitle: NSAttributedString?, _ topics: [(id: Int64, threadPeer: EnginePeer?, title: NSAttributedString, iconId: Int64?, iconColor: Int32?)]) -> (CGSize, () -> CGRect?) {
let makeAuthorLayout = TextNode.asyncLayout(self.authorNode) let makeAuthorLayout = TextNode.asyncLayout(self.authorNode)
var makeExistingTopicLayouts: [Int64: (_ constrainedWidth: CGFloat, _ context: AccountContext, _ theme: PresentationTheme, _ threadId: Int64, _ title: NSAttributedString, _ iconId: Int64?, _ iconColor: Int32?) -> (CGSize, () -> TopicItemNode)] = [:] var makeExistingTopicLayouts: [Int64: (_ constrainedWidth: CGFloat, _ context: AccountContext, _ theme: PresentationTheme, _ threadId: Int64, _ threadPeer: EnginePeer?, _ title: NSAttributedString, _ iconId: Int64?, _ iconColor: Int32?) -> (CGSize, () -> TopicItemNode)] = [:]
for (topicId, topicNode) in self.topicNodes { for (topicId, topicNode) in self.topicNodes {
makeExistingTopicLayouts[topicId] = TopicItemNode.asyncLayout(topicNode) makeExistingTopicLayouts[topicId] = TopicItemNode.asyncLayout(topicNode)
} }
@ -1178,7 +1205,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} }
let makeTopicLayout = makeExistingTopicLayouts[topic.id] ?? TopicItemNode.asyncLayout(nil) let makeTopicLayout = makeExistingTopicLayouts[topic.id] ?? TopicItemNode.asyncLayout(nil)
let (topicSize, topicApply) = makeTopicLayout(remainingWidth, context, theme, topic.id, topic.title, topic.iconId, topic.iconColor) let (topicSize, topicApply) = makeTopicLayout(remainingWidth, context, theme, topic.id, topic.threadPeer, topic.title, topic.iconId, topic.iconColor)
topicsSizeAndApply.append((topic.id, topicSize, topicApply)) topicsSizeAndApply.append((topic.id, topicSize, topicApply))
remainingWidth -= topicSize.width + 4.0 remainingWidth -= topicSize.width + 4.0
@ -3337,17 +3364,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let isSearching = item.interaction.searchTextHighightState != nil let isSearching = item.interaction.searchTextHighightState != nil
var isFirstForumThreadSelectable = false var isFirstForumThreadSelectable = false
var forumThreads: [(id: Int64, title: NSAttributedString, iconId: Int64?, iconColor: Int32?)] = [] var forumThreads: [(id: Int64, threadPeer: EnginePeer?, title: NSAttributedString, iconId: Int64?, iconColor: Int32?)] = []
if case .savedMessagesChats = item.chatListLocation { if case .savedMessagesChats = item.chatListLocation {
} else if case let .peer(peer) = item.content, case let .channel(channel) = peer.peer.peer, channel.flags.contains(.isMonoforum) { } else if case let .peer(peer) = item.content, case let .channel(channel) = peer.peer.peer, channel.flags.contains(.isMonoforum) {
if forumThread != nil || !topForumTopicItems.isEmpty { if forumThread != nil || !topForumTopicItems.isEmpty {
if let forumThread { if let forumThread {
isFirstForumThreadSelectable = forumThread.isUnread isFirstForumThreadSelectable = forumThread.isUnread
forumThreads.append((id: forumThread.id, title: NSAttributedString(string: forumThread.threadPeer?.compactDisplayTitle ?? " ", font: textFont, textColor: forumThread.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: nil, iconColor: nil)) forumThreads.append((id: forumThread.id, threadPeer: forumThread.threadPeer, title: NSAttributedString(string: forumThread.threadPeer?.compactDisplayTitle ?? " ", font: textFont, textColor: forumThread.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: nil, iconColor: nil))
} }
for topicItem in topForumTopicItems { for topicItem in topForumTopicItems {
if forumThread?.id != topicItem.id { if forumThread?.id != topicItem.id {
forumThreads.append((id: topicItem.id, title: NSAttributedString(string: topicItem.threadPeer?.compactDisplayTitle ?? " ", font: textFont, textColor: topicItem.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: nil, iconColor: nil)) forumThreads.append((id: topicItem.id, threadPeer: topicItem.threadPeer, title: NSAttributedString(string: topicItem.threadPeer?.compactDisplayTitle ?? " ", font: textFont, textColor: topicItem.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: nil, iconColor: nil))
} }
} }
@ -3364,13 +3391,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else if forumThread != nil || !topForumTopicItems.isEmpty { } else if forumThread != nil || !topForumTopicItems.isEmpty {
if let forumThread = forumThread { if let forumThread = forumThread {
isFirstForumThreadSelectable = forumThread.isUnread isFirstForumThreadSelectable = forumThread.isUnread
forumThreads.append((id: forumThread.id, title: NSAttributedString(string: forumThread.title, font: textFont, textColor: forumThread.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: forumThread.iconId, iconColor: forumThread.iconColor)) forumThreads.append((id: forumThread.id, threadPeer: forumThread.threadPeer, title: NSAttributedString(string: forumThread.title, font: textFont, textColor: forumThread.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: forumThread.iconId, iconColor: forumThread.iconColor))
} }
for topicItem in topForumTopicItems { for topicItem in topForumTopicItems {
if case let .peer(peer) = item.content, peer.peer.peerId.id._internalGetInt64Value() == topicItem.id { if case let .peer(peer) = item.content, peer.peer.peerId.id._internalGetInt64Value() == topicItem.id {
} else if forumThread?.id != topicItem.id { } else if forumThread?.id != topicItem.id {
forumThreads.append((id: topicItem.id, title: NSAttributedString(string: topicItem.title, font: textFont, textColor: topicItem.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: topicItem.iconFileId, iconColor: topicItem.iconColor)) forumThreads.append((id: topicItem.id, threadPeer: topicItem.threadPeer, title: NSAttributedString(string: topicItem.title, font: textFont, textColor: topicItem.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: topicItem.iconFileId, iconColor: topicItem.iconColor))
} }
} }

View File

@ -96,7 +96,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case enableDebugDataDisplay(Bool) case enableDebugDataDisplay(Bool)
case rippleEffect(Bool) case rippleEffect(Bool)
case browserExperiment(Bool) case browserExperiment(Bool)
case localTranscription(Bool) case allForumsHaveTabs(Bool)
case enableReactionOverrides(Bool) case enableReactionOverrides(Bool)
case compressedEmojiCache(Bool) case compressedEmojiCache(Bool)
case storiesJpegExperiment(Bool) case storiesJpegExperiment(Bool)
@ -133,7 +133,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.web.rawValue return DebugControllerSection.web.rawValue
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
return DebugControllerSection.experiments.rawValue return DebugControllerSection.experiments.rawValue
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation: case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .allForumsHaveTabs, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation:
return DebugControllerSection.experiments.rawValue return DebugControllerSection.experiments.rawValue
case .logTranslationRecognition, .resetTranslationStates: case .logTranslationRecognition, .resetTranslationStates:
return DebugControllerSection.translation.rawValue return DebugControllerSection.translation.rawValue
@ -226,7 +226,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 39 return 39
case .browserExperiment: case .browserExperiment:
return 40 return 40
case .localTranscription: case .allForumsHaveTabs:
return 41 return 41
case .enableReactionOverrides: case .enableReactionOverrides:
return 42 return 42
@ -1264,12 +1264,12 @@ private enum DebugControllerEntry: ItemListNodeEntry {
}) })
}).start() }).start()
}) })
case let .localTranscription(value): case let .allForumsHaveTabs(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Local Transcription", value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, title: "Forum Tabs Debug", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
settings.localTranscription = value settings.allForumsHaveTabs = value
return PreferencesEntry(settings) return PreferencesEntry(settings)
}) })
}).start() }).start()
@ -1526,7 +1526,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
entries.append(.browserExperiment(experimentalSettings.browserExperiment)) entries.append(.browserExperiment(experimentalSettings.browserExperiment))
} }
#endif #endif
entries.append(.localTranscription(experimentalSettings.localTranscription)) entries.append(.allForumsHaveTabs(experimentalSettings.allForumsHaveTabs))
if case .internal = sharedContext.applicationBindings.appBuildType { if case .internal = sharedContext.applicationBindings.appBuildType {
entries.append(.enableReactionOverrides(experimentalSettings.enableReactionOverrides)) entries.append(.enableReactionOverrides(experimentalSettings.enableReactionOverrides))
} }

View File

@ -862,6 +862,10 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
} }
} }
public func resetScrolledToItem() {
self.scrolledToItem = nil
}
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if let shouldStopScrolling = self.shouldStopScrolling, shouldStopScrolling(velocity.y) { if let shouldStopScrolling = self.shouldStopScrolling, shouldStopScrolling(velocity.y) {
targetContentOffset.pointee.y = scrollView.contentOffset.y targetContentOffset.pointee.y = scrollView.contentOffset.y

View File

@ -65,7 +65,11 @@ private func mappedChatListFilterPredicate(postbox: PostboxImpl, currentTransact
if let cachedPeerData = postbox.cachedPeerDataTable.get(peer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) { if let cachedPeerData = postbox.cachedPeerDataTable.get(peer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true displayAsRegularChat = true
} }
if postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value && !displayAsRegularChat { let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value
if !isThreadBased {
displayAsRegularChat = true
}
if isThreadBased && !displayAsRegularChat {
isUnread = (postbox.peerThreadsSummaryTable.get(peerId: peer.id)?.effectiveUnreadCount ?? 0) > 0 isUnread = (postbox.peerThreadsSummaryTable.get(peerId: peer.id)?.effectiveUnreadCount ?? 0) > 0
} else { } else {
isUnread = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false isUnread = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false
@ -435,13 +439,17 @@ private final class ChatListViewSpaceState {
if let cachedPeerData = postbox.cachedPeerDataTable.get(peer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) { if let cachedPeerData = postbox.cachedPeerDataTable.get(peer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true displayAsRegularChat = true
} }
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value
if !isThreadBased {
displayAsRegularChat = true
}
let isRemovedFromTotalUnreadCount = resolvedIsRemovedFromTotalUnreadCount(globalSettings: globalNotificationSettingsValue, peer: peer, peerSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId)) let isRemovedFromTotalUnreadCount = resolvedIsRemovedFromTotalUnreadCount(globalSettings: globalNotificationSettingsValue, peer: peer, peerSettings: postbox.peerNotificationSettingsTable.getEffective(notificationsPeerId))
let messageTagSummaryResult = resolveChatListMessageTagSummaryResultCalculation(postbox: postbox, peerId: peer.id, threadId: nil, calculation: filterPredicate.messageTagSummary) let messageTagSummaryResult = resolveChatListMessageTagSummaryResultCalculation(postbox: postbox, peerId: peer.id, threadId: nil, calculation: filterPredicate.messageTagSummary)
var isUnread: Bool var isUnread: Bool
if postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value && !displayAsRegularChat { if isThreadBased && !displayAsRegularChat {
isUnread = (postbox.peerThreadsSummaryTable.get(peerId: peer.id)?.effectiveUnreadCount ?? 0) > 0 isUnread = (postbox.peerThreadsSummaryTable.get(peerId: peer.id)?.effectiveUnreadCount ?? 0) > 0
} else { } else {
isUnread = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false isUnread = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false
@ -569,9 +577,13 @@ private final class ChatListViewSpaceState {
if let cachedPeerData = postbox.cachedPeerDataTable.get(entryPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) { if let cachedPeerData = postbox.cachedPeerDataTable.get(entryPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true displayAsRegularChat = true
} }
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(entryPeer).value
if !isThreadBased {
displayAsRegularChat = true
}
var isUnread: Bool var isUnread: Bool
if postbox.seedConfiguration.peerSummaryIsThreadBased(entryPeer).value && !displayAsRegularChat { if isThreadBased && !displayAsRegularChat {
isUnread = (postbox.peerThreadsSummaryTable.get(peerId: entryPeer.id)?.effectiveUnreadCount ?? 0) > 0 isUnread = (postbox.peerThreadsSummaryTable.get(peerId: entryPeer.id)?.effectiveUnreadCount ?? 0) > 0
} else { } else {
isUnread = postbox.readStateTable.getCombinedState(entryPeer.id)?.isUnread ?? false isUnread = postbox.readStateTable.getCombinedState(entryPeer.id)?.isUnread ?? false
@ -618,9 +630,13 @@ private final class ChatListViewSpaceState {
if let cachedPeerData = postbox.cachedPeerDataTable.get(mainPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) { if let cachedPeerData = postbox.cachedPeerDataTable.get(mainPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true displayAsRegularChat = true
} }
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(mainPeer).value
if !isThreadBased {
displayAsRegularChat = true
}
var isUnread: Bool var isUnread: Bool
if postbox.seedConfiguration.peerSummaryIsThreadBased(mainPeer).value && !displayAsRegularChat { if isThreadBased && !displayAsRegularChat {
isUnread = (postbox.peerThreadsSummaryTable.get(peerId: peerId)?.effectiveUnreadCount ?? 0) > 0 isUnread = (postbox.peerThreadsSummaryTable.get(peerId: peerId)?.effectiveUnreadCount ?? 0) > 0
} else { } else {
isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
@ -800,9 +816,13 @@ private final class ChatListViewSpaceState {
if let cachedPeerData = postbox.cachedPeerDataTable.get(entryPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) { if let cachedPeerData = postbox.cachedPeerDataTable.get(entryPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true displayAsRegularChat = true
} }
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(entryPeer).value
if !isThreadBased {
displayAsRegularChat = true
}
var isUnread: Bool var isUnread: Bool
if postbox.seedConfiguration.peerSummaryIsThreadBased(entryPeer).value && !displayAsRegularChat { if isThreadBased && !displayAsRegularChat {
isUnread = (postbox.peerThreadsSummaryTable.get(peerId: entryPeer.id)?.effectiveUnreadCount ?? 0) > 0 isUnread = (postbox.peerThreadsSummaryTable.get(peerId: entryPeer.id)?.effectiveUnreadCount ?? 0) > 0
} else { } else {
isUnread = postbox.readStateTable.getCombinedState(entryPeer.id)?.isUnread ?? false isUnread = postbox.readStateTable.getCombinedState(entryPeer.id)?.isUnread ?? false
@ -858,9 +878,13 @@ private final class ChatListViewSpaceState {
if let cachedPeerData = postbox.cachedPeerDataTable.get(mainPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) { if let cachedPeerData = postbox.cachedPeerDataTable.get(mainPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true displayAsRegularChat = true
} }
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(mainPeer).value
if !isThreadBased {
displayAsRegularChat = true
}
var isUnread: Bool var isUnread: Bool
if postbox.seedConfiguration.peerSummaryIsThreadBased(mainPeer).value && !displayAsRegularChat { if isThreadBased && !displayAsRegularChat {
isUnread = (postbox.peerThreadsSummaryTable.get(peerId: peerId)?.effectiveUnreadCount ?? 0) > 0 isUnread = (postbox.peerThreadsSummaryTable.get(peerId: peerId)?.effectiveUnreadCount ?? 0) > 0
} else { } else {
isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false
@ -952,10 +976,19 @@ private final class ChatListViewSpaceState {
if let cachedPeerData = postbox.cachedPeerDataTable.get(entryData.index.messageIndex.id.peerId), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) { if let cachedPeerData = postbox.cachedPeerDataTable.get(entryData.index.messageIndex.id.peerId), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true displayAsRegularChat = true
} }
let isThreadBased: Bool
if let peer = postbox.peerTable.get(entryData.index.messageIndex.id.peerId) {
isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value
} else {
isThreadBased = false
}
if !isThreadBased {
displayAsRegularChat = true
}
var updatedReadState = entryData.readState var updatedReadState = entryData.readState
if let peer = postbox.peerTable.get(entryData.index.messageIndex.id.peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value, !displayAsRegularChat { if isThreadBased, !displayAsRegularChat {
let summary = postbox.peerThreadsSummaryTable.get(peerId: peer.id) let summary = postbox.peerThreadsSummaryTable.get(peerId: entryData.index.messageIndex.id.peerId)
var count: Int32 = 0 var count: Int32 = 0
var isMuted: Bool = false var isMuted: Bool = false
@ -1605,6 +1638,9 @@ struct ChatListViewState {
autoremoveTimeout = postbox.seedConfiguration.decodeAutoremoveTimeout(cachedData) autoremoveTimeout = postbox.seedConfiguration.decodeAutoremoveTimeout(cachedData)
displayAsRegularChat = postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedData) displayAsRegularChat = postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedData)
} }
if !isThreadBased {
displayAsRegularChat = true
}
var topForumTopics: [ChatListForumTopicData] = [] var topForumTopics: [ChatListForumTopicData] = []
let readState: ChatListViewReadState? let readState: ChatListViewReadState?

View File

@ -247,7 +247,7 @@ func _internal_createForumChannelTopic(account: Account, peerId: PeerId, title:
} }
|> castError(CreateForumChannelTopicError.self) |> castError(CreateForumChannelTopicError.self)
|> mapToSignal { _ -> Signal<Int64, CreateForumChannelTopicError> in |> mapToSignal { _ -> Signal<Int64, CreateForumChannelTopicError> in
return resolveForumThreads(accountPeerId: account.peerId, postbox: account.postbox, source: .network(account.network), ids: [PeerAndBoundThreadId(peerId: peerId, threadId: topicId)]) return resolveForumThreads(accountPeerId: account.peerId, postbox: account.postbox, source: .network(account.network), additionalPeers: AccumulatedPeers(), ids: [PeerAndBoundThreadId(peerId: peerId, threadId: topicId)])
|> castError(CreateForumChannelTopicError.self) |> castError(CreateForumChannelTopicError.self)
|> map { _ -> Int64 in |> map { _ -> Int64 in
return topicId return topicId
@ -277,7 +277,7 @@ func _internal_fetchForumChannelTopic(account: Account, peerId: PeerId, threadId
if let info = info { if let info = info {
return .single(.result(info)) return .single(.result(info))
} else { } else {
return .single(.progress) |> then(resolveForumThreads(accountPeerId: account.peerId, postbox: account.postbox, source: .network(account.network), ids: [PeerAndBoundThreadId(peerId: peerId, threadId: threadId)]) return .single(.progress) |> then(resolveForumThreads(accountPeerId: account.peerId, postbox: account.postbox, source: .network(account.network), additionalPeers: AccumulatedPeers(), ids: [PeerAndBoundThreadId(peerId: peerId, threadId: threadId)])
|> mapToSignal { _ -> Signal<FetchForumChannelTopicResult, NoError> in |> mapToSignal { _ -> Signal<FetchForumChannelTopicResult, NoError> in
return account.postbox.transaction { transaction -> FetchForumChannelTopicResult in return account.postbox.transaction { transaction -> FetchForumChannelTopicResult in
if let data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { if let data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {

View File

@ -2019,7 +2019,16 @@ func resolveForumThreads(accountPeerId: PeerId, postbox: Postbox, source: FetchM
} }
if peer.flags.contains(.isMonoforum) { if peer.flags.contains(.isMonoforum) {
let signal = source.request(Api.functions.messages.getSavedDialogsByID(flags: 1 << 1, parentPeer: inputPeer, ids: threadIds.compactMap { transaction.getPeer(PeerId($0)).flatMap(apiInputPeer(_:)) })) let signal = source.request(Api.functions.messages.getSavedDialogsByID(flags: 1 << 1, parentPeer: inputPeer, ids: threadIds.compactMap { threadId in
let threadPeerId = PeerId(threadId)
if let threadPeer = state.peers[threadPeerId] {
return apiInputPeer(threadPeer)
} else if let threadPeer = transaction.getPeer(threadPeerId) {
return apiInputPeer(threadPeer)
} else {
return nil
}
}))
|> map { result -> (Peer, FetchedForumThreads)? in |> map { result -> (Peer, FetchedForumThreads)? in
let result = FetchedForumThreads(savedDialogs: result) let result = FetchedForumThreads(savedDialogs: result)
return (peer, result) return (peer, result)
@ -2146,7 +2155,7 @@ func resolveForumThreads(accountPeerId: PeerId, postbox: Postbox, source: FetchM
} }
} }
func resolveForumThreads(accountPeerId: PeerId, postbox: Postbox, source: FetchMessageHistoryHoleSource, ids: [PeerAndBoundThreadId]) -> Signal<Void, NoError> { func resolveForumThreads(accountPeerId: PeerId, postbox: Postbox, source: FetchMessageHistoryHoleSource, additionalPeers: AccumulatedPeers, ids: [PeerAndBoundThreadId]) -> Signal<Void, NoError> {
let forumThreadIds = Set(ids) let forumThreadIds = Set(ids)
if forumThreadIds.isEmpty { if forumThreadIds.isEmpty {
@ -2172,7 +2181,16 @@ func resolveForumThreads(accountPeerId: PeerId, postbox: Postbox, source: FetchM
} }
if peer.flags.contains(.isMonoforum) { if peer.flags.contains(.isMonoforum) {
let signal = source.request(Api.functions.messages.getSavedDialogsByID(flags: 1 << 1, parentPeer: inputPeer, ids: threadIds.compactMap { transaction.getPeer(PeerId($0)).flatMap(apiInputPeer(_:)) })) let signal = source.request(Api.functions.messages.getSavedDialogsByID(flags: 1 << 1, parentPeer: inputPeer, ids: threadIds.compactMap { threadId in
let threadPeerId = PeerId(threadId)
if let threadPeer = additionalPeers.get(threadPeerId) {
return apiInputPeer(threadPeer)
} else if let threadPeer = transaction.getPeer(threadPeerId) {
return apiInputPeer(threadPeer)
} else {
return nil
}
}))
|> map { result -> (Peer, FetchedForumThreads)? in |> map { result -> (Peer, FetchedForumThreads)? in
let result = FetchedForumThreads(savedDialogs: result) let result = FetchedForumThreads(savedDialogs: result)
return (peer, result) return (peer, result)
@ -2329,7 +2347,16 @@ func resolveForumThreads(accountPeerId: PeerId, postbox: Postbox, source: FetchM
} }
if peer.flags.contains(.isMonoforum) { if peer.flags.contains(.isMonoforum) {
let signal = source.request(Api.functions.messages.getSavedDialogsByID(flags: 1 << 1, parentPeer: inputPeer, ids: threadIds.compactMap { transaction.getPeer(PeerId($0)).flatMap(apiInputPeer(_:)) })) let signal = source.request(Api.functions.messages.getSavedDialogsByID(flags: 1 << 1, parentPeer: inputPeer, ids: threadIds.compactMap { threadId in
let threadPeerId = PeerId(threadId)
if let threadPeer = fetchedChatList.peers.get(threadPeerId) {
return apiInputPeer(threadPeer)
} else if let threadPeer = transaction.getPeer(threadPeerId) {
return apiInputPeer(threadPeer)
} else {
return nil
}
}))
|> map { result -> (Peer, FetchedForumThreads)? in |> map { result -> (Peer, FetchedForumThreads)? in
let result = FetchedForumThreads(savedDialogs: result) let result = FetchedForumThreads(savedDialogs: result)
return (peer, result) return (peer, result)

View File

@ -225,7 +225,7 @@ func withResolvedAssociatedMessages<T>(postbox: Postbox, source: FetchMessageHis
return resolveAssociatedStories(postbox: postbox, source: source, accountPeerId: accountPeerId, messages: storeMessages, additionalPeers: parsedPeers, result: Void()) return resolveAssociatedStories(postbox: postbox, source: source, accountPeerId: accountPeerId, messages: storeMessages, additionalPeers: parsedPeers, result: Void())
|> mapToSignal { _ -> Signal<T, NoError> in |> mapToSignal { _ -> Signal<T, NoError> in
if resolveThreads && !threadIds.isEmpty { if resolveThreads && !threadIds.isEmpty {
return resolveForumThreads(accountPeerId: accountPeerId, postbox: postbox, source: source, ids: Array(threadIds)) return resolveForumThreads(accountPeerId: accountPeerId, postbox: postbox, source: source, additionalPeers: parsedPeers, ids: Array(threadIds))
|> mapToSignal { _ -> Signal<T, NoError> in |> mapToSignal { _ -> Signal<T, NoError> in
return postbox.transaction { transaction -> T in return postbox.transaction { transaction -> T in
return f(transaction, parsedPeers, []) return f(transaction, parsedPeers, [])
@ -325,7 +325,8 @@ func withResolvedAssociatedMessages<T>(postbox: Postbox, source: FetchMessageHis
let combinedMessages = storeMessages + additionalMessages let combinedMessages = storeMessages + additionalMessages
return resolveUnknownEmojiFiles(postbox: postbox, source: source, messages: combinedMessages, reactions: [], result: Void()) return resolveUnknownEmojiFiles(postbox: postbox, source: source, messages: combinedMessages, reactions: [], result: Void())
|> mapToSignal { _ -> Signal<T, NoError> in |> mapToSignal { _ -> Signal<T, NoError> in
return resolveAssociatedStories(postbox: postbox, source: source, accountPeerId: accountPeerId, messages: storeMessages + additionalMessages, additionalPeers: parsedPeers.union(with: additionalPeers), result: Void()) let additionalPeers = parsedPeers.union(with: additionalPeers)
return resolveAssociatedStories(postbox: postbox, source: source, accountPeerId: accountPeerId, messages: storeMessages + additionalMessages, additionalPeers: additionalPeers, result: Void())
|> mapToSignal { _ -> Signal<T, NoError> in |> mapToSignal { _ -> Signal<T, NoError> in
var threadIds = Set<PeerAndBoundThreadId>() var threadIds = Set<PeerAndBoundThreadId>()
for message in combinedMessages { for message in combinedMessages {
@ -335,7 +336,7 @@ func withResolvedAssociatedMessages<T>(postbox: Postbox, source: FetchMessageHis
} }
if resolveThreads && !threadIds.isEmpty { if resolveThreads && !threadIds.isEmpty {
return resolveForumThreads(accountPeerId: accountPeerId, postbox: postbox, source: source, ids: Array(threadIds)) return resolveForumThreads(accountPeerId: accountPeerId, postbox: postbox, source: source, additionalPeers: additionalPeers, ids: Array(threadIds))
|> mapToSignal { _ -> Signal<T, NoError> in |> mapToSignal { _ -> Signal<T, NoError> in
return postbox.transaction { transaction -> T in return postbox.transaction { transaction -> T in
return f(transaction, parsedPeers, []) return f(transaction, parsedPeers, [])

View File

@ -76,7 +76,11 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
peerSummaryIsThreadBased: { peer in peerSummaryIsThreadBased: { peer in
if let channel = peer as? TelegramChannel { if let channel = peer as? TelegramChannel {
if channel.flags.contains(.isForum) { if channel.flags.contains(.isForum) {
return (true, false) if channel.flags.contains(.displayForumAsTabs) {
return (false, false)
} else {
return (true, false)
}
} else if channel.flags.contains(.isMonoforum) { } else if channel.flags.contains(.isMonoforum) {
return (true, true) return (true, true)
} else { } else {

View File

@ -208,7 +208,9 @@ func _internal_togglePeerUnreadMarkInteractively(transaction: Transaction, netwo
} }
var displayAsRegularChat: Bool = false var displayAsRegularChat: Bool = false
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData { if let channel = peer as? TelegramChannel, channel.flags.contains(.displayForumAsTabs) {
displayAsRegularChat = true
} else if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
displayAsRegularChat = cachedData.viewForumAsMessages.knownValue ?? false displayAsRegularChat = cachedData.viewForumAsMessages.knownValue ?? false
} }

View File

@ -125,8 +125,12 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
let compactAuthorName = message.author?.compactDisplayTitle ?? "" let compactAuthorName = message.author?.compactDisplayTitle ?? ""
var isChannel = false var isChannel = false
if message.id.peerId.namespace == Namespaces.Peer.CloudChannel, let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { var isMonoforum = false
isChannel = true if message.id.peerId.namespace == Namespaces.Peer.CloudChannel, let peer = message.peers[message.id.peerId] as? TelegramChannel {
if case .broadcast = peer.info {
isChannel = true
}
isMonoforum = peer.isMonoForum
} }
switch action.action { switch action.action {
@ -135,7 +139,12 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = NSAttributedString(string: strings.Notification_CreatedChannel, font: titleFont, textColor: primaryTextColor) attributedString = NSAttributedString(string: strings.Notification_CreatedChannel, font: titleFont, textColor: primaryTextColor)
} else { } else {
if forChatList { if forChatList {
attributedString = NSAttributedString(string: strings.Notification_CreatedGroup, font: titleFont, textColor: primaryTextColor) if isMonoforum {
//TODO:Localize
attributedString = NSAttributedString(string: "No messages here yet...", font: titleFont, textColor: primaryTextColor)
} else {
attributedString = NSAttributedString(string: strings.Notification_CreatedGroup, font: titleFont, textColor: primaryTextColor)
}
} else { } else {
attributedString = addAttributesToStringWithRanges(strings.Notification_CreatedChatWithTitle(authorName, title)._tuple, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) attributedString = addAttributesToStringWithRanges(strings.Notification_CreatedChatWithTitle(authorName, title)._tuple, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]))
} }

View File

@ -26,7 +26,7 @@ import LottieComponent
import BundleIconComponent import BundleIconComponent
private protocol ChatEmptyNodeContent { private protocol ChatEmptyNodeContent {
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize
} }
private let titleFont = Font.semibold(15.0) private let titleFont = Font.semibold(15.0)
@ -46,7 +46,7 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod
self.addSubnode(self.textNode) self.addSubnode(self.textNode)
} }
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme self.currentTheme = interfaceState.theme
self.currentStrings = interfaceState.strings self.currentStrings = interfaceState.strings
@ -155,7 +155,7 @@ public final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNod
let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file._parse()), false, self.view, self.stickerNode.bounds, nil, []) let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file._parse()), false, self.view, self.stickerNode.bounds, nil, [])
} }
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
let isFirstTime = self.currentTheme == nil let isFirstTime = self.currentTheme == nil
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
@ -363,7 +363,7 @@ public final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeS
let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file._parse()), false, self.view, self.stickerNode.bounds, nil, []) let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file._parse()), false, self.view, self.stickerNode.bounds, nil, [])
} }
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme self.currentTheme = interfaceState.theme
self.currentStrings = interfaceState.strings self.currentStrings = interfaceState.strings
@ -496,7 +496,7 @@ private final class ChatEmptyNodeSecretChatContent: ASDisplayNode, ChatEmptyNode
self.addSubnode(self.subtitleNode) self.addSubnode(self.subtitleNode)
} }
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme self.currentTheme = interfaceState.theme
self.currentStrings = interfaceState.strings self.currentStrings = interfaceState.strings
@ -630,7 +630,7 @@ private final class ChatEmptyNodeGroupChatContent: ASDisplayNode, ChatEmptyNodeC
self.addSubnode(self.subtitleNode) self.addSubnode(self.subtitleNode)
} }
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme self.currentTheme = interfaceState.theme
self.currentStrings = interfaceState.strings self.currentStrings = interfaceState.strings
@ -759,7 +759,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
self.shareBusinessLink?(businessLink.url) self.shareBusinessLink?(businessLink.url)
} }
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
var maxWidth: CGFloat = size.width var maxWidth: CGFloat = size.width
var centerText = false var centerText = false
@ -1125,7 +1125,7 @@ public final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeCo
self.addSubnode(self.textNode) self.addSubnode(self.textNode)
} }
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme self.currentTheme = interfaceState.theme
@ -1266,7 +1266,7 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE
} }
} }
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
let maxWidth = min(270.0, size.width) let maxWidth = min(270.0, size.width)
@ -1778,7 +1778,7 @@ public final class ChatEmptyNode: ASDisplayNode {
} }
} }
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: Subject, loadingNode: ChatLoadingNode?, backgroundNode: WallpaperBackgroundNode?, size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: Subject, loadingNode: ChatLoadingNode?, backgroundNode: WallpaperBackgroundNode?, size: CGSize, insets: UIEdgeInsets, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.wallpaperBackgroundNode = backgroundNode self.wallpaperBackgroundNode = backgroundNode
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
@ -1816,7 +1816,7 @@ public final class ChatEmptyNode: ASDisplayNode {
contentType = .secret contentType = .secret
} else if let group = peer as? TelegramGroup, case .creator = group.role { } else if let group = peer as? TelegramGroup, case .creator = group.role {
contentType = .group contentType = .group
} else if let channel = peer as? TelegramChannel, case .group = channel.info, channel.flags.contains(.isCreator) && !channel.flags.contains(.isGigagroup) { } else if let channel = peer as? TelegramChannel, case .group = channel.info, channel.flags.contains(.isCreator) && !channel.flags.contains(.isGigagroup) && !channel.isMonoForum {
contentType = .group contentType = .group
} else if let _ = interfaceState.peerNearbyData { } else if let _ = interfaceState.peerNearbyData {
contentType = .peerNearby contentType = .peerNearby
@ -1835,6 +1835,8 @@ public final class ChatEmptyNode: ASDisplayNode {
} }
} }
} }
} else if let channel = peer as? TelegramChannel, channel.isMonoForum, let sendPaidMessageStars = interfaceState.sendPaidMessageStars {
contentType = .starsRequired(sendPaidMessageStars.value)
} else { } else {
contentType = .regular contentType = .regular
} }
@ -1910,14 +1912,14 @@ public final class ChatEmptyNode: ASDisplayNode {
var contentSize = CGSize() var contentSize = CGSize()
if let contentNode = self.content?.1 { if let contentNode = self.content?.1 {
contentSize = contentNode.updateLayout(interfaceState: interfaceState, subject: subject, size: displayRect.size, transition: contentTransition) contentSize = contentNode.updateLayout(interfaceState: interfaceState, subject: subject, size: displayRect.size, leftInset: leftInset, rightInset: rightInset, transition: contentTransition)
if updateGreetingSticker { if updateGreetingSticker {
self.context.prefetchManager?.prepareNextGreetingSticker() self.context.prefetchManager?.prepareNextGreetingSticker()
} }
} }
let contentFrame = CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - contentSize.width) / 2.0), y: displayRect.minY + floor((displayRect.height - contentSize.height) / 2.0)), size: contentSize) let contentFrame = CGRect(origin: CGPoint(x: displayRect.minX + leftInset + floor((displayRect.width - leftInset - rightInset - contentSize.width) / 2.0), y: displayRect.minY + floor((displayRect.height - contentSize.height) / 2.0)), size: contentSize)
if let contentNode = self.content?.1 { if let contentNode = self.content?.1 {
contentTransition.updateFrame(node: contentNode, frame: contentFrame) contentTransition.updateFrame(node: contentNode, frame: contentFrame)
} }
@ -1978,7 +1980,7 @@ public final class ChatEmptyNode: ASDisplayNode {
wallpaperBackgroundNode: backgroundNode, wallpaperBackgroundNode: backgroundNode,
constrainedSize: CGSize(width: size.width - insets.left - insets.right, height: 200.0) constrainedSize: CGSize(width: size.width - insets.left - insets.right, height: 200.0)
) )
let attachedDescriptionFrame = CGRect(origin: CGPoint(x: floor((size.width - attachedDescriptionSize.width) * 0.5), y: contentFrame.maxY + 4.0), size: attachedDescriptionSize) let attachedDescriptionFrame = CGRect(origin: CGPoint(x: leftInset + floor((size.width - leftInset - rightInset - attachedDescriptionSize.width) * 0.5), y: contentFrame.maxY + 4.0), size: attachedDescriptionSize)
transition.updateFrame(node: attachedDescriptionNode, frame: attachedDescriptionFrame) transition.updateFrame(node: attachedDescriptionNode, frame: attachedDescriptionFrame)
if let (rect, containerSize) = self.absolutePosition { if let (rect, containerSize) = self.absolutePosition {

View File

@ -126,7 +126,7 @@ public final class ChatLoadingPlaceholderMessageContainer {
} }
if let avatarNode = self.avatarNode, let avatarBorderNode = self.avatarBorderNode { if let avatarNode = self.avatarNode, let avatarBorderNode = self.avatarBorderNode {
let avatarFrame = CGRect(origin: CGPoint(x: 3.0, y: rect.maxY + 1.0 - avatarSize.height), size: avatarSize) let avatarFrame = CGRect(origin: CGPoint(x: rect.minX + 3.0, y: rect.maxY + 1.0 - avatarSize.height), size: avatarSize)
transition.updateFrame(node: avatarNode, frame: avatarFrame) transition.updateFrame(node: avatarNode, frame: avatarFrame)
transition.updateFrame(node: avatarBorderNode, frame: avatarFrame) transition.updateFrame(node: avatarBorderNode, frame: avatarFrame)
@ -134,7 +134,7 @@ public final class ChatLoadingPlaceholderMessageContainer {
avatarOffset += avatarSize.width - 1.0 avatarOffset += avatarSize.width - 1.0
} }
let bubbleFrame = CGRect(origin: CGPoint(x: 3.0 + avatarOffset, y: rect.origin.y), size: CGSize(width: rect.width, height: rect.height)) let bubbleFrame = CGRect(origin: CGPoint(x: rect.minX + 3.0 + avatarOffset, y: rect.origin.y), size: CGSize(width: rect.width, height: rect.height))
transition.updateFrame(node: self.bubbleNode, frame: bubbleFrame) transition.updateFrame(node: self.bubbleNode, frame: bubbleFrame)
transition.updateFrame(node: self.bubbleBorderNode, frame: bubbleFrame) transition.updateFrame(node: self.bubbleBorderNode, frame: bubbleFrame)
} }
@ -484,7 +484,7 @@ public final class ChatLoadingPlaceholderNode: ASDisplayNode {
for messageContainer in self.messageContainers { for messageContainer in self.messageContainers {
let messageSize = dimensions[index % 14] let messageSize = dimensions[index % 14]
messageContainer.update(size: bounds.size, hasAvatar: self.chatType != .channel && self.chatType != .user, rect: CGRect(origin: CGPoint(x: 0.0, y: bounds.size.height - insets.bottom - offset - messageSize.height), size: messageSize), transition: transition) messageContainer.update(size: bounds.size, hasAvatar: self.chatType != .channel && self.chatType != .user, rect: CGRect(origin: CGPoint(x: insets.left, y: bounds.size.height - insets.bottom - offset - messageSize.height), size: messageSize), transition: transition)
offset += messageSize.height offset += messageSize.height
index += 1 index += 1
} }

View File

@ -1423,10 +1423,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
strongSelf.contextSourceNode.contentNode.insertSubnode(strongSelf.textNode.textNode, aboveSubnode: strongSelf.imageNode) strongSelf.contextSourceNode.contentNode.insertSubnode(strongSelf.textNode.textNode, aboveSubnode: strongSelf.imageNode)
} }
strongSelf.textNode.textNode.frame = imageFrame animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: imageFrame, completion: nil)
} }
strongSelf.imageNode.frame = updatedContentFrame animation.animator.updateFrame(layer: strongSelf.imageNode.layer, frame: updatedContentFrame, completion: nil)
strongSelf.contextSourceNode.contentRect = contextContentFrame strongSelf.contextSourceNode.contentRect = contextContentFrame
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
@ -1458,11 +1458,13 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let foregroundColor: UIColor = .clear// = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderColor, wallpaper: item.presentationData.theme.wallpaper) let foregroundColor: UIColor = .clear// = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderColor, wallpaper: item.presentationData.theme.wallpaper)
let shimmeringColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderShimmerColor, wallpaper: item.presentationData.theme.wallpaper) let shimmeringColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderShimmerColor, wallpaper: item.presentationData.theme.wallpaper)
strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: foregroundColor, shimmeringColor: shimmeringColor, data: immediateThumbnailData, size: animationNodeFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)) strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: foregroundColor, shimmeringColor: shimmeringColor, data: immediateThumbnailData, size: animationNodeFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0))
strongSelf.placeholderNode.frame = animationNodeFrame animation.animator.updateFrame(layer: strongSelf.placeholderNode.layer, frame: animationNodeFrame, completion: nil)
} }
if strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode { if strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode {
strongSelf.animationNode?.frame = animationNodeFrame if let animationNode = strongSelf.animationNode {
animation.animator.updateFrame(layer: animationNode.layer, frame: animationNodeFrame, completion: nil)
}
if let animationNode = strongSelf.animationNode as? AnimatedStickerNode { if let animationNode = strongSelf.animationNode as? AnimatedStickerNode {
animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size) animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
@ -1494,7 +1496,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
} }
let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account) let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account)
updatedShareButtonNode.frame = CGRect(origin: CGPoint(x: !incoming ? updatedImageFrame.minX - buttonSize.width - 6.0 : updatedImageFrame.maxX + 8.0, y: updatedImageFrame.maxY - buttonSize.height - 4.0 + imageBottomPadding), size: buttonSize) animation.animator.updateFrame(layer: updatedShareButtonNode.layer, frame: CGRect(origin: CGPoint(x: !incoming ? updatedImageFrame.minX - buttonSize.width - 6.0 : updatedImageFrame.maxX + 8.0, y: updatedImageFrame.maxY - buttonSize.height - 4.0 + imageBottomPadding), size: buttonSize), completion: nil)
} else if let shareButtonNode = strongSelf.shareButtonNode { } else if let shareButtonNode = strongSelf.shareButtonNode {
shareButtonNode.removeFromSupernode() shareButtonNode.removeFromSupernode()
strongSelf.shareButtonNode = nil strongSelf.shareButtonNode = nil

View File

@ -1926,7 +1926,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
inlineBotNameString = attribute.title inlineBotNameString = attribute.title
} }
} else if let attribute = attribute as? ReplyMessageAttribute { } else if let attribute = attribute as? ReplyMessageAttribute {
if let threadId = firstMessage.threadId, Int32(clamping: threadId) == attribute.messageId.id { if let threadId = firstMessage.threadId, Int32(clamping: threadId) == attribute.messageId.id, let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, channel.isForumOrMonoForum {
} else { } else {
replyMessage = firstMessage.associatedMessages[attribute.messageId] replyMessage = firstMessage.associatedMessages[attribute.messageId]
} }
@ -2429,6 +2429,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
var mediaInfoSizeApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?) = (CGSize(), { _ in nil }) var mediaInfoSizeApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?) = (CGSize(), { _ in nil })
var hasTitleAvatar = false var hasTitleAvatar = false
var hasTitleTopicNavigation = false
if displayHeader { if displayHeader {
let bubbleWidthInsets: CGFloat = mosaicRange == nil ? layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right : 0.0 let bubbleWidthInsets: CGFloat = mosaicRange == nil ? layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right : 0.0
@ -2439,6 +2440,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if isSidePanelOpen && incoming { if isSidePanelOpen && incoming {
hasTitleAvatar = true hasTitleAvatar = true
hasTitleTopicNavigation = item.chatLocation.threadId == nil
} }
let inlineBotNameColor = messageTheme.accentTextColor let inlineBotNameColor = messageTheme.accentTextColor
@ -2544,7 +2546,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
var nameAvatarSpaceWidth: CGFloat = 0.0 var nameAvatarSpaceWidth: CGFloat = 0.0
if hasTitleAvatar { if hasTitleAvatar {
headerSize.height += 12.0 headerSize.height += 12.0
nameAvatarSpaceWidth += 26.0 + 5.0 + 4.0 + 26.0 nameAvatarSpaceWidth += 26.0 + 5.0
if hasTitleTopicNavigation {
nameAvatarSpaceWidth += 4.0 + 26.0
}
nameNodeOriginY += 5.0 nameNodeOriginY += 5.0
} }
@ -3285,6 +3290,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contentOrigin: contentOrigin, contentOrigin: contentOrigin,
nameNodeOriginY: nameNodeOriginY + detachedContentNodesHeight + additionalTopHeight, nameNodeOriginY: nameNodeOriginY + detachedContentNodesHeight + additionalTopHeight,
hasTitleAvatar: hasTitleAvatar, hasTitleAvatar: hasTitleAvatar,
hasTitleTopicNavigation: hasTitleTopicNavigation,
authorNameColor: authorNameColor, authorNameColor: authorNameColor,
layoutConstants: layoutConstants, layoutConstants: layoutConstants,
currentCredibilityIcon: currentCredibilityIcon, currentCredibilityIcon: currentCredibilityIcon,
@ -3348,6 +3354,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contentOrigin: CGPoint, contentOrigin: CGPoint,
nameNodeOriginY: CGFloat, nameNodeOriginY: CGFloat,
hasTitleAvatar: Bool, hasTitleAvatar: Bool,
hasTitleTopicNavigation: Bool,
authorNameColor: UIColor?, authorNameColor: UIColor?,
layoutConstants: ChatMessageItemLayoutConstants, layoutConstants: ChatMessageItemLayoutConstants,
currentCredibilityIcon: (EmojiStatusComponent.Content, UIColor?)?, currentCredibilityIcon: (EmojiStatusComponent.Content, UIColor?)?,
@ -3538,21 +3545,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
strongSelf.clippingNode.addSubnode(nameAvatarNode) strongSelf.clippingNode.addSubnode(nameAvatarNode)
} }
let nameNavigateButton: NameNavigateButton
if let current = strongSelf.nameNavigateButton {
nameNavigateButton = current
} else {
nameNavigateButton = NameNavigateButton(frame: CGRect())
strongSelf.nameNavigateButton = nameNavigateButton
strongSelf.clippingNode.view.addSubview(nameNavigateButton)
nameNavigateButton.action = { [weak strongSelf] in
guard let strongSelf, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateChatLocationThread(item.content.firstMessage.threadId, nil)
}
}
let nameAvatarFrame = CGRect(origin: CGPoint(x: nameNodeFrame.minX, y: nameNodeFrame.minY - 4.0), size: CGSize(width: 26.0, height: 26.0)) let nameAvatarFrame = CGRect(origin: CGPoint(x: nameNodeFrame.minX, y: nameNodeFrame.minY - 4.0), size: CGSize(width: 26.0, height: 26.0))
let nameNavigateFrame = CGRect(origin: CGPoint(x: nameNodeFrame.maxX + 4.0 + nameNavigateButtonOffset, y: nameNodeFrame.minY - 4.0), size: CGSize(width: 26.0, height: 26.0)) let nameNavigateFrame = CGRect(origin: CGPoint(x: nameNodeFrame.maxX + 4.0 + nameNavigateButtonOffset, y: nameNodeFrame.minY - 4.0), size: CGSize(width: 26.0, height: 26.0))
@ -3563,11 +3555,38 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
} }
nameAvatarNode.updateSize(size: nameAvatarFrame.size) nameAvatarNode.updateSize(size: nameAvatarFrame.size)
nameNavigateButton.update(size: nameNavigateFrame.size, color: authorNameColor ?? item.presentationData.theme.theme.chat.message.incoming.accentTextColor) if hasTitleTopicNavigation {
let nameNavigateButton: NameNavigateButton
if let current = strongSelf.nameNavigateButton {
nameNavigateButton = current
} else {
nameNavigateButton = NameNavigateButton(frame: CGRect())
strongSelf.nameNavigateButton = nameNavigateButton
strongSelf.clippingNode.view.addSubview(nameNavigateButton)
nameNavigateButton.action = { [weak strongSelf] in
guard let strongSelf, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateChatLocationThread(item.content.firstMessage.threadId, nil)
}
}
nameNavigateButton.update(size: nameNavigateFrame.size, color: authorNameColor ?? item.presentationData.theme.theme.chat.message.incoming.accentTextColor)
} else {
if let nameNavigateButton = strongSelf.nameNavigateButton {
strongSelf.nameNavigateButton = nil
nameNavigateButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak nameNavigateButton] _ in
nameNavigateButton?.removeFromSuperview()
})
animation.animator.updateFrame(layer: nameNavigateButton.layer, frame: CGRect(origin: CGPoint(x: nameNodeFrame.maxX + nameNavigateButtonOffset - 26.0 * 0.5, y: nameNodeFrame.minY - 4.0), size: CGSize(width: 26.0, height: 26.0)), completion: nil)
animation.transition.updateTransformScale(layer: nameNavigateButton.layer, scale: CGPoint(x: 0.001, y: 0.001))
}
}
if animateNameAvatar { if animateNameAvatar {
animation.animator.updateFrame(layer: nameAvatarNode.layer, frame: nameAvatarFrame, completion: nil) animation.animator.updateFrame(layer: nameAvatarNode.layer, frame: nameAvatarFrame, completion: nil)
animation.animator.updateFrame(layer: nameNavigateButton.layer, frame: nameNavigateFrame, completion: nil) if let nameNavigateButton = strongSelf.nameNavigateButton {
animation.animator.updateFrame(layer: nameNavigateButton.layer, frame: nameNavigateFrame, completion: nil)
}
} else { } else {
nameAvatarNode.frame = CGRect(origin: CGPoint(x: previousNameNodeFrame.minX - 26.0 * 0.5, y: previousNameNodeFrame.minY - 4.0), size: CGSize(width: 26.0, height: 26.0)) nameAvatarNode.frame = CGRect(origin: CGPoint(x: previousNameNodeFrame.minX - 26.0 * 0.5, y: previousNameNodeFrame.minY - 4.0), size: CGSize(width: 26.0, height: 26.0))
animation.animator.updateFrame(layer: nameAvatarNode.layer, frame: nameAvatarFrame, completion: nil) animation.animator.updateFrame(layer: nameAvatarNode.layer, frame: nameAvatarFrame, completion: nil)
@ -3576,11 +3595,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
nameAvatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) nameAvatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
} }
nameNavigateButton.frame = CGRect(origin: CGPoint(x: previousNameNodeFrame.maxX + nameNavigateButtonOffset - 26.0 * 0.5, y: previousNameNodeFrame.minY - 4.0), size: CGSize(width: 26.0, height: 26.0)) if let nameNavigateButton = strongSelf.nameNavigateButton {
animation.animator.updateFrame(layer: nameNavigateButton.layer, frame: nameNavigateFrame, completion: nil) nameNavigateButton.frame = CGRect(origin: CGPoint(x: previousNameNodeFrame.maxX + nameNavigateButtonOffset - 26.0 * 0.5, y: previousNameNodeFrame.minY - 4.0), size: CGSize(width: 26.0, height: 26.0))
if animation.isAnimated { animation.animator.updateFrame(layer: nameNavigateButton.layer, frame: nameNavigateFrame, completion: nil)
animation.transition.animateTransformScale(view: nameNavigateButton, from: 0.001) if animation.isAnimated {
nameNavigateButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) animation.transition.animateTransformScale(view: nameNavigateButton, from: 0.001)
nameNavigateButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
}
} }
} }

View File

@ -141,7 +141,14 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs
} }
} }
if abs(lhsEffectiveTimestamp - rhsEffectiveTimestamp) < Int32(10 * 60) && sameChat && sameAuthor && sameThread && !isPaid { var isNonMergeablePaid = isPaid
if isNonMergeablePaid {
if let channel = lhs.peers[lhs.id.peerId] as? TelegramChannel, channel.flags.contains(.isMonoforum) {
isNonMergeablePaid = false
}
}
if abs(lhsEffectiveTimestamp - rhsEffectiveTimestamp) < Int32(10 * 60) && sameChat && sameAuthor && sameThread && !isNonMergeablePaid {
if let channel = lhs.peers[lhs.id.peerId] as? TelegramChannel, case .group = channel.info, lhsEffectiveAuthor?.id == channel.id, !lhs.effectivelyIncoming(accountPeerId) { if let channel = lhs.peers[lhs.id.peerId] as? TelegramChannel, case .group = channel.info, lhsEffectiveAuthor?.id == channel.id, !lhs.effectivelyIncoming(accountPeerId) {
return .none return .none
} }

View File

@ -178,6 +178,9 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
if firstMessage.id.peerId.isRepliesOrVerificationCodes, let author = firstMessage.forwardInfo?.author { if firstMessage.id.peerId.isRepliesOrVerificationCodes, let author = firstMessage.forwardInfo?.author {
avatarPeer = EnginePeer(author) avatarPeer = EnginePeer(author)
} }
if case let .channel(channel) = avatarPeer, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = firstMessage.peers[linkedMonoforumId] as? TelegramChannel {
avatarPeer = .channel(mainChannel)
}
self.avatarNode.setPeer(context: item.context, theme: presentationData.theme, peer: avatarPeer, overrideImage: peer.id == item.context.account.peerId ? .savedMessagesIcon : nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor) self.avatarNode.setPeer(context: item.context, theme: presentationData.theme, peer: avatarPeer, overrideImage: peer.id == item.context.account.peerId ? .savedMessagesIcon : nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor)
} }

View File

@ -995,7 +995,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
let placeholderFrame = updatedImageFrame.insetBy(dx: innerImageInset, dy: innerImageInset) let placeholderFrame = updatedImageFrame.insetBy(dx: innerImageInset, dy: innerImageInset)
strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: foregroundColor, shimmeringColor: shimmeringColor, data: immediateThumbnailData, size: placeholderFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency) strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: foregroundColor, shimmeringColor: shimmeringColor, data: immediateThumbnailData, size: placeholderFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency)
strongSelf.placeholderNode.frame = placeholderFrame animation.animator.updateFrame(layer: strongSelf.placeholderNode.layer, frame: placeholderFrame, completion: nil)
} }
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
@ -1067,7 +1067,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
strongSelf.contextSourceNode.contentNode.addSubnode(threadInfoNode) strongSelf.contextSourceNode.contentNode.addSubnode(threadInfoNode)
} }
let threadInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 6.0) : (params.width - params.rightInset - threadInfoSize.width - layoutConstants.bubble.edgeInset - 8.0)), y: 8.0), size: threadInfoSize) let threadInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 6.0) : (params.width - params.rightInset - threadInfoSize.width - layoutConstants.bubble.edgeInset - 8.0)), y: 8.0), size: threadInfoSize)
threadInfoNode.frame = threadInfoFrame animation.animator.updateFrame(layer: threadInfoNode.layer, frame: threadInfoFrame, completion: nil)
headersOffset += threadInfoSize.height + 10.0 headersOffset += threadInfoSize.height + 10.0
} else if let replyInfoNode = strongSelf.replyInfoNode { } else if let replyInfoNode = strongSelf.replyInfoNode {
@ -1120,7 +1120,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
} }
} }
let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0 + 5.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 8.0 - 5.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: forwardInfoSize) let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0 + 5.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 8.0 - 5.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: forwardInfoSize)
forwardInfoNode.frame = forwardInfoFrame animation.animator.updateFrame(layer: forwardInfoNode.layer, frame: forwardInfoFrame, completion: nil)
messageInfoSize = CGSize(width: messageInfoSize.width, height: messageInfoSize.height + forwardInfoSize.height + 8.0) messageInfoSize = CGSize(width: messageInfoSize.width, height: messageInfoSize.height + forwardInfoSize.height + 8.0)

View File

@ -590,6 +590,8 @@ public final class ChatSideTopicsPanel: Component {
public final class View: UIView { public final class View: UIView {
private let scrollView: ScrollView private let scrollView: ScrollView
private let scrollContainerView: UIView
private let scrollViewMask: UIImageView
private let background = ComponentView<Empty>() private let background = ComponentView<Empty>()
private let separatorLayer: SimpleLayer private let separatorLayer: SimpleLayer
@ -612,13 +614,20 @@ public final class ChatSideTopicsPanel: Component {
self.selectedLineView = UIImageView() self.selectedLineView = UIImageView()
self.scrollView = ScrollView(frame: CGRect()) self.scrollView = ScrollView(frame: CGRect())
self.scrollContainerView = UIView()
self.scrollViewMask = UIImageView(image: generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [
UIColor(white: 1.0, alpha: 0.0),
UIColor(white: 1.0, alpha: 1.0)
], locations: [0.0, 1.0], direction: .vertical)?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 8))
self.scrollContainerView.mask = self.scrollViewMask
self.separatorLayer = SimpleLayer() self.separatorLayer = SimpleLayer()
super.init(frame: frame) super.init(frame: frame)
self.scrollView.delaysContentTouches = false self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false self.scrollView.clipsToBounds = true
self.scrollView.contentInsetAdjustmentBehavior = .never self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
@ -629,7 +638,8 @@ public final class ChatSideTopicsPanel: Component {
self.scrollView.alwaysBounceVertical = false self.scrollView.alwaysBounceVertical = false
self.scrollView.scrollsToTop = false self.scrollView.scrollsToTop = false
self.addSubview(self.scrollView) self.addSubview(self.scrollContainerView)
self.scrollContainerView.addSubview(self.scrollView)
self.scrollView.addSubview(self.selectedLineView) self.scrollView.addSubview(self.selectedLineView)
} }
@ -709,7 +719,7 @@ public final class ChatSideTopicsPanel: Component {
if let backgroundView = self.background.view { if let backgroundView = self.background.view {
if backgroundView.superview == nil { if backgroundView.superview == nil {
self.insertSubview(backgroundView, belowSubview: self.scrollView) self.insertSubview(backgroundView, at: 0)
} }
transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize))
} }
@ -735,20 +745,9 @@ public final class ChatSideTopicsPanel: Component {
let itemSpacing: CGFloat = 24.0 let itemSpacing: CGFloat = 24.0
var contentSize = CGSize(width: panelWidth, height: 0.0) var topContainerInset: CGFloat = containerInsets.top
contentSize.height += containerInsets.top
var validIds: [Item.Id] = []
var isFirst = true
var selectedItemFrame: CGRect?
do { do {
if isFirst {
isFirst = false
} else {
contentSize.height += itemSpacing
}
var itemTransition = transition var itemTransition = transition
var animateIn = false var animateIn = false
let itemView: TabItemView let itemView: TabItemView
@ -764,11 +763,11 @@ public final class ChatSideTopicsPanel: Component {
component.togglePanel() component.togglePanel()
}) })
self.tabItemView = itemView self.tabItemView = itemView
self.scrollView.addSubview(itemView) self.addSubview(itemView)
} }
let itemSize = itemView.update(context: component.context, theme: component.theme, width: panelWidth, transition: .immediate) let itemSize = itemView.update(context: component.context, theme: component.theme, width: panelWidth, transition: .immediate)
let itemFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: contentSize.height), size: itemSize) let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: topContainerInset), size: itemSize)
itemTransition.setPosition(layer: itemView.layer, position: itemFrame.center) itemTransition.setPosition(layer: itemView.layer, position: itemFrame.center)
itemTransition.setBounds(layer: itemView.layer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) itemTransition.setBounds(layer: itemView.layer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
@ -778,10 +777,17 @@ public final class ChatSideTopicsPanel: Component {
transition.containedViewLayoutTransition.animateTransformScale(view: itemView, from: 0.001) transition.containedViewLayoutTransition.animateTransformScale(view: itemView, from: 0.001)
} }
contentSize.height += itemSize.height topContainerInset += itemSize.height
contentSize.height -= 20.0 topContainerInset -= 24.0
} }
var contentSize = CGSize(width: panelWidth, height: 0.0)
contentSize.height += 36.0
var validIds: [Item.Id] = []
var isFirst = true
var selectedItemFrame: CGRect?
do { do {
if isFirst { if isFirst {
isFirst = false isFirst = false
@ -931,7 +937,11 @@ public final class ChatSideTopicsPanel: Component {
contentSize.height += containerInsets.bottom contentSize.height += containerInsets.bottom
let scrollSize = CGSize(width: availableSize.width, height: availableSize.height) let scrollSize = CGSize(width: availableSize.width, height: availableSize.height - topContainerInset)
self.scrollContainerView.frame = CGRect(origin: CGPoint(x: 0.0, y: topContainerInset), size: scrollSize)
self.scrollViewMask.frame = CGRect(origin: CGPoint(x: 0.0, y: topContainerInset), size: scrollSize)
if self.scrollView.bounds.size != scrollSize { if self.scrollView.bounds.size != scrollSize {
self.scrollView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize) self.scrollView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize)
} }

View File

@ -145,6 +145,8 @@ final class ChatIntroItemComponent: Component {
backgroundNode: backgroundNode, backgroundNode: backgroundNode,
size: size, size: size,
insets: UIEdgeInsets(), insets: UIEdgeInsets(),
leftInset: 0.0,
rightInset: 0.0,
transition: .immediate transition: .immediate
) )

View File

@ -810,9 +810,16 @@ extension ChatControllerImpl {
contactStatus = ChatContactStatus(canAddContact: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot) contactStatus = ChatContactStatus(canAddContact: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot)
if let channel = peerView.peers[peerView.peerId] as? TelegramChannel { if let channel = peerView.peers[peerView.peerId] as? TelegramChannel {
if channel.flags.contains(.isCreator) || channel.adminRights != nil { if channel.isMonoForum {
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = peerView.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.sendSomething) {
} else {
sendPaidMessageStars = channel.sendPaidMessageStars
}
} else { } else {
sendPaidMessageStars = channel.sendPaidMessageStars if channel.flags.contains(.isCreator) || channel.adminRights != nil {
} else {
sendPaidMessageStars = channel.sendPaidMessageStars
}
} }
} }
} }
@ -1857,8 +1864,6 @@ extension ChatControllerImpl {
} }
if globalRemainingUnreadChatCount > 0 { if globalRemainingUnreadChatCount > 0 {
strongSelf.initialNavigationBadge = "\(globalRemainingUnreadChatCount)" strongSelf.initialNavigationBadge = "\(globalRemainingUnreadChatCount)"
} else {
strongSelf.initialNavigationBadge = ""
} }
} }
} }

View File

@ -195,7 +195,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
private var emptyNode: ChatEmptyNode? private var emptyNode: ChatEmptyNode?
private(set) var emptyType: ChatHistoryNodeLoadState.EmptyType? private(set) var emptyType: ChatHistoryNodeLoadState.EmptyType?
private var didDisplayEmptyGreeting = false private var didDisplayEmptyGreeting = false
private var validEmptyNodeLayout: (CGSize, UIEdgeInsets)? private var validEmptyNodeLayout: (CGSize, UIEdgeInsets, CGFloat, CGFloat)?
var restrictedNode: ChatRecentActionsEmptyNode? var restrictedNode: ChatRecentActionsEmptyNode?
private(set) var validLayout: (ContainerViewLayout, CGFloat)? private(set) var validLayout: (ContainerViewLayout, CGFloat)?
@ -1019,7 +1019,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.contentContainerNode.contentNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer) self.contentContainerNode.contentNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer)
} }
if let (size, insets) = self.validEmptyNodeLayout { if let (size, insets, leftInset, rightInset) = self.validEmptyNodeLayout {
let mappedType: ChatEmptyNode.Subject.EmptyType let mappedType: ChatEmptyNode.Subject.EmptyType
switch emptyType { switch emptyType {
case .generic: case .generic:
@ -1033,7 +1033,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
case .botInfo: case .botInfo:
mappedType = .botInfo mappedType = .botInfo
} }
emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(mappedType), loadingNode: wasLoading && self.loadingNode.supernode != nil ? self.loadingNode : nil, backgroundNode: self.backgroundNode, size: size, insets: insets, transition: .immediate) emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(mappedType), loadingNode: wasLoading && self.loadingNode.supernode != nil ? self.loadingNode : nil, backgroundNode: self.backgroundNode, size: size, insets: insets, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
emptyNode.frame = CGRect(origin: CGPoint(), size: size) emptyNode.frame = CGRect(origin: CGPoint(), size: size)
} }
if animated { if animated {
@ -2046,6 +2046,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0)) transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0))
} }
if immediatelyLayoutLeftPanelNodeAndAnimateAppearance || dismissedLeftPanel != nil || immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance || dismissedTitleTopicsAccessoryPanelNode != nil {
self.historyNode.resetScrolledToItem()
}
if let blurredHistoryNode = self.blurredHistoryNode { if let blurredHistoryNode = self.blurredHistoryNode {
transition.updateFrame(node: blurredHistoryNode, frame: contentBounds) transition.updateFrame(node: blurredHistoryNode, frame: contentBounds)
} }
@ -2199,28 +2203,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
} }
} }
var emptyNodeInsets = insets
emptyNodeInsets.bottom += inputPanelsHeight
self.validEmptyNodeLayout = (contentBounds.size, emptyNodeInsets)
if let emptyNode = self.emptyNode, let emptyType = self.emptyType {
let mappedType: ChatEmptyNode.Subject.EmptyType
switch emptyType {
case .generic:
mappedType = .generic
case .joined:
mappedType = .joined
case .clearedHistory:
mappedType = .clearedHistory
case .topic:
mappedType = .topic
case .botInfo:
mappedType = .botInfo
}
emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(mappedType), loadingNode: nil, backgroundNode: self.backgroundNode, size: contentBounds.size, insets: emptyNodeInsets, transition: transition)
transition.updateFrame(node: emptyNode, frame: contentBounds)
emptyNode.update(rect: contentBounds, within: contentBounds.size, transition: transition)
}
var contentBottomInset: CGFloat = inputPanelsHeight + 4.0 var contentBottomInset: CGFloat = inputPanelsHeight + 4.0
if let scrollContainerNode = self.scrollContainerNode { if let scrollContainerNode = self.scrollContainerNode {
@ -2239,10 +2221,17 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
let visibleAreaInset = UIEdgeInsets(top: containerInsets.top, left: 0.0, bottom: containerInsets.bottom + inputPanelsHeight, right: 0.0) let visibleAreaInset = UIEdgeInsets(top: containerInsets.top, left: 0.0, bottom: containerInsets.bottom + inputPanelsHeight, right: 0.0)
self.visibleAreaInset = visibleAreaInset self.visibleAreaInset = visibleAreaInset
self.loadingNode.updateLayout(size: contentBounds.size, insets: visibleAreaInset, transition: transition)
var loadingNodeInsets = visibleAreaInset
loadingNodeInsets.left = layout.safeInsets.left
loadingNodeInsets.right = layout.safeInsets.right
if let leftPanelSize {
loadingNodeInsets.left += leftPanelSize.width
}
self.loadingNode.updateLayout(size: contentBounds.size, insets: loadingNodeInsets, transition: transition)
if let loadingPlaceholderNode = self.loadingPlaceholderNode { if let loadingPlaceholderNode = self.loadingPlaceholderNode {
loadingPlaceholderNode.updateLayout(size: contentBounds.size, insets: visibleAreaInset, metrics: layout.metrics, transition: transition) loadingPlaceholderNode.updateLayout(size: contentBounds.size, insets: loadingNodeInsets, metrics: layout.metrics, transition: transition)
loadingPlaceholderNode.update(rect: contentBounds, within: contentBounds.size, transition: transition) loadingPlaceholderNode.update(rect: contentBounds, within: contentBounds.size, transition: transition)
} }
@ -2293,6 +2282,28 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
listInsets.left += leftPanelSize.width listInsets.left += leftPanelSize.width
} }
var emptyNodeInsets = insets
emptyNodeInsets.bottom += inputPanelsHeight
self.validEmptyNodeLayout = (contentBounds.size, emptyNodeInsets, listInsets.left, listInsets.right)
if let emptyNode = self.emptyNode, let emptyType = self.emptyType {
let mappedType: ChatEmptyNode.Subject.EmptyType
switch emptyType {
case .generic:
mappedType = .generic
case .joined:
mappedType = .joined
case .clearedHistory:
mappedType = .clearedHistory
case .topic:
mappedType = .topic
case .botInfo:
mappedType = .botInfo
}
emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(mappedType), loadingNode: nil, backgroundNode: self.backgroundNode, size: contentBounds.size, insets: emptyNodeInsets, leftInset: listInsets.left, rightInset: listInsets.right, transition: transition)
transition.updateFrame(node: emptyNode, frame: contentBounds)
emptyNode.update(rect: contentBounds, within: contentBounds.size, transition: transition)
}
var displayTopDimNode = false var displayTopDimNode = false
let ensureTopInsetForOverlayHighlightedItems: CGFloat? = nil let ensureTopInsetForOverlayHighlightedItems: CGFloat? = nil
var expandTopDimNode = false var expandTopDimNode = false
@ -3283,71 +3294,16 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
guard let self else { guard let self else {
return nil return nil
} }
guard let peerId = self.chatPresentationInterfaceState.chatLocation.peerId else { guard let peer = self.chatPresentationInterfaceState.renderedPeer?.peer else {
return nil
}
if !peer.isForumOrMonoForum {
return nil return nil
} }
let viewKey: PostboxViewKey = .savedMessagesIndex(peerId: peerId) let threadListSignal: Signal<EngineChatList, NoError> = context.sharedContext.subscribeChatListData(context: self.context, location: peer.isMonoForum ? .savedMessagesChats(peerId: peer.id) : .forum(peerId: peer.id))
let threadListSignal: Signal<EngineChatList?, NoError> = self.context.account.postbox.combinedView(keys: [viewKey]) return threadListSignal |> map(Optional.init)
|> map { views -> EngineChatList? in
guard let view = views.views[viewKey] as? MessageHistorySavedMessagesIndexView else {
preconditionFailure()
}
var items: [EngineChatList.Item] = []
for item in view.items {
guard let sourcePeer = item.peer else {
continue
}
let sourceId = PeerId(item.id)
var messages: [EngineMessage] = []
if let topMessage = item.topMessage {
messages.append(EngineMessage(topMessage))
}
let mappedMessageIndex = MessageIndex(id: MessageId(peerId: sourceId, namespace: item.index.id.namespace, id: item.index.id.id), timestamp: item.index.timestamp)
items.append(EngineChatList.Item(
id: .chatList(sourceId),
index: .chatList(ChatListIndex(pinningIndex: item.pinnedIndex.flatMap(UInt16.init), messageIndex: mappedMessageIndex)),
messages: messages,
readCounters: EnginePeerReadCounters(
incomingReadId: 0, outgoingReadId: 0, count: Int32(item.unreadCount), markedUnread: item.markedUnread),
isMuted: false,
draft: nil,
threadData: nil,
renderedPeer: EngineRenderedPeer(peer: EnginePeer(sourcePeer)),
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
forumTopicData: nil,
topForumTopicItems: [],
hasFailed: false,
isContact: false,
autoremoveTimeout: nil,
storyStats: nil,
displayAsTopicList: false,
isPremiumRequiredToMessage: false,
mediaDraftContentType: nil
))
}
let list = EngineChatList(
items: items.reversed(),
groupItems: [],
additionalItems: [],
hasEarlier: false,
hasLater: false,
isLoading: view.isLoading
)
return list
}
return threadListSignal
}, },
loadMoreSearchResults: { [weak self] in loadMoreSearchResults: { [weak self] in
guard let self, let controller = self.controller else { guard let self, let controller = self.controller else {

View File

@ -135,6 +135,20 @@ func chatHistoryEntriesForView(
continue loop continue loop
} }
} }
} else if case .peer = location {
for media in message.media {
if let action = media as? TelegramMediaAction, case .groupCreated = action.action {
var chatPeer: Peer?
for entry in view.additionalData {
if case let .peer(_, peer) = entry {
chatPeer = peer
}
}
if let channel = chatPeer as? TelegramChannel, channel.isMonoForum {
continue loop
}
}
}
} }
count += 1 count += 1

View File

@ -241,7 +241,7 @@ func titleTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterfa
return panel return panel
} }
} }
} else if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForum, chatPresentationInterfaceState.search == nil { } else if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForum, (channel.flags.contains(.displayForumAsTabs) || context.sharedContext.immediateExperimentalUISettings.allForumsHaveTabs), chatPresentationInterfaceState.search == nil {
let topicListDisplayMode = chatPresentationInterfaceState.topicListDisplayMode ?? .top let topicListDisplayMode = chatPresentationInterfaceState.topicListDisplayMode ?? .top
if case .top = topicListDisplayMode, let peerId = chatPresentationInterfaceState.chatLocation.peerId { if case .top = topicListDisplayMode, let peerId = chatPresentationInterfaceState.chatLocation.peerId {
if let currentPanel = currentPanel as? ChatTopicListTitleAccessoryPanelNode { if let currentPanel = currentPanel as? ChatTopicListTitleAccessoryPanelNode {

View File

@ -182,7 +182,7 @@ private final class CustomBadgeComponent: Component {
} }
} }
final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, ChatControllerCustomNavigationPanelNode, ASScrollViewDelegate { final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, ChatControllerCustomNavigationPanelNode {
private struct Params: Equatable { private struct Params: Equatable {
var width: CGFloat var width: CGFloat
var leftInset: CGFloat var leftInset: CGFloat
@ -736,6 +736,8 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
private let isMonoforum: Bool private let isMonoforum: Bool
private let scrollView: ScrollView private let scrollView: ScrollView
private let scrollViewContainer: UIView
private let scrollViewMask: UIImageView
private var params: Params? private var params: Params?
@ -756,12 +758,18 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
self.selectedLineView = UIImageView() self.selectedLineView = UIImageView()
self.scrollView = ScrollView(frame: CGRect()) self.scrollView = ScrollView(frame: CGRect())
self.scrollViewMask = UIImageView(image: generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [
UIColor(white: 1.0, alpha: 0.0),
UIColor(white: 1.0, alpha: 1.0)
], locations: [0.0, 1.0], direction: .horizontal)?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 0))
self.scrollViewContainer = UIView()
super.init() super.init()
self.scrollView.delaysContentTouches = false self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false self.scrollView.clipsToBounds = true
self.scrollView.contentInsetAdjustmentBehavior = .never self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
@ -771,9 +779,11 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
self.scrollView.alwaysBounceHorizontal = false self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = false self.scrollView.alwaysBounceVertical = false
self.scrollView.scrollsToTop = false self.scrollView.scrollsToTop = false
self.scrollView.delegate = self.wrappedScrollViewDelegate
self.view.addSubview(self.scrollView) self.scrollViewContainer.addSubview(self.scrollView)
self.scrollViewContainer.mask = self.scrollViewMask
self.view.addSubview(self.scrollViewContainer)
self.scrollView.addSubview(self.selectedLineView) self.scrollView.addSubview(self.selectedLineView)
@ -843,20 +853,9 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
let containerInsets = UIEdgeInsets(top: 0.0, left: params.leftInset + 16.0, bottom: 0.0, right: params.rightInset + 16.0) let containerInsets = UIEdgeInsets(top: 0.0, left: params.leftInset + 16.0, bottom: 0.0, right: params.rightInset + 16.0)
let itemSpacing: CGFloat = 24.0 let itemSpacing: CGFloat = 24.0
var contentSize = CGSize(width: 0.0, height: panelHeight) var leftContentInset: CGFloat = containerInsets.left + 8.0
contentSize.width += containerInsets.left + 8.0
var validIds: [Item.Id] = []
var isFirst = true
var selectedItemFrame: CGRect?
do { do {
if isFirst {
isFirst = false
} else {
contentSize.width += itemSpacing
}
var itemTransition = transition var itemTransition = transition
var animateIn = false var animateIn = false
let itemView: TabItemView let itemView: TabItemView
@ -872,11 +871,11 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
self.interfaceInteraction?.toggleChatSidebarMode() self.interfaceInteraction?.toggleChatSidebarMode()
}) })
self.tabItemView = itemView self.tabItemView = itemView
self.scrollView.addSubview(itemView) self.view.addSubview(itemView)
} }
let itemSize = itemView.update(context: self.context, theme: params.interfaceState.theme, height: panelHeight, transition: .immediate) let itemSize = itemView.update(context: self.context, theme: params.interfaceState.theme, height: panelHeight, transition: .immediate)
let itemFrame = CGRect(origin: CGPoint(x: contentSize.width, y: -5.0), size: itemSize) let itemFrame = CGRect(origin: CGPoint(x: leftContentInset, y: -5.0), size: itemSize)
itemTransition.updatePosition(layer: itemView.layer, position: itemFrame.center) itemTransition.updatePosition(layer: itemView.layer, position: itemFrame.center)
itemTransition.updateBounds(layer: itemView.layer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) itemTransition.updateBounds(layer: itemView.layer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
@ -886,9 +885,15 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
transition.animateTransformScale(view: itemView, from: 0.001) transition.animateTransformScale(view: itemView, from: 0.001)
} }
contentSize.width += itemSize.width leftContentInset += itemSize.width + 8.0
} }
var contentSize = CGSize(width: itemSpacing - 8.0, height: panelHeight)
var validIds: [Item.Id] = []
var isFirst = true
var selectedItemFrame: CGRect?
do { do {
if isFirst { if isFirst {
isFirst = false isFirst = false
@ -1034,9 +1039,12 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
contentSize.width += containerInsets.right contentSize.width += containerInsets.right
let scrollSize = CGSize(width: params.width, height: contentSize.height) let scrollSize = CGSize(width: params.width - leftContentInset, height: contentSize.height)
self.scrollViewContainer.frame = CGRect(origin: CGPoint(x: leftContentInset, y: 0.0), size: scrollSize)
self.scrollViewMask.frame = CGRect(origin: CGPoint(), size: scrollSize)
if self.scrollView.bounds.size != scrollSize { if self.scrollView.bounds.size != scrollSize {
self.scrollView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize) self.scrollView.frame = CGRect(origin: CGPoint(), size: scrollSize)
} }
if self.scrollView.contentSize != contentSize { if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize self.scrollView.contentSize = contentSize

View File

@ -167,9 +167,13 @@ final class ChatTranslationPanelNode: ASDisplayNode {
let buttonTextSize = self.buttonTextNode.updateLayout(CGSize(width: width - contentRightInset - moreButtonSize.width, height: panelHeight)) let buttonTextSize = self.buttonTextNode.updateLayout(CGSize(width: width - contentRightInset - moreButtonSize.width, height: panelHeight))
if let icon = self.buttonIconNode.image { if let icon = self.buttonIconNode.image {
let buttonSize = CGSize(width: buttonTextSize.width + icon.size.width + buttonSpacing + buttonPadding * 2.0, height: panelHeight) let buttonSize = CGSize(width: buttonTextSize.width + icon.size.width + buttonSpacing + buttonPadding * 2.0, height: panelHeight)
self.button.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - buttonSize.width) / 2.0), y: 0.0), size: buttonSize) transition.updateFrame(node: self.button, frame: CGRect(origin: CGPoint(x: leftInset + floorToScreenPixels((width - leftInset - rightInset - buttonSize.width) / 2.0), y: 0.0), size: buttonSize))
self.buttonIconNode.frame = CGRect(origin: CGPoint(x: buttonPadding, y: floorToScreenPixels((buttonSize.height - icon.size.height) / 2.0)), size: icon.size)
self.buttonTextNode.frame = CGRect(origin: CGPoint(x: buttonPadding + icon.size.width + buttonSpacing, y: floorToScreenPixels((buttonSize.height - buttonTextSize.height) / 2.0)), size: buttonTextSize) transition.updateFrame(node: self.buttonIconNode, frame: CGRect(origin: CGPoint(x: buttonPadding, y: floorToScreenPixels((buttonSize.height - icon.size.height) / 2.0)), size: icon.size))
let buttonTextFrame = CGRect(origin: CGPoint(x: buttonPadding + icon.size.width + buttonSpacing, y: floorToScreenPixels((buttonSize.height - buttonTextSize.height) / 2.0)), size: buttonTextSize)
transition.updatePosition(node: self.buttonTextNode, position: buttonTextFrame.center)
self.buttonTextNode.bounds = CGRect(origin: CGPoint(), size: buttonTextFrame.size)
} }
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))

View File

@ -42,16 +42,20 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
viewForumAsMessages = .single(false) viewForumAsMessages = .single(false)
} }
} else if case let .peer(peer) = params.chatLocation, case let .channel(channel) = peer, channel.flags.contains(.isForum) { } else if case let .peer(peer) = params.chatLocation, case let .channel(channel) = peer, channel.flags.contains(.isForum) {
viewForumAsMessages = params.context.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peer.id)]) if channel.flags.contains(.displayForumAsTabs) {
|> take(1) viewForumAsMessages = .single(true)
|> map { combinedView in } else {
guard let cachedDataView = combinedView.views[.cachedPeerData(peerId: peer.id)] as? CachedPeerDataView else { viewForumAsMessages = params.context.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peer.id)])
return false |> take(1)
} |> map { combinedView in
if let cachedData = cachedDataView.cachedPeerData as? CachedChannelData, case let .known(viewForumAsMessages) = cachedData.viewForumAsMessages, viewForumAsMessages { guard let cachedDataView = combinedView.views[.cachedPeerData(peerId: peer.id)] as? CachedPeerDataView else {
return true return false
} else { }
return false if let cachedData = cachedDataView.cachedPeerData as? CachedChannelData, case let .known(viewForumAsMessages) = cachedData.viewForumAsMessages, viewForumAsMessages {
return true
} else {
return false
}
} }
} }
} else if case let .peer(peer) = params.chatLocation, peer.id == params.context.account.peerId { } else if case let .peer(peer) = params.chatLocation, peer.id == params.context.account.peerId {

View File

@ -64,7 +64,7 @@ private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceh
self.wallpaperBackgroundNode.updateLayout(size: size, displayMode: needsTiling ? .aspectFit : .aspectFill, transition: transition) self.wallpaperBackgroundNode.updateLayout(size: size, displayMode: needsTiling ? .aspectFit : .aspectFill, transition: transition)
transition.updateFrame(node: self.wallpaperBackgroundNode, frame: contentBounds) transition.updateFrame(node: self.wallpaperBackgroundNode, frame: contentBounds)
self.emptyNode.updateLayout(interfaceState: self.presentationInterfaceState, subject: .detailsPlaceholder, loadingNode: nil, backgroundNode: self.wallpaperBackgroundNode, size: contentBounds.size, insets: .zero, transition: transition) self.emptyNode.updateLayout(interfaceState: self.presentationInterfaceState, subject: .detailsPlaceholder, loadingNode: nil, backgroundNode: self.wallpaperBackgroundNode, size: contentBounds.size, insets: .zero, leftInset: 0.0, rightInset: 0.0, transition: transition)
transition.updateFrame(node: self.emptyNode, frame: CGRect(origin: .zero, size: size)) transition.updateFrame(node: self.emptyNode, frame: CGRect(origin: .zero, size: size))
self.emptyNode.update(rect: contentBounds, within: contentBounds.size, transition: transition) self.emptyNode.update(rect: contentBounds, within: contentBounds.size, transition: transition)
} }

View File

@ -66,6 +66,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
public var fakeAds: Bool public var fakeAds: Bool
public var conferenceDebug: Bool public var conferenceDebug: Bool
public var checkSerializedData: Bool public var checkSerializedData: Bool
public var allForumsHaveTabs: Bool
public static var defaultSettings: ExperimentalUISettings { public static var defaultSettings: ExperimentalUISettings {
return ExperimentalUISettings( return ExperimentalUISettings(
@ -109,7 +110,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
devRequests: false, devRequests: false,
fakeAds: false, fakeAds: false,
conferenceDebug: false, conferenceDebug: false,
checkSerializedData: false checkSerializedData: false,
allForumsHaveTabs: false
) )
} }
@ -154,7 +156,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
devRequests: Bool, devRequests: Bool,
fakeAds: Bool, fakeAds: Bool,
conferenceDebug: Bool, conferenceDebug: Bool,
checkSerializedData: Bool checkSerializedData: Bool,
allForumsHaveTabs: Bool
) { ) {
self.keepChatNavigationStack = keepChatNavigationStack self.keepChatNavigationStack = keepChatNavigationStack
self.skipReadHistory = skipReadHistory self.skipReadHistory = skipReadHistory
@ -197,6 +200,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.fakeAds = fakeAds self.fakeAds = fakeAds
self.conferenceDebug = conferenceDebug self.conferenceDebug = conferenceDebug
self.checkSerializedData = checkSerializedData self.checkSerializedData = checkSerializedData
self.allForumsHaveTabs = allForumsHaveTabs
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -243,6 +247,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.fakeAds = try container.decodeIfPresent(Bool.self, forKey: "fakeAds") ?? false self.fakeAds = try container.decodeIfPresent(Bool.self, forKey: "fakeAds") ?? false
self.conferenceDebug = try container.decodeIfPresent(Bool.self, forKey: "conferenceDebug") ?? false self.conferenceDebug = try container.decodeIfPresent(Bool.self, forKey: "conferenceDebug") ?? false
self.checkSerializedData = try container.decodeIfPresent(Bool.self, forKey: "checkSerializedData") ?? false self.checkSerializedData = try container.decodeIfPresent(Bool.self, forKey: "checkSerializedData") ?? false
self.allForumsHaveTabs = try container.decodeIfPresent(Bool.self, forKey: "allForumsHaveTabs") ?? false
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
@ -289,6 +294,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
try container.encodeIfPresent(self.fakeAds, forKey: "fakeAds") try container.encodeIfPresent(self.fakeAds, forKey: "fakeAds")
try container.encodeIfPresent(self.conferenceDebug, forKey: "conferenceDebug") try container.encodeIfPresent(self.conferenceDebug, forKey: "conferenceDebug")
try container.encodeIfPresent(self.checkSerializedData, forKey: "checkSerializedData") try container.encodeIfPresent(self.checkSerializedData, forKey: "checkSerializedData")
try container.encodeIfPresent(self.allForumsHaveTabs, forKey: "allForumsHaveTabs")
} }
} }