[WIP] Topics

This commit is contained in:
Ali 2022-10-22 22:15:33 +04:00
parent d9ab563c94
commit e0746fa0c2
19 changed files with 289 additions and 51 deletions

View File

@ -277,6 +277,7 @@ public enum ResolvedUrl {
case groupBotStart(peerId: PeerId, payload: String, adminRights: ResolvedBotAdminRights?)
case channelMessage(peer: Peer, messageId: MessageId, timecode: Double?)
case replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage, messageId: MessageId)
case replyThread(messageId: MessageId)
case stickerPack(name: String, type: StickerPackUrlType)
case instantView(TelegramMediaWebpage, String?)
case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?)
@ -737,7 +738,7 @@ public protocol SharedAccountContext: AnyObject {
func makePrivacyAndSecurityController(context: AccountContext) -> ViewController
func navigateToChatController(_ params: NavigateToChatControllerParams)
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?) -> Signal<Never, NoError>
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError>
func chatControllerForForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64) -> Signal<ChatController, NoError>
func openStorageUsage(context: AccountContext)
func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController)

View File

@ -31,8 +31,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
public let isPremium: Bool
public let forceInlineReactions: Bool
public let accountPeer: EnginePeer?
public let topicAuthorId: EnginePeer.Id?
public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set<EnginePeer.Id> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?, forceInlineReactions: Bool = false) {
public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set<EnginePeer.Id> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?, forceInlineReactions: Bool = false, topicAuthorId: EnginePeer.Id? = nil) {
self.automaticDownloadPeerType = automaticDownloadPeerType
self.automaticDownloadNetworkType = automaticDownloadNetworkType
self.isRecentActions = isRecentActions
@ -49,6 +50,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
self.isPremium = isPremium
self.accountPeer = accountPeer
self.forceInlineReactions = forceInlineReactions
self.topicAuthorId = topicAuthorId
}
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
@ -97,6 +99,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
if lhs.forceInlineReactions != rhs.forceInlineReactions {
return false
}
if lhs.topicAuthorId != rhs.topicAuthorId {
return false
}
return true
}
}

View File

