Monoforums

This commit is contained in:
Isaac 2025-05-27 02:19:48 +08:00
parent c984cb956b
commit 5d2b252850
9 changed files with 331 additions and 149 deletions

View File

@ -50,7 +50,7 @@ public final class PeerSelectionControllerParams {
public let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
public let filter: ChatListNodePeersFilter
public let requestPeerType: [ReplyMarkupButtonRequestPeerType]?
public let forumPeerId: EnginePeer.Id?
public let forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?
public let hasFilters: Bool
public let hasChatListSelector: Bool
public let hasContactSelector: Bool
@ -72,7 +72,7 @@ public final class PeerSelectionControllerParams {
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
filter: ChatListNodePeersFilter = [.onlyWriteable],
requestPeerType: [ReplyMarkupButtonRequestPeerType]? = nil,
forumPeerId: EnginePeer.Id? = nil,
forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)? = nil,
hasFilters: Bool = false,
hasChatListSelector: Bool = true,
hasContactSelector: Bool = true,

View File

@ -284,9 +284,14 @@ public final class AvatarNode: ASDisplayNode {
public static func addAvatarBubblePath(context: CGContext, rect: CGRect) {
if let path = try? convertSvgPath("M60,30.274903 C60,46.843446 46.568544,60.274904 30,60.274904 C13.431458,60.274904 0,46.843446 0,30.274903 C0,23.634797 2.158635,17.499547 5.810547,12.529785 L6.036133,12.226074 C6.921364,10.896042 7.367402,8.104698 5.548828,5.316895 C3.606939,2.340088 1.186019,0.979668 2.399414,0.470215 C3.148032,0.156204 7.572027,0.000065 10.764648,1.790527 C12.148517,2.56662 13.2296,3.342422 14.09224,4.039734 C14.42622,4.309704 14.892063,4.349773 15.265962,4.138523 C19.618079,1.679604 24.644722,0.274902 30,0.274902 C46.568544,0.274902 60,13.70636 60,30.274903 Z ") {
var transform = CGAffineTransformMake(1.0, 0.0, 0.0, -1.0, 0.0, 60.274904)
transform = CGAffineTransformScale(transform, rect.width / 60.0, rect.height / 60.0)
transform = CGAffineTransformTranslate(transform, rect.minX, rect.minY)
let sx = rect.width / 60.0
let sy = rect.height / 60.0
var transform = CGAffineTransform(
a: sx, b: 0.0,
c: 0.0, d: -sy,
tx: rect.minX,
ty: rect.minY + rect.height
)
let transformedPath = path.copy(using: &transform)!
context.addPath(transformedPath)
}
@ -950,6 +955,10 @@ public final class AvatarNode: ASDisplayNode {
context.beginPath()
context.addPath(UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height: bounds.size.height), cornerRadius: floor(bounds.size.width * 0.25)).cgPath)
context.clip()
} else if case .bubble = parameters.clipStyle {
context.beginPath()
AvatarNode.addAvatarBubblePath(context: context, rect: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height: bounds.size.height))
context.clip()
}
} else {
colors = grayscaleColors

View File

@ -215,7 +215,14 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
context.addPath(UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: displayDimensions.width, height: displayDimensions.height).insetBy(dx: inset, dy: inset), cornerRadius: floor(displayDimensions.width * 0.25)).cgPath)
context.clip()
case .bubble:
AvatarNode.addAvatarBubblePath(context: context, rect: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
let rect = CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)
context.translateBy(x: rect.midX, y: rect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -rect.midX, y: -rect.midY)
AvatarNode.addAvatarBubblePath(context: context, rect: rect)
context.translateBy(x: rect.midX, y: rect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -rect.midX, y: -rect.midY)
context.clip()
}
@ -285,8 +292,14 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
context.addPath(UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: displayDimensions.width, height: displayDimensions.height).insetBy(dx: inset, dy: inset), cornerRadius: floor(displayDimensions.width * 0.25)).cgPath)
context.fillPath()
case .bubble:
context.beginPath()
AvatarNode.addAvatarBubblePath(context: context, rect: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
let rect = CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)
context.translateBy(x: rect.midX, y: rect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -rect.midX, y: -rect.midY)
AvatarNode.addAvatarBubblePath(context: context, rect: rect)
context.translateBy(x: rect.midX, y: rect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -rect.midX, y: -rect.midY)
context.clip()
}
}
@ -305,8 +318,14 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
context.addPath(UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: displayDimensions.width, height: displayDimensions.height).insetBy(dx: inset, dy: inset), cornerRadius: floor(displayDimensions.width * 0.25)).cgPath)
context.fillPath()
case .bubble:
context.beginPath()
AvatarNode.addAvatarBubblePath(context: context, rect: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
let rect = CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)
context.translateBy(x: rect.midX, y: rect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -rect.midX, y: -rect.midY)
AvatarNode.addAvatarBubblePath(context: context, rect: rect)
context.translateBy(x: rect.midX, y: rect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -rect.midX, y: -rect.midY)
context.clip()
}
}
@ -346,8 +365,14 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
context.addPath(UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: displayDimensions.width, height: displayDimensions.height).insetBy(dx: inset, dy: inset), cornerRadius: floor(displayDimensions.width * 0.25)).cgPath)
context.fillPath()
case .bubble:
context.beginPath()
AvatarNode.addAvatarBubblePath(context: context, rect: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
let rect = CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)
context.translateBy(x: rect.midX, y: rect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -rect.midX, y: -rect.midY)
AvatarNode.addAvatarBubblePath(context: context, rect: rect)
context.translateBy(x: rect.midX, y: rect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -rect.midX, y: -rect.midY)
context.clip()
}
}

