Monoforums

This commit is contained in:
Isaac 2025-05-26 23:38:55 +08:00
parent cf5223ab46
commit 60f2b98ee8
5 changed files with 130 additions and 12 deletions

View File

@ -35,6 +35,7 @@ public enum AvatarNodeClipStyle {
case none
case round
case roundedRect
case bubble
}
private class AvatarNodeParameters: NSObject {
@ -272,6 +273,25 @@ public final class AvatarEditOverlayNode: ASDisplayNode {
}
public final class AvatarNode: ASDisplayNode {
public static func avatarBubbleMask(size: CGSize) -> UIImage! {
return generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
AvatarNode.addAvatarBubblePath(context: context, rect: CGRect(origin: CGPoint(), size: size))
context.fillPath()
})
}
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 transformedPath = path.copy(using: &transform)!
context.addPath(transformedPath)
}
}
public static let gradientColors: [[UIColor]] = [
[UIColor(rgb: 0xff516a), UIColor(rgb: 0xff885e)],
[UIColor(rgb: 0xffa85c), UIColor(rgb: 0xffcd6a)],
@ -335,6 +355,7 @@ public final class AvatarNode: ASDisplayNode {
private var theme: PresentationTheme?
private var overrideImage: AvatarNodeImageOverride?
public let imageNode: ImageNode
private var imageNodeMask: UIImageView?
public var editOverlayNode: AvatarEditOverlayNode?
private let imageReadyDisposable = MetaDisposable()
@ -399,7 +420,7 @@ public final class AvatarNode: ASDisplayNode {
self.displaysAsynchronously = true
self.disableClearContentsOnHide = true
self.imageNode.isLayerBacked = true
self.imageNode.isUserInteractionEnabled = false
self.addSubnode(self.imageNode)
self.imageNode.contentUpdated = { [weak self] image in
@ -434,6 +455,9 @@ public final class AvatarNode: ASDisplayNode {
public func updateSize(size: CGSize) {
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
self.editOverlayNode?.frame = self.imageNode.frame
if let imageNodeMask = self.imageNodeMask {
imageNodeMask.frame = CGRect(origin: CGPoint(), size: size)
}
if !self.displaySuspended {
self.setNeedsDisplay()
self.editOverlayNode?.setNeedsDisplay()
@ -678,6 +702,7 @@ public final class AvatarNode: ASDisplayNode {
if self.params == params {
return
}
let previousSize = self.params?.displayDimensions
self.params = params
switch clipStyle {
@ -690,6 +715,29 @@ public final class AvatarNode: ASDisplayNode {
case .roundedRect:
self.imageNode.clipsToBounds = true
self.imageNode.cornerRadius = displayDimensions.height * 0.25
case .bubble:
break
}
if case .bubble = clipStyle {
var updateMask = false
let imageNodeMask: UIImageView
if let current = self.imageNodeMask {
imageNodeMask = current
updateMask = previousSize != params.displayDimensions
} else {
imageNodeMask = UIImageView()
self.imageNodeMask = imageNodeMask
self.imageNode.view.mask = imageNodeMask
imageNodeMask.frame = self.imageNode.frame
updateMask = true
}
if updateMask {
imageNodeMask.image = AvatarNode.avatarBubbleMask(size: params.displayDimensions)
}
} else if self.imageNodeMask != nil {
self.imageNodeMask = nil
self.imageNode.view.mask = nil
}
if let imageCache = genericContext.imageCache as? DirectMediaImageCache, let peer, let smallProfileImage = peer.smallProfileImage, let peerReference = PeerReference(peer._asPeer()) {
@ -1472,3 +1520,4 @@ public final class AvatarNode: ASDisplayNode {
}
}
}

View File

@ -214,6 +214,9 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
case .roundedRect:
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))
context.clip()
}
var shouldBlur = false
@ -265,6 +268,8 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
}
case .roundedRect:
break
case .bubble:
break
}
} else {
if let emptyColor = emptyColor {
@ -279,6 +284,10 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
context.beginPath()
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))
context.clip()
}
}
}
@ -295,6 +304,10 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
context.beginPath()
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))
context.clip()
}
}
@ -332,6 +345,10 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
context.beginPath()
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))
context.clip()
}
}
})

View File