@ -1372,7 +1372,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.context.sharedContext.navigateToForumChannel(context: strongSelf.context, peerId: channel.id, navigationController: navigationController)
} else {
if let threadId = threadId {
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil).start()
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, keepStack: .never).start()
strongSelf.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
} else {
var navigationAnimationOptions: NavigationAnimationOptions = []
@ -1473,7 +1473,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
navigationAnimationOptions = .removeOnMasterDetails
}
if let threadId = threadId {
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil).start()
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, keepStack: .never).start()
} else {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: actualPeerId), subject: .message(id: .id(messageId), highlight: true, timecode: nil), purposefulAction: {
if deactivateOnAction {
@ -1506,7 +1506,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
navigationAnimationOptions = .removeOnMasterDetails
}
if let threadId = threadId {
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil).start()
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, keepStack: .never).start()
} else {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peer.id), purposefulAction: { [weak self] in
self?.deactivateSearch(animated: false)
@ -1602,7 +1602,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: ForumCreateTopicScreen.iconColors.randomElement()!, iconFileId: fileId)
|> deliverOnMainQueue).start(next: { topicId in
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text).start()
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text, keepStack: .never).start()
}, error: { _ in
controller?.isInProgress = false
})
@ -2670,7 +2670,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: ForumCreateTopicScreen.iconColors.randomElement()!, iconFileId: fileId)
|> deliverOnMainQueue).start(next: { topicId in
if let navigationController = (sourceController.navigationController as? NavigationController) {
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text).start()
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text, keepStack: .never).start()
}
}, error: { _ in
controller?.isInProgress = false

View File

@ -2534,7 +2534,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let currentMutedIconImage = currentMutedIconImage {
strongSelf.mutedIconNode.image = currentMutedIconImage
strongSelf.mutedIconNode.isHidden = false
transition.updateFrame(node: strongSelf.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin - 5.0, y: floorToScreenPixels(titleFrame.midY - currentMutedIconImage.size.height / 2.0) - UIScreenPixel), size: currentMutedIconImage.size))
transition.updateFrame(node: strongSelf.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin - 5.0, y: titleFrame.minY - 1.0 - UIScreenPixel), size: currentMutedIconImage.size))
nextTitleIconOrigin += currentMutedIconImage.size.width + 1.0
} else {
strongSelf.mutedIconNode.image = nil

View File

@ -196,6 +196,12 @@ public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13
}, initialValues: [:], queue: queue)
}
public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, E>(queue: Queue? = nil, _ s1: Signal<T1, E>, _ s2: Signal<T2, E>, _ s3: Signal<T3, E>, _ s4: Signal<T4, E>, _ s5: Signal<T5, E>, _ s6: Signal<T6, E>, _ s7: Signal<T7, E>, _ s8: Signal<T8, E>, _ s9: Signal<T9, E>, _ s10: Signal<T10, E>, _ s11: Signal<T11, E>, _ s12: Signal<T12, E>, _ s13: Signal<T13, E>, _ s14: Signal<T14, E>, _ s15: Signal<T15, E>, _ s16: Signal<T16, E>, _ s17: Signal<T17, E>, _ s18: Signal<T18, E>, _ s19: Signal<T19, E>) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19), E> {
return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19)], combine: { values in
return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19)
}, initialValues: [:], queue: queue)
}
public func combineLatest<T, E>(queue: Queue? = nil, _ signals: [Signal<T, E>]) -> Signal<[T], E> {
if signals.count == 0 {
return single([T](), E.self)

View File

@ -238,6 +238,32 @@ func _internal_createForumChannelTopic(account: Account, peerId: PeerId, title:
}
}
func _internal_fetchForumChannelTopic(account: Account, peerId: PeerId, threadId: Int64) -> Signal<EngineMessageHistoryThread.Info?, NoError> {
return account.postbox.transaction { transaction -> (info: EngineMessageHistoryThread.Info?, inputChannel: Api.InputChannel?) in
if let data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
return (data.info, nil)
} else {
return (nil, transaction.getPeer(peerId).flatMap(apiInputChannel))
}
}
|> mapToSignal { info, _ -> Signal<EngineMessageHistoryThread.Info?, NoError> in
if let info = info {
return .single(info)
} else {
return resolveForumThreads(postbox: account.postbox, network: account.network, ids: [MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))])
|> mapToSignal { _ -> Signal<EngineMessageHistoryThread.Info?, NoError> in
return account.postbox.transaction { transaction -> EngineMessageHistoryThread.Info? in
if let data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
return data.info
} else {
return nil
}
}
}
}
}
}
public enum EditForumChannelTopicError {
case generic
}

View File

@ -832,6 +832,10 @@ public extension TelegramEngine {
return _internal_createForumChannelTopic(account: self.account, peerId: id, title: title, iconColor: iconColor, iconFileId: iconFileId)
}
public func fetchForumChannelTopic(id: EnginePeer.Id, threadId: Int64) -> Signal<EngineMessageHistoryThread.Info?, NoError> {
return _internal_fetchForumChannelTopic(account: self.account, peerId: id, threadId: threadId)
}
public func editForumChannelTopic(id: EnginePeer.Id, threadId: Int64, title: String, iconFileId: Int64?) -> Signal<Never, EditForumChannelTopicError> {
return _internal_editForumChannelTopic(account: self.account, peerId: id, threadId: threadId, title: title, iconFileId: iconFileId)
}

View File

