Fix monoforum user unread counters

This commit is contained in:
Isaac 2025-05-27 18:41:24 +08:00
parent 8f7006281c
commit 3cad999c75
21 changed files with 117 additions and 984 deletions

View File

@ -14382,3 +14382,17 @@ Sorry for the inconvenience.";
"ChannelMessages.SwitchTitle" = "Allow Channel Messages";
"ChannelMessages.PriceSectionTitle" = "PRICE FOR EACH MESSAGE";
"ChannelMessages.PriceSectionFooter" = "Charge users for the ability to suggest one post for your channel. You're not required to publish any suggestions by charging this. You'll receive 85% of the selected fee for each incoming suggestion.";
"ChatList.MonoforumLabel" = "MESSAGES";
"ChatList.MonoforumEmptyText" = "No messages here yet...";
"Chat.InlineTopicMenu.Reorder" = "Reorder";
"Chat.InlineTopicMenu.Edit" = "Edit";
"Chat.InlineTopicMenu.AllTab" = "All";
"Chat.ChannelMessagesHint" = "Tap here to send a message";
"Chat.ChannelMessagesHintBadge" = "NEW";
"PeerInfo.AllowChannelMessages" = "Allow Channel Messages";
"PeerInfo.AllowChannelMessages.On" = "On";
"PeerInfo.AllowChannelMessages.Off" = "Off";
"PeerInfo.ChannelMessages" = "Channel Messages";

View File

@ -632,8 +632,7 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, thr
})))
if isPinned, let reorder {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_InlineTopicMenu_Reorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c?.dismiss(completion: {
})
reorder()
@ -664,8 +663,7 @@ public func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, thr
}
if threadId != 1, canOpenClose, let customEdit {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Edit", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { c, f in
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_InlineTopicMenu_Edit, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { c, f in
if let c = c as? ContextController {
customEdit(c)
} else {

View File

@ -3014,8 +3014,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
textColor = theme.titleColor
}
if case let .channel(channel) = itemPeer.peer, channel.flags.contains(.isMonoforum) {
//TODO:localize
titleBadgeText = "MESSAGES"
titleBadgeText = item.presentationData.strings.ChatList_MonoforumLabel
}
titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: textColor)
}

View File