View File

@ -468,7 +468,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
interaction: nodeInteraction
), directionHint: entry.directionHint)
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence):
let itemPeer = peer.chatMainPeer
let itemPeer = peer.chatOrMonoforumMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
chatPeer = peer
@ -643,7 +643,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
animationRenderer: nodeInteraction.animationRenderer
), directionHint: entry.directionHint)
case .peerType:
let itemPeer = peer.chatMainPeer
let itemPeer = peer.chatOrMonoforumMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
chatPeer = peer
@ -867,7 +867,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
interaction: nodeInteraction
), directionHint: entry.directionHint)
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence):
let itemPeer = peer.chatMainPeer
let itemPeer = peer.chatOrMonoforumMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
chatPeer = peer
@ -993,7 +993,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
animationRenderer: nodeInteraction.animationRenderer
), directionHint: entry.directionHint)
case .peerType:
let itemPeer = peer.chatMainPeer
let itemPeer = peer.chatOrMonoforumMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
chatPeer = peer

View File

@ -1211,7 +1211,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
strongSelf.contextSourceNode.contentRect = extractedRect
switch item.peer {
case let .peer(peer, _):
case let .peer(peer, chatPeer):
if let peer = peer {
var overrideImage: AvatarNodeImageOverride?
if peer.id == item.context.account.peerId, case let .generalSearch(isSavedMessages) = item.peerMode, case .treatSelfAsSaved = item.aliasHandling {
@ -1233,11 +1233,19 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
if case .app(true) = item.peerMode {
clipStyle = .roundedRect
displayDimensions = CGSize(width: displayDimensions.width, height: displayDimensions.width * 1.2)
} else if case let .channel(channel) = peer, channel.isForumOrMonoForum {
} else if case let .channel(channel) = peer {
if case let .channel(chatPeer) = chatPeer, chatPeer.isMonoForum {
clipStyle = .bubble
} else {
if channel.isForum {
clipStyle = .roundedRect
} else {
clipStyle = .round
}
}
} else {
clipStyle = .round
}
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoads, displayDimensions: displayDimensions)
}

View File

@ -1924,7 +1924,87 @@ public final class ChatSideTopicsPanel: Component {
}
component.updateTopicId(topicId, direction)
}
let itemContextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? = (self.isReordering || component.isMonoforum) ? nil : { [weak self] gesture, sourceNode in
var itemContextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)?
if !self.isReordering && component.isMonoforum {
itemContextGesture = { [weak self] gesture, sourceNode in
guard let self, let component = self.component else {
return
}
guard let controller = component.controller() else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
if let listView = self.list.view as? AsyncListComponent.View {
listView.stopScrolling()
}
let topicId: Int64
switch item.item.id {
case let .chatList(peerId):
topicId = peerId.toInt64()
case let .forum(topicIdValue):
topicId = topicIdValue
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in
guard let self else {
return
}
c?.dismiss(completion: { [weak self] in
guard let self, let component = self.component, let controller = component.controller() else {
return
}
let actionSheet = ActionSheetController(presentationData: presentationData)
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: presentationData.strings.ChatList_DeleteTopicConfirmationText, parseMarkdown: true))
items.append(ActionSheetButtonItem(title: presentationData.strings.ChatList_DeleteTopicConfirmationAction, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self, let component = self.component else {
return
}
if component.topicId == topicId {
component.updateTopicId(nil, false)
}
let _ = component.context.engine.peers.removeForumChannelThread(id: component.peerId, threadId: topicId).startStandalone(completed: {
})
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
controller.present(actionSheet, in: .window(.root))
})
})))
let contextController = ContextController(
presentationData: presentationData,
source: .extracted(ItemExtractedContentSource(
sourceNode: sourceNode,
containerView: self,
keepInPlace: false
)),
items: .single(ContextController.Items(content: .list(items))),
recognizer: nil,
gesture: gesture
)
controller.presentInGlobalOverlay(contextController)
}
} else if !self.isReordering {
itemContextGesture = { [weak self] gesture, sourceNode in
guard let self, let component = self.component else {
return
}
@ -2041,6 +2121,7 @@ public final class ChatSideTopicsPanel: Component {
controller.presentInGlobalOverlay(contextController)
})
}
}
switch component.location {
case .side:

View File

@ -526,7 +526,7 @@ public func threadNotificationExceptionsScreen(context: AccountContext, peerId:
}, addException: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader]
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, forumPeerId: peerId, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle))
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, forumPeerId: (peerId, false), hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle))
controller.peerSelected = { [weak controller] _, threadId in
guard let threadId = threadId else {
return

View File

@ -22,7 +22,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
public var peerSelected: ((EnginePeer, Int64?) -> Void)?
public var multiplePeersSelected: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?, ChatSendMessageActionSheetController.SendParameters?) -> Void)?
private let filter: ChatListNodePeersFilter
private let forumPeerId: EnginePeer.Id?
private let forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?
private let selectForumThreads: Bool
private let attemptSelection: ((EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void)?
@ -277,17 +277,33 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
self.peerSelectionNode.requestOpenPeer = { [weak self] peer, threadId in
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
if case let .channel(peer) = peer, peer.isForumOrMonoForum, threadId == nil, strongSelf.selectForumThreads {
let mainPeer: Signal<EnginePeer?, NoError>
if peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId {
mainPeer = self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: linkedMonoforumId)
)
} else {
mainPeer = .single(nil)
}
let _ = (mainPeer |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] mainPeer in
guard let strongSelf else {
return
}
let displayPeer = mainPeer ?? EnginePeer(peer)
let controller = PeerSelectionControllerImpl(
PeerSelectionControllerParams(
context: strongSelf.context,
updatedPresentationData: nil,
filter: strongSelf.filter,
forumPeerId: peer.id,
forumPeerId: (peer.id, peer.isMonoForum),
hasFilters: false,
hasChatListSelector: false,
hasContactSelector: false,
hasGlobalSearch: false,
title: EnginePeer(peer).compactDisplayTitle,
title: displayPeer.compactDisplayTitle,
attemptSelection: strongSelf.attemptSelection,
createNewGroup: nil,
pretendPresentedInModal: false,
@ -299,6 +315,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
)
controller.peerSelected = strongSelf.peerSelected
strongSelf.push(controller)
})
} else {
peerSelected(peer, threadId)
}
@ -322,7 +339,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
context: strongSelf.context,
updatedPresentationData: nil,
filter: strongSelf.filter,
forumPeerId: peer.id,
forumPeerId: (peer.id, peer.isMonoForum),
hasFilters: false,
hasChatListSelector: false,
hasContactSelector: false,