@ -314,7 +314,7 @@ private final class ChatHistoryTransactionOpaqueState {
}
}
private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?) -> ChatMessageItemAssociatedData {
private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?, topicAuthorId: EnginePeer.Id?) -> ChatMessageItemAssociatedData {
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
var contactsPeerIds: Set<PeerId> = Set()
var channelDiscussionGroup: ChatMessageItemAssociatedData.ChannelDiscussionGroupStatus = .unknown
@ -363,7 +363,7 @@ private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHist
}
}
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer)
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, topicAuthorId: topicAuthorId)
}
private extension ChatHistoryLocationInput {
@ -1032,6 +1032,17 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
|> distinctUntilChanged
let topicAuthorId: Signal<EnginePeer.Id?, NoError>
if let peerId = chatLocation.peerId, let threadId = chatLocation.threadId {
topicAuthorId = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.ThreadData(id: peerId, threadId: threadId))
|> map { data -> EnginePeer.Id? in
return data?.author
}
|> distinctUntilChanged
} else {
topicAuthorId = .single(nil)
}
let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue,
historyViewUpdate,
self.chatPresentationDataPromise.get(),
@ -1050,8 +1061,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
adMessages,
availableReactions,
defaultReaction,
accountPeer
).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageIdAndType, scrollToMessageId, adMessages, availableReactions, defaultReaction, accountPeer in
accountPeer,
topicAuthorId
).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageIdAndType, scrollToMessageId, adMessages, availableReactions, defaultReaction, accountPeer, topicAuthorId in
let currentlyPlayingMessageId = currentlyPlayingMessageIdAndType?.0
func applyHole() {
@ -1185,7 +1197,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
isPremium = true
}
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer)
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, topicAuthorId: topicAuthorId)
let filteredEntries = chatHistoryEntriesForView(
location: chatLocation,

View File

@ -1392,6 +1392,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
}
authorRank = attributes.rank
}
if authorRank == nil {
if let topicAuthorId = item.associatedData.topicAuthorId, topicAuthorId == message.author?.id {
//TODO:localize
authorRank = .custom("Topic Author")
}
}
case .group:
break
}

View File

@ -1571,7 +1571,7 @@ private class QrContentNode: ASDisplayNode, ContentNode {
codeLink = ""
}
if let threadId = threadId {
codeLink += "?topic=\(threadId)"
codeLink += "/\(threadId)"
}
let codeReadyPromise = ValuePromise<Bool>()

View File

@ -901,6 +901,10 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
if let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(message: replyThreadMessage), subject: .message(id: .id(messageId), highlight: true, timecode: nil)))
}
case let .replyThread(messageId):
if let navigationController = strongSelf.getNavigationController() {
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: messageId.peerId, threadId: Int64(messageId.id), messageId: nil, navigationController: navigationController, activateInput: nil, keepStack: .always).start()
}
case let .stickerPack(name, type):
let _ = type
let packReference: StickerPackReference = .name(name)

View File

@ -227,7 +227,7 @@ public func isOverlayControllerForChatNotificationOverlayPresentation(_ controll
return false
}
public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?) -> Signal<Never, NoError> {
public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError> {
return fetchAndPreloadReplyThreadInfo(context: context, subject: .groupMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))), atMessageId: messageId, preload: false)
|> deliverOnMainQueue
|> beforeNext { [weak context, weak navigationController] result in
@ -248,7 +248,7 @@ public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePee
chatLocationContextHolder: result.contextHolder,
subject: messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) },
activateInput: actualActivateInput,
keepStack: .never
keepStack: keepStack
)
)
}

View File

@ -163,6 +163,10 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
present(c, a)
}, messageId: replyThreadMessage.messageId, isChannelPost: replyThreadMessage.isChannelPost, atMessage: messageId, displayModalProgress: true).start()
}
case let .replyThread(messageId):
if let navigationController = navigationController {
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: messageId.peerId, threadId: Int64(messageId.id), messageId: nil, navigationController: navigationController, activateInput: nil, keepStack: .always).start()
}
case let .stickerPack(name, _):
dismissInput()

View File