@ -185,7 +185,31 @@ final class ChatListIndexTable: Table {
var updatedPeerTags: [PeerId: (previous: PeerSummaryCounterTags, updated: PeerSummaryCounterTags)] = [:]
var updatedIsThreadBasedUnreadCountCalculation: [PeerId: Bool] = [:]
var upatedPeerMap: [PeerId: (Peer?, Peer)] = [:]
for (previous, updated) in updatedPeers {
var needsAssociatedPeers = false
if let previous, previous.0.associatedPeerId != nil {
needsAssociatedPeers = true
} else if updated.0.associatedPeerId != nil {
needsAssociatedPeers = true
}
if needsAssociatedPeers && upatedPeerMap.isEmpty {
for (previous, updated) in updatedPeers {
upatedPeerMap[updated.0.id] = (previous?.0, updated.0)
}
}
var previousAssociatedPeer: Peer?
var updatedAssociatedPeer: Peer?
if let previous, let previousAssociatedPeerId = previous.0.associatedPeerId {
previousAssociatedPeer = upatedPeerMap[previousAssociatedPeerId]?.0 ?? postbox.peerTable.get(previousAssociatedPeerId)
}
if let updatedAssociatedPeerId = updated.0.associatedPeerId {
updatedAssociatedPeer = upatedPeerMap[updatedAssociatedPeerId]?.1 ?? postbox.peerTable.get(updatedAssociatedPeerId)
}
let previousTags: PeerSummaryCounterTags
if let (previous, previousIsContact) = previous {
previousTags = postbox.seedConfiguration.peerSummaryCounterTags(previous, previousIsContact)
@ -198,13 +222,13 @@ final class ChatListIndexTable: Table {
}
if let previous = previous {
var isThreadBasedUnreadCalculation = postbox.seedConfiguration.peerSummaryIsThreadBased(updated.0).value
var isThreadBasedUnreadCalculation = postbox.seedConfiguration.peerSummaryIsThreadBased(updated.0, updatedAssociatedPeer).value
if let cachedData = updatedCachedPeerData[updated.0.id]?.1, postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedData) {
isThreadBasedUnreadCalculation = false
}
var wasThreadBasedUnreadCalculation = false
if postbox.seedConfiguration.peerSummaryIsThreadBased(previous.0).value {
if postbox.seedConfiguration.peerSummaryIsThreadBased(previous.0, previousAssociatedPeer).value {
if let cachedData = postbox.cachedPeerDataTable.get(previous.0.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedData) {
} else {
wasThreadBasedUnreadCalculation = true
@ -223,14 +247,15 @@ final class ChatListIndexTable: Table {
guard let peer = postbox.peerTable.get(peerId) else {
continue
}
let associatedPeer = peer.associatedPeerId.flatMap(postbox.peerTable.get)
var isThreadBasedUnreadCalculation = postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value
var isThreadBasedUnreadCalculation = postbox.seedConfiguration.peerSummaryIsThreadBased(peer, associatedPeer).value
if postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedDataUpdate.1) {
isThreadBasedUnreadCalculation = false
}
var wasThreadBasedUnreadCalculation = false
if postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value {
if postbox.seedConfiguration.peerSummaryIsThreadBased(peer, associatedPeer).value {
if let previousCachedData = cachedDataUpdate.0, postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(previousCachedData) {
} else {
wasThreadBasedUnreadCalculation = true
@ -439,7 +464,7 @@ final class ChatListIndexTable: Table {
displayAsRegularChat = true
}
if let peer = postbox.peerTable.get(peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value, !displayAsRegularChat {
if let peer = postbox.peerTable.get(peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value, !displayAsRegularChat {
let previousCount: Int32
if let previousSummary = alteredInitialPeerThreadsSummaries[peerId] {
previousCount = previousSummary.effectiveUnreadCount
@ -458,7 +483,7 @@ final class ChatListIndexTable: Table {
}
let currentReadState: CombinedPeerReadState?
if let peer = postbox.peerTable.get(peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value, !displayAsRegularChat {
if let peer = postbox.peerTable.get(peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value, !displayAsRegularChat {
let count = postbox.peerThreadsSummaryTable.get(peerId: peerId)?.effectiveUnreadCount ?? 0
currentReadState = CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 1, maxKnownId: 0, count: count, markedUnread: false))])
} else {
@ -691,7 +716,7 @@ final class ChatListIndexTable: Table {
}
let combinedState: CombinedPeerReadState?
if postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value {
if postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value {
let count: Int32 = postbox.peerThreadsSummaryTable.get(peerId: peerId)?.effectiveUnreadCount ?? 0
combinedState = CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 1, maxKnownId: 0, count: count, markedUnread: false))])
} else {
@ -788,7 +813,7 @@ final class ChatListIndexTable: Table {
}
let combinedState: CombinedPeerReadState?
if postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value {
if postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value {
let count: Int32 = postbox.peerThreadsSummaryTable.get(peerId: peerId)?.effectiveUnreadCount ?? 0
combinedState = CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 1, maxKnownId: 0, count: count, markedUnread: false))])
} else {

View File

@ -714,7 +714,7 @@ final class MutableChatListView {
let renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers))
let isUnread: Bool
if postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value {
if postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value {
let hasUnmutedUnread = postbox.peerThreadsSummaryTable.get(peerId: peer.id)?.hasUnmutedUnread ?? false
isUnread = hasUnmutedUnread
} else {
@ -846,7 +846,7 @@ final class MutableChatListView {
displayAsRegularChat = postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedData)
}
if let peer = groupEntries[i].renderedPeers[j].peer.peer, postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value, !displayAsRegularChat {
if let peer = groupEntries[i].renderedPeers[j].peer.peer, postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value, !displayAsRegularChat {
isUnread = postbox.peerThreadsSummaryTable.get(peerId: peer.id)?.hasUnmutedUnread ?? false
} else {
isUnread = postbox.readStateTable.getCombinedState(groupEntries[i].renderedPeers[j].peer.peerId)?.isUnread ?? false
@ -942,7 +942,7 @@ final class MutableChatListView {
var isThreadBased = false
var threadsArePeers = false
if let peer = renderedPeer.peer {
let value = postbox.seedConfiguration.peerSummaryIsThreadBased(peer)
let value = postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get))
isThreadBased = value.value
threadsArePeers = value.threadsArePeers
}

View File

@ -65,7 +65,7 @@ private func mappedChatListFilterPredicate(postbox: PostboxImpl, currentTransact
if let cachedPeerData = postbox.cachedPeerDataTable.get(peer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true
}
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value
if !isThreadBased {
displayAsRegularChat = true
}
@ -439,7 +439,7 @@ private final class ChatListViewSpaceState {
if let cachedPeerData = postbox.cachedPeerDataTable.get(peer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true
}
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value
if !isThreadBased {
displayAsRegularChat = true
}
@ -577,7 +577,7 @@ private final class ChatListViewSpaceState {
if let cachedPeerData = postbox.cachedPeerDataTable.get(entryPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true
}
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(entryPeer).value
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(entryPeer, entryPeer.associatedPeerId.flatMap(postbox.peerTable.get)).value
if !isThreadBased {
displayAsRegularChat = true
}
@ -630,7 +630,7 @@ private final class ChatListViewSpaceState {
if let cachedPeerData = postbox.cachedPeerDataTable.get(mainPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true
}
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(mainPeer).value
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(mainPeer, mainPeer.associatedPeerId.flatMap(postbox.peerTable.get)).value
if !isThreadBased {
displayAsRegularChat = true
}
@ -816,7 +816,7 @@ private final class ChatListViewSpaceState {
if let cachedPeerData = postbox.cachedPeerDataTable.get(entryPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true
}
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(entryPeer).value
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(entryPeer, entryPeer.associatedPeerId.flatMap(postbox.peerTable.get)).value
if !isThreadBased {
displayAsRegularChat = true
}
@ -878,7 +878,7 @@ private final class ChatListViewSpaceState {
if let cachedPeerData = postbox.cachedPeerDataTable.get(mainPeer.id), postbox.seedConfiguration.decodeDisplayPeerAsRegularChat(cachedPeerData) {
displayAsRegularChat = true
}
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(mainPeer).value
let isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(mainPeer, mainPeer.associatedPeerId.flatMap(postbox.peerTable.get)).value
if !isThreadBased {
displayAsRegularChat = true
}
@ -978,7 +978,7 @@ private final class ChatListViewSpaceState {
}
let isThreadBased: Bool
if let peer = postbox.peerTable.get(entryData.index.messageIndex.id.peerId) {
isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value
isThreadBased = postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value
} else {
isThreadBased = false
}
@ -1620,7 +1620,7 @@ struct ChatListViewState {
var isThreadBased = false
var threadsArePeers = false
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
let value = postbox.seedConfiguration.peerSummaryIsThreadBased(peer)
let value = postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get))
isThreadBased = value.value
threadsArePeers = value.threadsArePeers
}

View File

@ -3187,7 +3187,6 @@ final class PostboxImpl {
let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> = self.transactionSignal(userInteractive: true, { subscriber, transaction in
let peerIds = self.peerIdsForLocation(chatLocation, ignoreRelatedChats: false)
//TODO:release jump to unread
var anchor: HistoryViewInputAnchor = .upperBound
switch peerIds {
case let .single(peerId, threadId):

View File

@ -66,7 +66,7 @@ public final class SeedConfiguration {
public let existingGlobalMessageTags: GlobalMessageTags
public let peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace]
public let peerSummaryCounterTags: (Peer, Bool) -> PeerSummaryCounterTags
public let peerSummaryIsThreadBased: (Peer) -> (value: Bool, threadsArePeers: Bool)
public let peerSummaryIsThreadBased: (Peer, Peer?) -> (value: Bool, threadsArePeers: Bool)
public let additionalChatListIndexNamespace: MessageId.Namespace?
public let messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>
public let defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState]
@ -97,7 +97,7 @@ public final class SeedConfiguration {
existingGlobalMessageTags: GlobalMessageTags,
peerNamespacesRequiringMessageTextIndex: [PeerId.Namespace],
peerSummaryCounterTags: @escaping (Peer, Bool) -> PeerSummaryCounterTags,
peerSummaryIsThreadBased: @escaping (Peer) -> (value: Bool, threadsArePeers: Bool),
peerSummaryIsThreadBased: @escaping (Peer, Peer?) -> (value: Bool, threadsArePeers: Bool),
additionalChatListIndexNamespace: MessageId.Namespace?,
messageNamespacesRequiringGroupStatsValidation: Set<MessageId.Namespace>,
defaultMessageNamespaceReadStates: [MessageId.Namespace: PeerReadState],

View File

@ -64,7 +64,7 @@ final class MutableUnreadMessageCountsView: MutablePostboxView {
case let .totalInGroup(groupId):
return .totalInGroup(groupId, postbox.messageHistoryMetadataTable.getTotalUnreadState(groupId: groupId))
case let .peer(peerId, handleThreads):
if handleThreads, let peer = postbox.peerTable.get(peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value {
if handleThreads, let peer = postbox.peerTable.get(peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value {
var count: Int32 = 0
if let summary = postbox.peerThreadsSummaryTable.get(peerId: peerId) {
count = summary.totalUnreadCount
@ -113,7 +113,7 @@ final class MutableUnreadMessageCountsView: MutablePostboxView {
}
}
case let .peer(peerId, handleThreads, _):
if handleThreads, let peer = postbox.peerTable.get(peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value {
if handleThreads, let peer = postbox.peerTable.get(peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value {
if transaction.updatedPeerThreadsSummaries.contains(peerId) {
var count: Int32 = 0
if let summary = postbox.peerThreadsSummaryTable.get(peerId: peerId) {
@ -143,7 +143,7 @@ final class MutableUnreadMessageCountsView: MutablePostboxView {
case let .totalInGroup(groupId):
return .totalInGroup(groupId, postbox.messageHistoryMetadataTable.getTotalUnreadState(groupId: groupId))
case let .peer(peerId, handleThreads):
if handleThreads, let peer = postbox.peerTable.get(peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value {
if handleThreads, let peer = postbox.peerTable.get(peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value {
var count: Int32 = 0
if let summary = postbox.peerThreadsSummaryTable.get(peerId: peerId) {
count = summary.totalUnreadCount
@ -240,7 +240,7 @@ final class MutableCombinedReadStateView: MutablePostboxView {
var updated = false
if transaction.alteredInitialPeerCombinedReadStates[self.peerId] != nil || transaction.updatedPeerThreadCombinedStates.contains(self.peerId) {
if self.handleThreads, let peer = postbox.peerTable.get(self.peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value {
if self.handleThreads, let peer = postbox.peerTable.get(self.peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value {
var count: Int32 = 0
if let summary = postbox.peerThreadsSummaryTable.get(peerId: peerId) {
count = summary.totalUnreadCount
@ -260,14 +260,14 @@ final class MutableCombinedReadStateView: MutablePostboxView {
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
let state: CombinedPeerReadState?
if handleThreads, let peer = postbox.peerTable.get(peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer).value {
if self.handleThreads, let peer = postbox.peerTable.get(self.peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer, peer.associatedPeerId.flatMap(postbox.peerTable.get)).value {
var count: Int32 = 0
if let summary = postbox.peerThreadsSummaryTable.get(peerId: peerId) {
if let summary = postbox.peerThreadsSummaryTable.get(peerId: self.peerId) {
count = summary.totalUnreadCount
}
state = CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: 1, maxKnownId: 1, count: count, markedUnread: false))])
} else {
state = postbox.readStateTable.getCombinedState(peerId)
state = postbox.readStateTable.getCombinedState(self.peerId)
}
if state != self.state {

View File

@ -73,7 +73,7 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
return .nonContact
}
},
peerSummaryIsThreadBased: { peer in
peerSummaryIsThreadBased: { peer, associatedPeer in
if let channel = peer as? TelegramChannel {
if channel.flags.contains(.isForum) {
if channel.flags.contains(.displayForumAsTabs) {
@ -82,6 +82,7 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
return (true, false)
}
} else if channel.flags.contains(.isMonoforum) {
if let associatedPeer = associatedPeer as? TelegramChannel, associatedPeer.hasPermission(.sendSomething) {
return (true, true)
} else {
return (false, false)
@ -89,6 +90,9 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
} else {
return (false, false)
}
} else {
return (false, false)
}
},
additionalChatListIndexNamespace: Namespaces.Message.Cloud,
messageNamespacesRequiringGroupStatsValidation: [Namespaces.Message.Cloud],

View File

@ -140,8 +140,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
} else {
if forChatList {
if isMonoforum {
//TODO:Localize
attributedString = NSAttributedString(string: "No messages here yet...", font: titleFont, textColor: primaryTextColor)
attributedString = NSAttributedString(string: strings.ChatList_MonoforumEmptyText, font: titleFont, textColor: primaryTextColor)
} else {
attributedString = NSAttributedString(string: strings.Notification_CreatedGroup, font: titleFont, textColor: primaryTextColor)
}

View File

@ -526,14 +526,29 @@ public final class AsyncListComponent: Component {
var scrollToItem: ListViewScrollToItem?
if let resetScrollingRequest = component.externalStateValue.resetScrollingRequest, previousComponent?.externalStateValue.resetScrollingRequest != component.externalStateValue.resetScrollingRequest {
//TODO:release calculate direction hint
if let index = entries.firstIndex(where: { $0.id == resetScrollingRequest.id }) {
var directionHint: ListViewScrollToItemDirectionHint = .Down
var didSelectDirection = false
self.listNode.forEachItemNode { itemNode in
if didSelectDirection {
return
}
if let itemNode = itemNode as? ListItemNodeImpl, let itemIndex = itemNode.index {
if itemIndex <= index {
directionHint = .Up
} else {
directionHint = .Down
}
didSelectDirection = true
}
}
scrollToItem = ListViewScrollToItem(
index: index,
position: .visible,
animated: animateTransition,
curve: updateSizeAndInsets.curve,
directionHint: .Down
directionHint: directionHint
)
}
}
@ -548,13 +563,25 @@ public final class AsyncListComponent: Component {
transactionOptions.insert(.Synchronous)
self.listNode.transaction(
deleteIndices: [],
insertIndicesAndItems: [],
updateIndicesAndItems: [],
options: transactionOptions,
scrollToItem: nil,
updateSizeAndInsets: updateSizeAndInsets,
stationaryItemRange: nil,
updateOpaqueState: nil,
completion: { _ in }
)
self.listNode.transaction(
deleteIndices: deletions,
insertIndicesAndItems: insertions,
updateIndicesAndItems: updates,
options: transactionOptions,
scrollToItem: scrollToItem,
updateSizeAndInsets: updateSizeAndInsets,
updateSizeAndInsets: nil,
stationaryItemRange: nil,
updateOpaqueState: nil,
completion: { _ in }

View File

@ -381,14 +381,13 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = presentationData
//TODO:localize
let text: String = "Tap here to send a message"
let text: String = presentationData.strings.Chat_ChannelMessagesHint
let tooltipController = TooltipScreen(
account: context.account,
sharedContext: context.sharedContext,
text: .plain(text: text),
textBadge: "NEW",
textBadge: presentationData.strings.Chat_ChannelMessagesHintBadge,
balancedTextLayout: false,
style: .wide,
arrowStyle: .small,

View File

@ -619,8 +619,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private let shadowNode: ChatMessageShadowNode
private var clippingNode: ChatMessageBubbleClippingNode
private var suggestedPostInfoNode: ChatMessageSuggestedPostInfoNode?
override public var extractedBackgroundNode: ASDisplayNode? {
return self.shadowNode
}
@ -1427,8 +1425,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let weakSelf = Weak(self)
let makeSuggestedPostInfoNodeLayout: ChatMessageSuggestedPostInfoNode.AsyncLayout = ChatMessageSuggestedPostInfoNode.asyncLayout(self.suggestedPostInfoNode)
return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in
let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData)
return ChatMessageBubbleItemNode.beginLayout(
@ -1451,7 +1447,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
unlockButtonLayout: unlockButtonLayout,
mediaInfoLayout: mediaInfoLayout,
mosaicStatusLayout: mosaicStatusLayout,
makeSuggestedPostInfoNodeLayout: makeSuggestedPostInfoNodeLayout,
layoutConstants: layoutConstants,
currentItem: currentItem,
currentForwardInfo: currentForwardInfo,
@ -1480,7 +1475,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
unlockButtonLayout: (ChatMessageUnlockMediaNode.Arguments) -> (CGSize, (Bool) -> ChatMessageUnlockMediaNode),
mediaInfoLayout: (ChatMessageStarsMediaInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode),
mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)),
makeSuggestedPostInfoNodeLayout: ChatMessageSuggestedPostInfoNode.AsyncLayout,
layoutConstants: ChatMessageItemLayoutConstants,
currentItem: ChatMessageItem?,
currentForwardInfo: (Peer?, String?)?,
@ -3000,7 +2994,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
var totalContentNodesHeight: CGFloat = 0.0
var currentContainerGroupOverlap: CGFloat = 0.0
var detachedContentNodesHeight: CGFloat = 0.0
var additionalTopHeight: CGFloat = 0.0
let additionalTopHeight: CGFloat = 0.0
var mosaicStatusOrigin: CGPoint?
var unlockButtonPosition: CGPoint?
@ -3177,13 +3171,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth)
}
var suggestedPostInfoNodeLayout: (CGSize, () -> ChatMessageSuggestedPostInfoNode)?
suggestedPostInfoNodeLayout = nil
if let suggestedPostInfoNodeLayout {
additionalTopHeight += 4.0 + suggestedPostInfoNodeLayout.0.height + 8.0
}
let minimalContentSize: CGSize
if hideBackground {
minimalContentSize = CGSize(width: 1.0, height: 1.0)
@ -3320,7 +3307,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
avatarOffset: avatarOffset,
hidesHeaders: hidesHeaders,
disablesComments: disablesComments,
suggestedPostInfoNodeLayout: suggestedPostInfoNodeLayout,
alignment: alignment,
isSidePanelOpen: isSidePanelOpen
)
@ -3384,7 +3370,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
avatarOffset: CGFloat?,
hidesHeaders: Bool,
disablesComments: Bool,
suggestedPostInfoNodeLayout: (CGSize, () -> ChatMessageSuggestedPostInfoNode)?,
alignment: ChatMessageBubbleContentAlignment,
isSidePanelOpen: Bool
) -> Void {
@ -3468,22 +3453,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
strongSelf.backgroundNode.backgroundFrame = backgroundFrame
if let (suggestedPostInfoSize, suggestedPostInfoApply) = suggestedPostInfoNodeLayout {
let suggestedPostInfoNode = suggestedPostInfoApply()
if suggestedPostInfoNode !== strongSelf.suggestedPostInfoNode {
strongSelf.suggestedPostInfoNode?.removeFromSupernode()
strongSelf.suggestedPostInfoNode = suggestedPostInfoNode
strongSelf.mainContextSourceNode.contentNode.addSubnode(suggestedPostInfoNode)
let suggestedPostInfoFrame = CGRect(origin: CGPoint(x: floor((params.width - suggestedPostInfoSize.width) * 0.5), y: 4.0), size: suggestedPostInfoSize)
suggestedPostInfoNode.frame = suggestedPostInfoFrame
//animation.animator.updateFrame(layer: suggestedPostInfoNode.layer, frame: suggestedPostInfoFrame, completion: nil)
}
} else if let suggestedPostInfoNode = strongSelf.suggestedPostInfoNode {
strongSelf.suggestedPostInfoNode = nil
suggestedPostInfoNode.removeFromSupernode()
}
if let avatarOffset {
strongSelf.updateAttachedAvatarNodeOffset(offset: avatarOffset, transition: .animated(duration: 0.3, curve: .spring))
}

View File

@ -1,169 +0,0 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TextFormat
import AccountContext
import WallpaperBackgroundNode
import ChatMessageItem
import TelegramStringFormatting
public final class ChatMessageSuggestedPostInfoNode: ASDisplayNode {
private var titleNode: TextNode?
private var priceLabelNode: TextNode?
private var priceValueNode: TextNode?
private var timeLabelNode: TextNode?
private var timeValueNode: TextNode?
private var backgroundNode: WallpaperBubbleBackgroundNode?
override public init() {
super.init()
}
public typealias AsyncLayout = (ChatMessageItem, CGFloat) -> (CGSize, () -> ChatMessageSuggestedPostInfoNode)
public static func asyncLayout(_ node: ChatMessageSuggestedPostInfoNode?) -> (ChatMessageItem, CGFloat) -> (CGSize, () -> ChatMessageSuggestedPostInfoNode) {
let makeTitleLayout = TextNode.asyncLayout(node?.titleNode)
let makePriceLabelLayout = TextNode.asyncLayout(node?.priceLabelNode)
let makePriceValueLayout = TextNode.asyncLayout(node?.priceValueNode)
let makeTimeLabelLayout = TextNode.asyncLayout(node?.timeLabelNode)
let makeTimeValueLayout = TextNode.asyncLayout(node?.timeValueNode)
return { item, maxWidth in
let insets = UIEdgeInsets(
top: 12.0,
left: 12.0,
bottom: 12.0,
right: 12.0
)
let titleSpacing: CGFloat = 8.0
let labelSpacing: CGFloat = 8.0
let valuesVerticalSpacing: CGFloat = 2.0
var amount: Int64 = 0
var timestamp: Int32?
if "".isEmpty {
amount = 0
timestamp = nil
}
/*for attribute in item.message.attributes {
if let attribute = attribute as? OutgoingSuggestedPostMessageAttribute {
amount = attribute.price.value
timestamp = attribute.timestamp
}
}*/
//TODO:localize
let amountString: String
if amount == 0 {
amountString = "Free"
} else if amount == 1 {
amountString = "1 Star"
} else {
amountString = "\(amount) Stars"
}
var timestampString: String
if let timestamp {
timestampString = humanReadableStringForTimestamp(strings: item.presentationData.strings, dateTimeFormat: PresentationDateTimeFormat(), timestamp: timestamp, alwaysShowTime: true).string
if timestampString.count > 1 {
timestampString = String(timestampString[timestampString.startIndex]).capitalized + timestampString[timestampString.index(after: timestampString.startIndex)...]
}
} else {
timestampString = "Anytime"
}
let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
//TODO:localize
let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "You suggest to post\nthis message.", font: Font.regular(13.0), textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxWidth - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let priceLabelLayout = makePriceLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Price", font: Font.regular(13.0), textColor: serviceColor.primaryText.withMultipliedAlpha(0.5)), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxWidth - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let timeLabelLayout = makeTimeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Time", font: Font.regular(13.0), textColor: serviceColor.primaryText.withMultipliedAlpha(0.5)), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxWidth - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let priceValueLayout = makePriceValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: amountString, font: Font.semibold(13.0), textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxWidth - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let timeValueLayout = makeTimeValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: timestampString, font: Font.semibold(13.0), textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxWidth - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var maxContentWidth: CGFloat = 0.0
var contentHeight: CGFloat = 0.0
maxContentWidth = max(maxContentWidth, titleLayout.0.size.width)
contentHeight += titleLayout.0.size.height
contentHeight += titleSpacing
maxContentWidth = max(maxContentWidth, priceLabelLayout.0.size.width + labelSpacing + priceValueLayout.0.size.width)
contentHeight += priceLabelLayout.0.size.height + valuesVerticalSpacing
maxContentWidth = max(maxContentWidth, timeLabelLayout.0.size.width + labelSpacing + timeValueLayout.0.size.width)
contentHeight += timeLabelLayout.0.size.height
let size = CGSize(width: insets.left + insets.right + maxContentWidth, height: insets.top + insets.bottom + contentHeight)
return (size, {
let node = node ?? ChatMessageSuggestedPostInfoNode()
if node.backgroundNode == nil {
if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
node.backgroundNode = backgroundNode
backgroundNode.layer.masksToBounds = true
backgroundNode.layer.cornerRadius = 15.0
node.insertSubnode(backgroundNode, at: 0)
}
}
if let backgroundNode = node.backgroundNode {
backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
}
let titleNode = titleLayout.1()
if node.titleNode !== titleNode {
node.titleNode = titleNode
node.addSubnode(titleNode)
}
let priceLabelNode = priceLabelLayout.1()
if node.priceLabelNode !== priceLabelNode {
node.priceLabelNode = priceLabelNode
node.addSubnode(priceLabelNode)
}
let priceValueNode = priceValueLayout.1()
if node.priceValueNode !== priceValueNode {
node.priceValueNode = priceValueNode
node.addSubnode(priceValueNode)
}
let timeLabelNode = timeLabelLayout.1()
if node.timeLabelNode !== timeLabelNode {
node.timeLabelNode = timeLabelNode
node.addSubnode(timeLabelNode)
}
let timeValueNode = timeValueLayout.1()
if node.timeValueNode !== timeValueNode {
node.timeValueNode = timeValueNode
node.addSubnode(timeValueNode)
}
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.0.size.width) * 0.5), y: insets.top), size: titleLayout.0.size)
titleNode.frame = titleFrame
let priceLabelFrame = CGRect(origin: CGPoint(x: insets.left, y: titleFrame.maxY + titleSpacing), size: priceLabelLayout.0.size)
priceLabelNode.frame = priceLabelFrame
priceValueNode.frame = CGRect(origin: CGPoint(x: priceLabelFrame.maxX + labelSpacing, y: priceLabelFrame.minY), size: priceValueLayout.0.size)
let timeLabelFrame = CGRect(origin: CGPoint(x: insets.left, y: priceLabelFrame.maxY + valuesVerticalSpacing), size: timeLabelLayout.0.size)
timeLabelNode.frame = timeLabelFrame
timeValueNode.frame = CGRect(origin: CGPoint(x: timeLabelFrame.maxX + labelSpacing, y: timeLabelFrame.minY), size: timeValueLayout.0.size)
return node
})
}
}
}

View File

@ -139,8 +139,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
}
if case let .channel(channel) = peer, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = firstMessage.peers[linkedMonoforumId] {
//TODO:localize
title = authorString + "@" + EnginePeer(mainChannel).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder) + " Messages"
title = authorString + "@" + EnginePeer(mainChannel).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
} else {
if let threadData = item.threadData {
title = "\(authorString)\(threadData.info.title)"

View File

@ -417,8 +417,7 @@ public final class ChatSideTopicsPanel: Component {
if let threadData = component.item.item.threadData {
titleText = threadData.info.title
} else {
//TODO:localize
titleText = "General"
titleText = " "
}
} else {
titleText = component.item.item.renderedPeer.chatMainPeer?.compactDisplayTitle ?? " "
@ -847,8 +846,7 @@ public final class ChatSideTopicsPanel: Component {
if let threadData = component.item.item.threadData {
titleText = threadData.info.title
} else {
//TODO:localize
titleText = "General"
titleText = " "
}
} else {
titleText = component.item.item.renderedPeer.chatMainPeer?.compactDisplayTitle ?? " "
@ -1220,8 +1218,7 @@ public final class ChatSideTopicsPanel: Component {
containerSize: CGSize(width: 100.0, height: 100.0)
)
//TODO:localize
let titleText: String = "All"
let titleText: String = component.strings.Chat_InlineTopicMenu_AllTab
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
@ -1348,8 +1345,7 @@ public final class ChatSideTopicsPanel: Component {
let leftInset: CGFloat = 6.0
let rightInset: CGFloat = 12.0
//TODO:localize
let titleText: String = "All"
let titleText: String = component.strings.Chat_InlineTopicMenu_AllTab
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(

View File

@ -2219,8 +2219,7 @@ private func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostSt
interaction.editingOpenDiscussionGroupSetup()
}))
//TODO:localize
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPostSuggestionsSettings, label: .text(channel.linkedMonoforumId == nil ? "Off" : "On"), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Allow Channel Messages", icon: UIImage(bundleImageName: "Chat/Info/PostSuggestionsIcon"), action: {
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPostSuggestionsSettings, label: .text(channel.linkedMonoforumId == nil ? presentationData.strings.PeerInfo_AllowChannelMessages_Off : presentationData.strings.PeerInfo_AllowChannelMessages_On), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.PeerInfo_AllowChannelMessages, icon: UIImage(bundleImageName: "Chat/Info/PostSuggestionsIcon"), action: {
interaction.editingOpenPostSuggestionsSetup()
}))
}
@ -2357,8 +2356,7 @@ private func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostSt
}))
if channel.linkedMonoforumId != nil {
//TODO:localize
items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemChannelMessages, label: .none, text: "Channel Messages", icon: UIImage(bundleImageName: "Chat/Info/RecentActionsIcon"), action: {
items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemChannelMessages, label: .none, text: presentationData.strings.PeerInfo_ChannelMessages, icon: UIImage(bundleImageName: "Chat/Info/RecentActionsIcon"), action: {
interaction.openChannelMessages()
}))
}

View File

@ -566,8 +566,6 @@ extension ChatControllerImpl {
let initTimestamp = self.initTimestamp
#endif
//TODO:release ready must include chat node
self.ready.set(combineLatest(queue: .mainQueue(), [
self.contentDataReady.get(),
self.wallpaperReady.get(),

View File

@ -547,7 +547,6 @@ extension ChatControllerImpl {
strongSelf.state.chatTitleContent = .custom(strings.Chat_TitlePinnedMessages(Int32(displayedCount ?? 1)), nil, false)
} else if let channel = peer as? TelegramChannel, channel.isMonoForum {
if let linkedMonoforumId = channel.linkedMonoforumId, let mainPeer = peerView.peers[linkedMonoforumId] {
//TODO:localize
strongSelf.state.chatTitleContent = .custom(mainPeer.debugDisplayTitle, nil, false)
} else {
strongSelf.state.chatTitleContent = .custom(channel.debugDisplayTitle, nil, false)

View File

@ -49,486 +49,6 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
}
}
/*private final class ItemView: UIView {
private let context: AccountContext
private let action: () -> Void
private let extractedContainerNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let containerButton: HighlightTrackingButton
private var icon: ComponentView<Empty>?
private var avatarNode: AvatarNode?
private let title = ComponentView<Empty>()
private var badge: ComponentView<Empty>?
init(context: AccountContext, action: @escaping (() -> Void), contextGesture: @escaping (ContextGesture, ContextExtractedContentContainingNode) -> Void) {
self.context = context
self.action = action
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.containerButton = HighlightTrackingButton()
super.init(frame: CGRect())
self.extractedContainerNode.contentNode.view.addSubview(self.containerButton)
self.containerNode.addSubnode(self.extractedContainerNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.addSubview(self.containerNode.view)
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.containerButton.highligthedChanged = { [weak self] highlighted in
if let self, self.bounds.width > 0.0 {
let topScale: CGFloat = (self.bounds.width - 1.0) / self.bounds.width
let maxScale: CGFloat = (self.bounds.width + 1.0) / self.bounds.width
if highlighted {
self.layer.removeAnimation(forKey: "opacity")
self.layer.removeAnimation(forKey: "sublayerTransform")
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
transition.updateTransformScale(layer: self.layer, scale: topScale)
} else {
let transition: ContainedViewLayoutTransition = .immediate
transition.updateTransformScale(layer: self.layer, scale: 1.0)
self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
}
}
}
self.containerNode.isGestureEnabled = false
}
required init?(coder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
self.action()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var mappedPoint = point
if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) {
mappedPoint = self.bounds.center
}
return super.hitTest(mappedPoint, with: event)
}
func update(context: AccountContext, item: Item, isSelected: Bool, theme: PresentationTheme, height: CGFloat, transition: ComponentTransition) -> CGSize {
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
let spacing: CGFloat = 3.0
let badgeSpacing: CGFloat = 4.0
let iconSize = CGSize(width: 18.0, height: 18.0)
var avatarIconContent: EmojiStatusComponent.Content?
if case let .forum(topicId) = item.item.id {
if topicId != 1, let threadData = item.item.threadData {
if let fileId = threadData.info.icon, fileId != 0 {
avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: iconSize, placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(0))
} else {
avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: iconSize)
}
} else {
avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(theme), tintColor: theme.rootController.navigationBar.secondaryTextColor)
}
}
if let avatarIconContent {
let avatarIconComponent = EmojiStatusComponent(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
content: avatarIconContent,
isVisibleForAnimations: false,
action: nil
)
let icon: ComponentView<Empty>
if let current = self.icon {
icon = current
} else {
icon = ComponentView()
self.icon = icon
}
let _ = icon.update(
transition: .immediate,
component: AnyComponent(avatarIconComponent),
environment: {},
containerSize: CGSize(width: 18.0, height: 18.0)
)
} else if let icon = self.icon {
self.icon = nil
icon.view?.removeFromSuperview()
}
let titleText: String
if case let .forum(topicId) = item.item.id {
let _ = topicId
if let threadData = item.item.threadData {
titleText = threadData.info.title
} else {
//TODO:localize
titleText = "General"
}
} else {
titleText = item.item.renderedPeer.chatMainPeer?.compactDisplayTitle ?? " "
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: isSelected ? theme.rootController.navigationBar.accentTextColor : theme.rootController.navigationBar.secondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
var badgeSize: CGSize?
if let readCounters = item.item.readCounters, readCounters.count > 0 {
let badge: ComponentView<Empty>
var badgeTransition = transition
if let current = self.badge {
badge = current
} else {
badgeTransition = .immediate
badge = ComponentView<Empty>()
self.badge = badge
}
badgeSize = badge.update(
transition: badgeTransition,
component: AnyComponent(TextBadgeComponent(
text: countString(Int64(readCounters.count)),
font: Font.regular(12.0),
background: item.item.isMuted ? theme.chatList.unreadBadgeInactiveBackgroundColor : theme.chatList.unreadBadgeActiveBackgroundColor,
foreground: item.item.isMuted ? theme.chatList.unreadBadgeInactiveTextColor : theme.chatList.unreadBadgeActiveTextColor,
insets: UIEdgeInsets(top: 1.0, left: 5.0, bottom: 2.0, right: 5.0)
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
}
var contentSize: CGFloat = iconSize.width + spacing + titleSize.width
if let badgeSize {
contentSize += badgeSpacing + badgeSize.width
}
let size = CGSize(width: contentSize, height: height)
let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: 5.0 + floor((size.height - iconSize.height) * 0.5)), size: iconSize)
let titleFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: 5.0 + floor((size.height - titleSize.height) * 0.5)), size: titleSize)
if let icon = self.icon {
if let iconView = icon.view {
if iconView.superview == nil {
iconView.isUserInteractionEnabled = false
self.containerButton.addSubview(iconView)
}
iconView.frame = iconFrame
}
if let avatarNode = self.avatarNode {
self.avatarNode = nil
avatarNode.view.removeFromSuperview()
}
} else {
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 7.0))
self.avatarNode = avatarNode
self.containerButton.addSubview(avatarNode.view)
}
avatarNode.frame = iconFrame
avatarNode.updateSize(size: iconFrame.size)
if let peer = item.item.renderedPeer.chatMainPeer {
if peer.smallProfileImage != nil {
avatarNode.setPeerV2(context: context, theme: theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size)
} else {
avatarNode.setPeer(context: context, theme: theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size)
}
}
}
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
}
titleView.frame = titleFrame
}
if let badgeSize, let badge = self.badge {
let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + badgeSpacing, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize)
if let badgeView = badge.view {
if badgeView.superview == nil {
badgeView.isUserInteractionEnabled = false
self.containerButton.addSubview(badgeView)
badgeView.frame = badgeFrame
badgeView.alpha = 0.0
}
transition.setPosition(view: badgeView, position: badgeFrame.center)
transition.setBounds(view: badgeView, bounds: CGRect(origin: CGPoint(), size: badgeFrame.size))
transition.setScale(view: badgeView, scale: 1.0)
alphaTransition.setAlpha(view: badgeView, alpha: 1.0)
}
} else if let badge = self.badge {
self.badge = nil
if let badgeView = badge.view {
let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + badgeSpacing, y: titleFrame.minX + floorToScreenPixels((titleFrame.height - badgeView.bounds.height) * 0.5)), size: badgeView.bounds.size)
transition.setPosition(view: badgeView, position: badgeFrame.center)
transition.setScale(view: badgeView, scale: 0.001)
alphaTransition.setAlpha(view: badgeView, alpha: 0.0, completion: { [weak badgeView] _ in
badgeView?.removeFromSuperview()
})
}
}
transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size))
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
return size
}
}
private final class TabItemView: UIView {
private let context: AccountContext
private let action: () -> Void
private let extractedContainerNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let containerButton: HighlightTrackingButton
private let icon = ComponentView<Empty>()
init(context: AccountContext, action: @escaping (() -> Void)) {
self.context = context
self.action = action
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.containerButton = HighlightTrackingButton()
super.init(frame: CGRect())
self.extractedContainerNode.contentNode.view.addSubview(self.containerButton)
self.containerNode.addSubnode(self.extractedContainerNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.addSubview(self.containerNode.view)
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.containerButton.highligthedChanged = { [weak self] highlighted in
if let self, self.bounds.width > 0.0 {
let topScale: CGFloat = (self.bounds.width - 1.0) / self.bounds.width
let maxScale: CGFloat = (self.bounds.width + 1.0) / self.bounds.width
if highlighted {
self.layer.removeAnimation(forKey: "opacity")
self.layer.removeAnimation(forKey: "sublayerTransform")
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
transition.updateTransformScale(layer: self.layer, scale: topScale)
} else {
let transition: ContainedViewLayoutTransition = .immediate
transition.updateTransformScale(layer: self.layer, scale: 1.0)
self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
}
}
}
self.containerNode.isGestureEnabled = false
}
required init?(coder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
self.action()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var mappedPoint = point
if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) {
mappedPoint = self.bounds.center
}
return super.hitTest(mappedPoint, with: event)
}
func update(context: AccountContext, theme: PresentationTheme, height: CGFloat, transition: ComponentTransition) -> CGSize {
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(
name: "Chat/Title Panels/SidebarIcon",
tintColor: theme.rootController.navigationBar.secondaryTextColor,
maxSize: nil,
scaleFactor: 1.0
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let contentSize: CGFloat = iconSize.width
let size = CGSize(width: contentSize, height: height)
let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: 5.0 + floor((size.height - iconSize.height) * 0.5)), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
iconView.isUserInteractionEnabled = false
self.containerButton.addSubview(iconView)
}
iconView.frame = iconFrame
}
transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size))
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
return size
}
}
private final class AllItemView: UIView {
private let context: AccountContext
private let action: () -> Void
private let extractedContainerNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let containerButton: HighlightTrackingButton
private let title = ComponentView<Empty>()
init(context: AccountContext, action: @escaping (() -> Void)) {
self.context = context
self.action = action
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.containerButton = HighlightTrackingButton()
super.init(frame: CGRect())
self.extractedContainerNode.contentNode.view.addSubview(self.containerButton)
self.containerNode.addSubnode(self.extractedContainerNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.addSubview(self.containerNode.view)
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.containerButton.highligthedChanged = { [weak self] highlighted in
if let self, self.bounds.width > 0.0 {
let topScale: CGFloat = (self.bounds.width - 1.0) / self.bounds.width
let maxScale: CGFloat = (self.bounds.width + 1.0) / self.bounds.width
if highlighted {
self.layer.removeAnimation(forKey: "opacity")
self.layer.removeAnimation(forKey: "sublayerTransform")
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
transition.updateTransformScale(layer: self.layer, scale: topScale)
} else {
let transition: ContainedViewLayoutTransition = .immediate
transition.updateTransformScale(layer: self.layer, scale: 1.0)
self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
}
}
}
self.containerNode.isGestureEnabled = false
}
required init?(coder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
self.action()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var mappedPoint = point
if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) {
mappedPoint = self.bounds.center
}
return super.hitTest(mappedPoint, with: event)
}
func update(context: AccountContext, isSelected: Bool, theme: PresentationTheme, height: CGFloat, transition: ComponentTransition) -> CGSize {
//TODO:localize
let titleText: String = "All"
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: isSelected ? theme.rootController.navigationBar.accentTextColor : theme.rootController.navigationBar.secondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let contentSize: CGFloat = titleSize.width
let size = CGSize(width: contentSize, height: height)
let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: 5.0 + floor((size.height - titleSize.height) * 0.5)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
}
titleView.frame = titleFrame
}
transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size))
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
return size
}
}*/
private var params: Params?
private let context: AccountContext
@ -618,234 +138,11 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
}
transition.updateFrame(view: panelView, frame: panelFrame)
}
/*
let containerInsets = UIEdgeInsets(top: 0.0, left: params.leftInset + 16.0, bottom: 0.0, right: params.rightInset + 16.0)
let itemSpacing: CGFloat = 24.0
var leftContentInset: CGFloat = containerInsets.left + 8.0
do {
var itemTransition = transition
var animateIn = false
let itemView: TabItemView
if let current = self.tabItemView {
itemView = current
} else {
itemTransition = .immediate
animateIn = true
itemView = TabItemView(context: self.context, action: { [weak self] in
guard let self else {
return
}
self.interfaceInteraction?.toggleChatSidebarMode()
})
self.tabItemView = itemView
self.view.addSubview(itemView)
}
let itemSize = itemView.update(context: self.context, theme: params.interfaceState.theme, height: panelHeight, transition: .immediate)
let itemFrame = CGRect(origin: CGPoint(x: leftContentInset, y: -5.0), size: itemSize)
itemTransition.updatePosition(layer: itemView.layer, position: itemFrame.center)
itemTransition.updateBounds(layer: itemView.layer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
if animateIn && transition.isAnimated {
itemView.layer.animateAlpha(from: 0.0, to: itemView.alpha, duration: 0.15)
transition.animateTransformScale(view: itemView, from: 0.001)
}
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 {
if isFirst {
isFirst = false
} else {
contentSize.width += itemSpacing
}
var itemTransition = transition
var animateIn = false
let itemView: AllItemView
if let current = self.allItemView {
itemView = current
} else {
itemTransition = .immediate
animateIn = true
itemView = AllItemView(context: self.context, action: { [weak self] in
guard let self else {
return
}
self.interfaceInteraction?.updateChatLocationThread(nil, .left)
})
self.allItemView = itemView
self.scrollView.addSubview(itemView)
}
var isSelected = false
if params.interfaceState.chatLocation.threadId == nil {
isSelected = true
}
let itemSize = itemView.update(context: self.context, isSelected: isSelected, theme: params.interfaceState.theme, height: panelHeight, transition: .immediate)
let itemFrame = CGRect(origin: CGPoint(x: contentSize.width, y: -5.0), size: itemSize)
if isSelected {
selectedItemFrame = itemFrame
}
itemTransition.updatePosition(layer: itemView.layer, position: itemFrame.center)
itemTransition.updateBounds(layer: itemView.layer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
if animateIn && transition.isAnimated {
itemView.layer.animateAlpha(from: 0.0, to: itemView.alpha, duration: 0.15)
transition.animateTransformScale(view: itemView, from: 0.001)
}
contentSize.width += itemSize.width
}
for item in self.items {
if isFirst {
isFirst = false
} else {
contentSize.width += itemSpacing
}
let itemId = item.id
validIds.append(itemId)
var itemTransition = transition
var animateIn = false
let itemView: ItemView
if let current = self.itemViews[itemId] {
itemView = current
} else {
itemTransition = .immediate
animateIn = true
let chatListItem = item.item
itemView = ItemView(context: self.context, action: { [weak self] in
guard let self else {
return
}
let topicId: Int64
if case let .forum(topicIdValue) = chatListItem.id {
topicId = topicIdValue
} else {
topicId = chatListItem.renderedPeer.peerId.toInt64()
}
var direction = true
if let params = self.params, let lhsIndex = self.topicIndex(threadId: params.interfaceState.chatLocation.threadId), let rhsIndex = self.topicIndex(threadId: topicId) {
direction = lhsIndex < rhsIndex
}
self.interfaceInteraction?.updateChatLocationThread(topicId, direction ? .right : .left)
}, contextGesture: { gesture, sourceNode in
})
self.itemViews[itemId] = itemView
self.scrollView.addSubview(itemView)
}
var isSelected = false
if case let .forum(topicId) = item.item.id {
isSelected = params.interfaceState.chatLocation.threadId == topicId
} else {
isSelected = params.interfaceState.chatLocation.threadId == item.item.renderedPeer.peerId.toInt64()
}
let itemSize = itemView.update(context: self.context, item: item, isSelected: isSelected, theme: params.interfaceState.theme, height: panelHeight, transition: .immediate)
let itemFrame = CGRect(origin: CGPoint(x: contentSize.width, y: -5.0), size: itemSize)
if isSelected {
selectedItemFrame = itemFrame
}
itemTransition.updatePosition(layer: itemView.layer, position: itemFrame.center)
itemTransition.updateBounds(layer: itemView.layer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
if animateIn && transition.isAnimated {
itemView.layer.animateAlpha(from: 0.0, to: itemView.alpha, duration: 0.15)
transition.animateTransformScale(view: itemView, from: 0.001)
}
contentSize.width += itemSize.width
}
var removedIds: [Item.Id] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removedIds.append(id)
if transition.isAnimated {
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak itemView] _ in
itemView?.removeFromSuperview()
})
transition.updateTransformScale(layer: itemView.layer, scale: 0.001)
} else {
itemView.removeFromSuperview()
}
}
}
for id in removedIds {
self.itemViews.removeValue(forKey: id)
}
if let selectedItemFrame {
let lineFrame = CGRect(origin: CGPoint(x: selectedItemFrame.minX, y: panelHeight - 4.0), size: CGSize(width: selectedItemFrame.width + 4.0, height: 4.0))
if self.selectedLineView.isHidden {
self.selectedLineView.isHidden = false
self.selectedLineView.frame = lineFrame
} else {
transition.updateFrame(view: self.selectedLineView, frame: lineFrame)
}
} else {
self.selectedLineView.isHidden = true
}
contentSize.width += containerInsets.right
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 {
self.scrollView.frame = CGRect(origin: CGPoint(), size: scrollSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollToId: ScrollId
if let threadId = params.interfaceState.chatLocation.threadId {
scrollToId = .topic(threadId)
} else {
scrollToId = .all
}
if self.appliedScrollToId != scrollToId {
if case let .topic(threadId) = scrollToId {
if let itemView = self.itemViews[.forum(threadId)] {
self.appliedScrollToId = scrollToId
self.scrollView.scrollRectToVisible(itemView.frame.insetBy(dx: -46.0, dy: 0.0), animated: hadItemViews)
}
} else if case .all = scrollToId {
self.appliedScrollToId = scrollToId
self.scrollView.scrollRectToVisible(CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), animated: hadItemViews)
} else {
self.appliedScrollToId = scrollToId
}
}*/
}
public func updateGlobalOffset(globalOffset: CGFloat, transition: ComponentTransition) {
if let panelView = self.panel.view as? ChatSideTopicsPanel.View {
panelView.updateGlobalOffset(globalOffset: globalOffset, transition: transition)
//transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(0.0, -globalOffset, 0.0))
}
}
@ -855,23 +152,5 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
} else {
return nil
}
/*if let threadId {
if let value = self.items.firstIndex(where: { item in
if item.id == .chatList(PeerId(threadId)) {
return true
} else if item.id == .forum(threadId) {
return true
} else {
return false
}
}) {
return value + 1
} else {
return nil
}
} else {
return 0
}*/
}
}