@ -1318,6 +1318,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
private var inlineNavigationMarkLayer: SimpleLayer?
public let titleNode: TextNode
private var titleBadge: (backgroundView: UIImageView, textNode: TextNode)?
public let authorNode: AuthorNode
private var compoundHighlightingNode: LinkHighlightingNode?
private var textArrowNode: ASImageNode?
@ -1835,10 +1836,19 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.avatarNode.font = avatarPlaceholderFont(size: avatarFontSize)
}
}
if peer.smallProfileImage != nil && overrideImage == nil {
self.avatarNode.setPeerV2(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: avatarDiameter, height: avatarDiameter))
let avatarClipStyle: AvatarNodeClipStyle
if peerIsMonoforum {
avatarClipStyle = .bubble
} else if isForumAvatar {
avatarClipStyle = .roundedRect
} else {
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0))
avatarClipStyle = .round
}
if peer.smallProfileImage != nil && overrideImage == nil {
self.avatarNode.setPeerV2(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: avatarClipStyle, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: avatarDiameter, height: avatarDiameter))
} else {
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: avatarClipStyle, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0))
}
if peer.isPremium && peer.id != item.context.account.peerId {
@ -2028,6 +2038,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let makeTrailingTextBadgeLayout = TextNode.asyncLayout(self.trailingTextBadgeNode)
let titleLayout = TextNode.asyncLayout(self.titleNode)
let titleBadgeLayout = TextNode.asyncLayout(self.titleBadge?.textNode)
let authorLayout = self.authorNode.asyncLayout()
let makeMeasureLayout = TextNode.asyncLayout(self.measureNode)
let inputActivitiesLayout = self.inputActivitiesNode.asyncLayout()
@ -2226,6 +2237,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var textLeftCutout: CGFloat = 0.0
var dateAttributedString: NSAttributedString?
var titleAttributedString: NSAttributedString?
var titleBadgeText: String?
var badgeContent = ChatListBadgeContent.none
var mentionBadgeContent = ChatListBadgeContent.none
var statusState = ChatListStatusNodeState.none
@ -3001,12 +3013,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
textColor = theme.titleColor
}
//TODO:localize
if case let .channel(channel) = itemPeer.peer, channel.flags.contains(.isMonoforum) {
titleAttributedString = NSAttributedString(string: "\(displayTitle) Messages", font: titleFont, textColor: textColor)
} else {
titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: textColor)
//TODO:localize
titleBadgeText = "MESSAGES"
}
titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: textColor)
}
case .group:
titleAttributedString = NSAttributedString(string: item.presentationData.strings.ChatList_ArchivedChatsTitle, font: titleFont, textColor: theme.titleColor)
@ -3224,7 +3235,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
default:
break
}
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer {
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatOrMonoforumMainPeer {
if peer.isSubscription {
isSubscription = true
}
@ -3369,7 +3380,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else if case let .peer(peer) = item.content, case let .channel(channel) = peer.peer.peer, channel.flags.contains(.isMonoforum) {
if forumThread != nil || !topForumTopicItems.isEmpty {
if let forumThread {
isFirstForumThreadSelectable = forumThread.isUnread
isFirstForumThreadSelectable = false
forumThreads.append((id: forumThread.id, threadPeer: forumThread.threadPeer, title: NSAttributedString(string: forumThread.threadPeer?.compactDisplayTitle ?? " ", font: textFont, textColor: forumThread.isUnread || isSearching ? theme.authorNameColor : theme.messageTextColor), iconId: nil, iconColor: nil))
}
for topicItem in topForumTopicItems {
@ -3463,11 +3474,19 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
titleAttributedString = NSAttributedString(string: " ", font: titleFont, textColor: theme.titleColor)
}
let titleRectWidth = rawContentWidth - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth
var titleRectWidth = rawContentWidth - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth
var titleCutout: TextNodeCutout?
if !titleLeftCutout.isZero {
titleCutout = TextNodeCutout(topLeft: CGSize(width: titleLeftCutout, height: 10.0), topRight: nil, bottomRight: nil)
}
var titleBadgeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let titleBadgeText {
let titleBadgeLayoutAndApplyValue = titleBadgeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleBadgeText, font: Font.semibold(11.0), textColor: theme.titleColor.withMultipliedAlpha(0.4)), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
titleBadgeLayoutAndApply = titleBadgeLayoutAndApplyValue
titleRectWidth = max(10.0, titleRectWidth - titleBadgeLayoutAndApplyValue.0.size.width - 8.0)
}
let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: maxTitleLines, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: titleCutout, insets: UIEdgeInsets()))
var inputActivitiesSize: CGSize?
@ -4244,6 +4263,36 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let contentDelta = CGPoint(x: contentRect.origin.x - (strongSelf.titleNode.frame.minX - titleOffset), y: contentRect.origin.y - (strongSelf.titleNode.frame.minY - UIScreenPixel))
let titleFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
if let (titleBadgeLayout, titleBadgeApply) = titleBadgeLayoutAndApply {
let titleBadgeNode = titleBadgeApply()
let backgroundView: UIImageView
if let current = strongSelf.titleBadge {
backgroundView = current.backgroundView
} else {
backgroundView = UIImageView(image: generateStretchableFilledCircleImage(radius: 4.0, color: .white)?.withRenderingMode(.alwaysTemplate))
strongSelf.titleBadge = (backgroundView, titleBadgeNode)
strongSelf.mainContentContainerNode.view.addSubview(backgroundView)
strongSelf.mainContentContainerNode.addSubnode(titleBadgeNode)
}
let titleBadgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + titleIconsWidth + 10.0, y: titleFrame.minY + floor((titleFrame.height - titleBadgeLayout.size.height) * 0.5)), size: titleBadgeLayout.size)
titleBadgeNode.frame = titleBadgeFrame
var titleBadgeBackgroundFrame = titleBadgeFrame.insetBy(dx: -4.0, dy: -2.0)
titleBadgeBackgroundFrame.size.height -= 1.0
backgroundView.frame = titleBadgeBackgroundFrame
if item.presentationData.theme.overallDarkAppearance {
backgroundView.tintColor = theme.titleColor.withMultipliedAlpha(0.1)
} else {
backgroundView.tintColor = theme.titleColor.withMultipliedAlpha(0.05)
}
} else if let titleBadge = strongSelf.titleBadge {
strongSelf.titleBadge = nil
titleBadge.backgroundView.removeFromSuperview()
titleBadge.textNode.removeFromSupernode()
}
let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height), size: authorLayout)
strongSelf.authorNode.frame = authorNodeFrame
let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.height.isZero ? 0.0 : (authorLayout.height - 3.0))), size: textLayout.size)

View File

@ -430,6 +430,9 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch
if message.minAutoremoveOrClearTimeout == viewOnceTimeout {
canReplyInAnotherChat = false
}
if let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.isMonoForum {
canReplyInAnotherChat = false
}
}
if canReplyInAnotherChat {

View File

@ -548,7 +548,7 @@ extension ChatControllerImpl {
} 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) Messages", nil, false)
strongSelf.state.chatTitleContent = .custom(mainPeer.debugDisplayTitle, nil, false)
} else {
strongSelf.state.chatTitleContent = .custom(channel.debugDisplayTitle, nil, false)
}