@ -1,19 +1,22 @@
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AppBundle
final class PeerInfoScreenSwitchItem: PeerInfoScreenItem {
let id: AnyHashable
let text: String
let value: Bool
let icon: UIImage?
let isLocked: Bool
let toggled: ((Bool) -> Void)?
init(id: AnyHashable, text: String, value: Bool, icon: UIImage? = nil, toggled: ((Bool) -> Void)?) {
init(id: AnyHashable, text: String, value: Bool, icon: UIImage? = nil, isLocked: Bool = false, toggled: ((Bool) -> Void)?) {
self.id = id
self.text = text
self.value = value
self.icon = icon
self.isLocked = isLocked
self.toggled = toggled
}
@ -28,7 +31,9 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
private let iconNode: ASImageNode
private let textNode: ImmediateTextNode
private let switchNode: SwitchNode
private var lockedIconNode: ASImageNode?
private let bottomSeparatorNode: ASDisplayNode
private var lockedButtonNode: HighlightableButtonNode?
private let activateArea: AccessibilityAreaNode
private var item: PeerInfoScreenSwitchItem?
@ -91,12 +96,51 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
let firstTime = self.item == nil
var updateLockedIconImage = false
if item.isLocked {
let lockedIconNode: ASImageNode
if let current = self.lockedIconNode {
lockedIconNode = current
} else {
updateLockedIconImage = true
lockedIconNode = ASImageNode()
self.lockedIconNode = lockedIconNode
self.insertSubnode(lockedIconNode, aboveSubnode: self.switchNode)
}
} else if let lockedIconNode = self.lockedIconNode {
self.lockedIconNode = nil
lockedIconNode.removeFromSupernode()
}
if item.isLocked {
self.switchNode.isUserInteractionEnabled = false
if self.lockedButtonNode == nil {
let lockedButtonNode = HighlightableButtonNode()
self.lockedButtonNode = lockedButtonNode
self.insertSubnode(lockedButtonNode, aboveSubnode: self.switchNode)
lockedButtonNode.addTarget(self, action: #selector(self.lockedButtonPressed), forControlEvents: .touchUpInside)
}
} else {
if let lockedButtonNode = self.lockedButtonNode {
self.lockedButtonNode = nil
lockedButtonNode.removeFromSupernode()
}
self.switchNode.isUserInteractionEnabled = true
}
if self.theme !== presentationData.theme {
self.theme = presentationData.theme
self.switchNode.frameColor = presentationData.theme.list.itemSwitchColors.frameColor
self.switchNode.contentColor = presentationData.theme.list.itemSwitchColors.contentColor
self.switchNode.handleColor = presentationData.theme.list.itemSwitchColors.handleColor
updateLockedIconImage = true
}
if updateLockedIconImage, let lockedIconNode = self.lockedIconNode, let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: presentationData.theme.list.itemSecondaryTextColor) {
lockedIconNode.image = image
}
self.item = item
@ -143,10 +187,17 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
}
let switchSize = switchView.bounds.size
self.switchNode.frame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - safeInsets.right, y: floor((height - switchSize.height) / 2.0)), size: switchSize)
let switchFrame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - safeInsets.right, y: floor((height - switchSize.height) / 2.0)), size: switchSize)
self.switchNode.frame = switchFrame
if switchView.isOn != item.value {
switchView.setOn(item.value, animated: !firstTime)
}
self.lockedButtonNode?.frame = switchFrame
if let lockedIconNode = self.lockedIconNode, let icon = lockedIconNode.image {
lockedIconNode.frame = CGRect(origin: CGPoint(x: switchFrame.minX + 10.0 + UIScreenPixel, y: switchFrame.minY + 9.0), size: icon.size)
}
}
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
@ -168,4 +219,8 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
return height
}
@objc private func lockedButtonPressed() {
self.item?.toggled?(self.switchNode.isOn)
}
}

View File