View File

@ -33,7 +33,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
private let presentInGlobalOverlay: (ViewController, Any?) -> Void
private let dismiss: () -> Void
private let filter: ChatListNodePeersFilter
private let forumPeerId: EnginePeer.Id?
private let forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?
private let hasGlobalSearch: Bool
private let forwardedMessageIds: [EngineMessage.Id]
private let hasTypeHeaders: Bool
@ -109,7 +109,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
return (self.presentationData, self.presentationDataPromise.get())
}
init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
self.context = context
self.controller = controller
self.present = present
@ -193,8 +193,12 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}
let chatListLocation: ChatListControllerLocation
if let forumPeerId = self.forumPeerId {
if let (forumPeerId, isMonoforum) = self.forumPeerId {
if isMonoforum {
chatListLocation = .savedMessagesChats(peerId: forumPeerId)
} else {
chatListLocation = .forum(peerId: forumPeerId)
}
} else {
chatListLocation = .chatList(groupId: .root)
}
@ -248,12 +252,46 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}
self.chatListNode?.peerSelected = { [weak self] peer, threadId, _, _, _ in
self?.chatListNode?.clearHighlightAnimated(true)
self?.requestOpenPeer?(peer, threadId)
guard let self else {
return
}
if let (peerId, isMonoforum) = self.forumPeerId, isMonoforum {
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] mainPeer in
guard let self, let mainPeer else {
return
}
self.chatListNode?.clearHighlightAnimated(true)
self.requestOpenPeer?(mainPeer, peer.id.toInt64())
})
} else {
self.chatListNode?.clearHighlightAnimated(true)
self.requestOpenPeer?(peer, threadId)
}
}
self.mainContainerNode?.peerSelected = { [weak self] peer, threadId, _, _, _ in
self?.chatListNode?.clearHighlightAnimated(true)
self?.requestOpenPeer?(peer, threadId)
guard let self else {
return
}
if let (peerId, isMonoforum) = self.forumPeerId, isMonoforum {
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] mainPeer in
guard let self, let mainPeer else {
return
}
self.chatListNode?.clearHighlightAnimated(true)
self.requestOpenPeer?(mainPeer, peer.id.toInt64())
})
} else {
self.chatListNode?.clearHighlightAnimated(true)
self.requestOpenPeer?(peer, threadId)
}
}
self.chatListNode?.disabledPeerSelected = { [weak self] peer, threadId, reason in
@ -1212,8 +1250,12 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.mainContainerNode?.accessibilityElementsHidden = true
let chatListLocation: ChatListControllerLocation
if let forumPeerId = self.forumPeerId {
if let (forumPeerId, isMonoforum) = self.forumPeerId {
if isMonoforum {
chatListLocation = .savedMessagesChats(peerId: forumPeerId)
} else {
chatListLocation = .forum(peerId: forumPeerId)
}
} else {
chatListLocation = .chatList(groupId: EngineChatList.Group(.root))
}