@ -188,6 +188,7 @@ final class PeerInfoScreenData {
let requests: PeerInvitationImportersState?
let requestsContext: PeerInvitationImportersContext?
let threadData: MessageHistoryThreadData?
let appConfiguration: AppConfiguration?
init(
peer: Peer?,
@ -206,7 +207,8 @@ final class PeerInfoScreenData {
invitations: PeerExportedInvitationsState?,
requests: PeerInvitationImportersState?,
requestsContext: PeerInvitationImportersContext?,
threadData: MessageHistoryThreadData?
threadData: MessageHistoryThreadData?,
appConfiguration: AppConfiguration?
) {
self.peer = peer
self.chatPeer = chatPeer
@ -225,6 +227,7 @@ final class PeerInfoScreenData {
self.requests = requests
self.requestsContext = requestsContext
self.threadData = threadData
self.appConfiguration = appConfiguration
}
}
@ -435,7 +438,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
})
var enableQRLogin = false
if let appConfiguration = accountPreferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self), let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR {
let appConfiguration = accountPreferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self)
if let appConfiguration, let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR {
enableQRLogin = true
}
@ -477,7 +481,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
invitations: nil,
requests: nil,
requestsContext: nil,
threadData: nil
threadData: nil,
appConfiguration: appConfiguration
)
}
}
@ -504,7 +509,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
invitations: nil,
requests: nil,
requestsContext: nil,
threadData: nil
threadData: nil,
appConfiguration: nil
))
case let .user(userPeerId, secretChatId, kind):
let groupsInCommon: GroupsInCommonContext?
@ -635,7 +641,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
invitations: nil,
requests: nil,
requestsContext: nil,
threadData: nil
threadData: nil,
appConfiguration: nil
)
}
case .channel:
@ -711,7 +718,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
invitations: invitations,
requests: requests,
requestsContext: currentRequestsContext,
threadData: nil
threadData: nil,
appConfiguration: nil
)
}
case let .group(groupId):
@ -840,9 +848,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
invitationsStatePromise.get(),
requestsContextPromise.get(),
requestsStatePromise.get(),
threadData
threadData,
context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
)
|> map { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, threadData -> PeerInfoScreenData in
|> map { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, threadData, preferencesView -> PeerInfoScreenData in
var discussionPeer: Peer?
if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] {
discussionPeer = peer
@ -890,6 +899,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings
}
let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue
return PeerInfoScreenData(
peer: peerView.peers[groupId],
chatPeer: peerView.peers[groupId],
@ -907,7 +918,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
invitations: invitations,
requests: requests,
requestsContext: currentRequestsContext,
threadData: threadData
threadData: threadData,
appConfiguration: appConfiguration
)
}
}

View File

@ -509,6 +509,7 @@ private final class PeerInfoInteraction {
let editingOpenReactionsSetup: () -> Void
let dismissInput: () -> Void
let toggleForumTopics: (Bool) -> Void
let displayTopicsLimited: (Int) -> Void
init(
openUsername: @escaping (String) -> Void,
@ -553,7 +554,8 @@ private final class PeerInfoInteraction {
openQrCode: @escaping () -> Void,
editingOpenReactionsSetup: @escaping () -> Void,
dismissInput: @escaping () -> Void,
toggleForumTopics: @escaping (Bool) -> Void
toggleForumTopics: @escaping (Bool) -> Void,
displayTopicsLimited: @escaping (Int) -> Void
) {
self.openUsername = openUsername
self.openPhone = openPhone
@ -598,6 +600,7 @@ private final class PeerInfoInteraction {
self.editingOpenReactionsSetup = editingOpenReactionsSetup
self.dismissInput = dismissInput
self.toggleForumTopics = toggleForumTopics
self.displayTopicsLimited = displayTopicsLimited
}
}
@ -1094,7 +1097,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
threadId = Int64(message.messageId.id)
}
let linkText = "https://t.me/\(mainUsername)?topic=\(threadId)"
let linkText = "https://t.me/\(mainUsername)/\(threadId)"
items[.peerInfo]!.append(
PeerInfoScreenLabeledValueItem(
@ -1611,12 +1614,33 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
}))
}
if isCreator {
//TODO:localize
items[.peerDataSettings]!.append(PeerInfoScreenSwitchItem(id: ItemTopics, text: "Topics", value: channel.flags.contains(.isForum), icon: UIImage(bundleImageName: "Settings/Menu/ChatListFilters"), toggled: { value in
interaction.toggleForumTopics(value)
}))
items[.peerDataSettings]!.append(PeerInfoScreenCommentItem(id: ItemTopicsText, text: "The group chat will be divided into topics created by admins or users."))
if isCreator, let appConfiguration = data.appConfiguration {
var minParticipants = 5
if let data = appConfiguration.data, let value = data["forum_min_participants"] as? Int {
minParticipants = value
}
var canSetupTopics = false
var areTopicsLocked = true
if channel.flags.contains(.isForum) {
canSetupTopics = true
areTopicsLocked = false
} else if let memberCount = cachedData.participantsSummary.memberCount {
canSetupTopics = true
areTopicsLocked = Int(memberCount) < minParticipants
}
if canSetupTopics {
//TODO:localize
items[.peerDataSettings]!.append(PeerInfoScreenSwitchItem(id: ItemTopics, text: "Topics", value: channel.flags.contains(.isForum), icon: UIImage(bundleImageName: "Settings/Menu/ChatListFilters"), isLocked: areTopicsLocked, toggled: { value in
if areTopicsLocked {
interaction.displayTopicsLimited(minParticipants)
} else {
interaction.toggleForumTopics(value)
}
}))
items[.peerDataSettings]!.append(PeerInfoScreenCommentItem(id: ItemTopicsText, text: "The group chat will be divided into topics created by admins or users."))
}
}
var canViewAdminsAndBanned = false
@ -2050,6 +2074,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
return
}
let _ = strongSelf.context.engine.peers.setChannelForumMode(id: strongSelf.peerId, isForum: value).start()
},
displayTopicsLimited: { [weak self] minCount in
guard let self else {
return
}
//TODO:localize
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let text = "Only groups with more than **\(minCount) members** can have topics enabled."
self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_mute_for", scale: 0.066, colors: [:], title: nil, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
)
@ -6810,7 +6844,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
return
}
var threadId: Int64 = 0
var threadId: Int64?
if case let .replyThread(message) = self.chatLocation {
threadId = Int64(message.messageId.id)
}

View File

@ -1159,8 +1159,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
navigateToForumChannelImpl(context: context, peerId: peerId, navigationController: navigationController)
}
public func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?) -> Signal<Never, NoError> {
return navigateToForumThreadImpl(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: activateInput)
public func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError> {
return navigateToForumThreadImpl(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: activateInput, keepStack: keepStack)
}
public func chatControllerForForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64) -> Signal<ChatController, NoError> {

View File

@ -70,6 +70,10 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate
controller?.present(c, in: .window(.root), with: a)
}, messageId: replyThreadMessage.messageId, isChannelPost: replyThreadMessage.isChannelPost, atMessage: messageId, displayModalProgress: true).start()
}
case let .replyThread(messageId):
if let navigationController = controller.navigationController as? NavigationController {
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: messageId.peerId, threadId: Int64(messageId.id), messageId: nil, navigationController: navigationController, activateInput: nil, keepStack: .always).start()
}
case let .stickerPack(name, _):
let packReference: StickerPackReference = .name(name)
controller.present(StickerPackScreen(context: context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root))

View File

@ -275,7 +275,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
return .invoice(component)
}
return .peer(.name(peerName), nil)
} else if pathComponents.count == 2 || pathComponents.count == 3 {
} else if pathComponents.count == 2 || pathComponents.count == 3 || pathComponents.count == 4 {
if pathComponents[0] == "addstickers" {
return .stickerPack(name: pathComponents[1], type: .stickers)
} else if pathComponents[0] == "addemoji" {
@ -429,6 +429,24 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
} else {
return nil
}
} else if pathComponents.count == 4 && pathComponents[0] == "c" {
if let channelId = Int64(pathComponents[1]), let threadId = Int32(pathComponents[2]), let messageId = Int32(pathComponents[3]) {
var timecode: Double?
if let queryItems = components.queryItems {
for queryItem in queryItems {
if let value = queryItem.value {
if queryItem.name == "t" {
if let doubleValue = Double(value) {
timecode = doubleValue
}
}
}
}
}
return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode)
} else {
return nil
}
} else if pathComponents.count == 2 && pathComponents[0] == "c" {
if let channelId = Int64(pathComponents[1]) {
var threadId: Int32?
@ -475,7 +493,10 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
}
}
}
if let threadId = threadId {
if pathComponents.count >= 3, let subMessageId = Int32(pathComponents[2]) {
return .peer(.name(peerName), .replyThread(value, subMessageId))
} else if let threadId = threadId {
return .peer(.name(peerName), .replyThread(threadId, value))
} else if let commentId = commentId {
return .peer(.name(peerName), .replyThread(value, commentId))
@ -569,19 +590,42 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
}
}
case let .channelMessage(id, timecode):
return .single(.channelMessage(peer: peer, messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), timecode: timecode))
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: Int64(id))
|> map { info -> ResolvedUrl? in
if let _ = info {
return .replyThread(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: id))
} else {
return .peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))
}
}
} else {
return .single(.channelMessage(peer: peer, messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), timecode: timecode))
}
case let .replyThread(id, replyId):
let replyThreadMessageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id)
return context.engine.messages.fetchChannelReplyThreadMessage(messageId: replyThreadMessageId, atMessageId: nil)
|> map(Optional.init)
|> `catch` { _ -> Signal<ChatReplyThreadMessage?, NoError> in
return .single(nil)
}
|> map { result -> ResolvedUrl? in
guard let result = result else {
return .channelMessage(peer: peer, messageId: replyThreadMessageId, timecode: nil)
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: Int64(replyThreadMessageId.id))
|> map { info -> ResolvedUrl? in
if let _ = info {
return .replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: replyThreadMessageId.id)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: replyId))
} else {
return .peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))
}
}
} else {
return context.engine.messages.fetchChannelReplyThreadMessage(messageId: replyThreadMessageId, atMessageId: nil)
|> map(Optional.init)
|> `catch` { _ -> Signal<ChatReplyThreadMessage?, NoError> in
return .single(nil)
}
|> map { result -> ResolvedUrl? in
guard let result = result else {
return .channelMessage(peer: peer, messageId: replyThreadMessageId, timecode: nil)
}
return .replyThreadMessage(replyThreadMessage: result, messageId: MessageId(peerId: result.messageId.peerId, namespace: Namespaces.Message.Cloud, id: replyId))
}
return .replyThreadMessage(replyThreadMessage: result, messageId: MessageId(peerId: result.messageId.peerId, namespace: Namespaces.Message.Cloud, id: replyId))
}
case let .voiceChat(invite):
return .single(.joinVoiceChat(peer.id, invite))
@ -614,7 +658,27 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
return foundPeer
|> mapToSignal { foundPeer -> Signal<ResolvedUrl?, NoError> in
if let foundPeer = foundPeer {
if let threadId = threadId {
if case let .channel(channel) = foundPeer, channel.flags.contains(.isForum) {
if let threadId = threadId {
return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: Int64(threadId))
|> map { info -> ResolvedUrl? in
if let _ = info {
return .replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)
} else {
return .peer(peer?._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))
}
}
} else {
return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: Int64(messageId.id))
|> map { info -> ResolvedUrl? in
if let _ = info {
return .replyThread(messageId: messageId)
} else {
return .peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))
}
}
}
} else if let threadId = threadId {
let replyThreadMessageId = MessageId(peerId: foundPeer.id, namespace: Namespaces.Message.Cloud, id: threadId)
return context.engine.messages.fetchChannelReplyThreadMessage(messageId: replyThreadMessageId, atMessageId: nil)
|> map(Optional.init)