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

This commit is contained in:
Mikhail Filimonov 2025-05-27 08:15:52 +01:00
commit ecaae4c29a
60 changed files with 3789 additions and 1289 deletions

View File

@ -14013,9 +14013,17 @@ Sorry for the inconvenience.";
"Notification.PaidMessagePriceChanged.Stars_1" = "%@ Star";
"Notification.PaidMessagePriceChanged.Stars_any" = "%@ Stars";
"Notification.PaidMessagePriceChanged" = "%1$@ changed price to %2$@ per message";
"Notification.PaidMessagePriceChangedAndEnabledChannelMessage" = "%1$@ changed price to %2$@ per message, and enabled messaging this channel";
"Notification.PaidMessagePriceChangedYou" = "You changed price to %1$@ per message";
"Notification.PaidMessagePriceChangedAndEnabledChannelMessageYou" = "You changed price to %1$@ per message, and enabled messaging this channel";
"Notification.ChannelMessagePriceChanged_1" = "{name} set the private message price to %d star";
"Notification.ChannelMessagePriceChanged_any" = "{name} set the private message price to %d stars";
"Notification.ChannelMessagePriceZeroChanged" = "%@ now accepts private messages";
"Notification.ChannelMessagePriceChangedAndEnabledChannelMessage_1" = "{name} now accepts private messages for %d star";
"Notification.ChannelMessagePriceChangedAndEnabledChannelMessage_any" = "{name} now accepts private messages for %d stars";
"Notification.ChannelMessagePriceZeroChangedAndEnabledChannelMessage" = "%@ now accepts private messages";
"Notification.ChannelMessageDisabled" = "%@ no longer accepts private messages";
"Premium.MaxExpiringStoriesTextNumberFormat_1" = "**%d** story";
"Premium.MaxExpiringStoriesTextNumberFormat_any" = "**%d** stories";
@ -14368,3 +14376,9 @@ Sorry for the inconvenience.";
"PeerInfo.OptionTopics.Enabled" = "Enabled";
"PeerInfo.OptionTopics.Disabled" = "Disabled";
"ChannelMessages.Title" = "Allow Channel Messages";
"ChannelMessages.Info" = "Allow users to send messages to your channel, with the option to charge a fee for each message.";
"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.";

View File

@ -1118,6 +1118,7 @@ public protocol SharedAccountContext: AnyObject {
func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController
func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal<CollectibleItemInfoScreenInitialData?, NoError>
func makeBotSettingsScreen(context: AccountContext, peerId: EnginePeer.Id?) -> ViewController
func makeEditForumTopicScreen(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, threadInfo: EngineMessageHistoryThread.Info, isHidden: Bool) -> ViewController
func navigateToChatController(_ params: NavigateToChatControllerParams)
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)

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

@ -1297,7 +1297,11 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
if let data = view.cachedData as? CachedUserData {
return data.sendPaidMessageStars
} else if let channel = peerViewMainPeer(view) as? TelegramChannel {
return channel.sendPaidMessageStars
if channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = view.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.sendSomething) {
return nil
} else {
return channel.sendPaidMessageStars
}
} else {
return nil
}

View File

@ -35,6 +35,7 @@ public enum AvatarNodeClipStyle {
case none
case round
case roundedRect
case bubble
}
private class AvatarNodeParameters: NSObject {
@ -272,6 +273,30 @@ 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 ") {
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)
}
}
public static let gradientColors: [[UIColor]] = [
[UIColor(rgb: 0xff516a), UIColor(rgb: 0xff885e)],
[UIColor(rgb: 0xffa85c), UIColor(rgb: 0xffcd6a)],
@ -335,6 +360,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 +425,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 +460,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 +707,7 @@ public final class AvatarNode: ASDisplayNode {
if self.params == params {
return
}
let previousSize = self.params?.displayDimensions
self.params = params
switch clipStyle {
@ -690,6 +720,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()) {
@ -902,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
@ -1472,3 +1529,4 @@ public final class AvatarNode: ASDisplayNode {
}
}
}

View File

@ -214,6 +214,16 @@ 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:
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()
}
var shouldBlur = false
@ -265,6 +275,8 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
}
case .roundedRect:
break
case .bubble:
break
}
} else {
if let emptyColor = emptyColor {
@ -279,6 +291,16 @@ 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:
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()
}
}
}
@ -295,6 +317,16 @@ 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:
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()
}
}
@ -332,6 +364,16 @@ 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:
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

@ -581,7 +581,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
}
}
func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: Int64, isPinned: Bool?, isClosed: Bool?, chatListController: ChatListControllerImpl?, joined: Bool, canSelect: Bool) -> Signal<[ContextMenuItem], NoError> {
public func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: Int64, isPinned: Bool?, isClosed: Bool?, chatListController: ViewController?, joined: Bool, canSelect: Bool, customEdit: ((ContextController) -> Void)? = nil, customPinUnpin: ((ContextController) -> Void)? = nil, reorder: (() -> Void)? = nil) -> Signal<[ContextMenuItem], NoError> {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
let strings = presentationData.strings
@ -603,8 +603,17 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
if let isClosed = isClosed, isClosed && threadId != 1 {
} else {
if let isPinned = isPinned, channel.hasPermission(.manageTopics) {
items.append(.action(ContextMenuActionItem(text: isPinned ? presentationData.strings.ChatList_Context_Unpin : presentationData.strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in
if let isPinned, channel.hasPermission(.manageTopics) {
items.append(.action(ContextMenuActionItem(text: isPinned ? presentationData.strings.ChatList_Context_Unpin : presentationData.strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { c, f in
if let customPinUnpin {
if let c = c as? ContextController {
customPinUnpin(c)
} else {
f(.default)
}
return
}
f(.default)
let _ = (context.engine.peers.toggleForumChannelTopicPinned(id: peerId, threadId: threadId)
@ -621,6 +630,15 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
}
})
})))
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
c?.dismiss(completion: {
})
reorder()
})))
}
}
}
@ -636,6 +654,27 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
})))
}
var canOpenClose = false
if channel.flags.contains(.isCreator) {
canOpenClose = true
} else if channel.hasPermission(.manageTopics) {
canOpenClose = true
} else if threadData.isOwnedByMe {
canOpenClose = true
}
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
if let c = c as? ContextController {
customEdit(c)
} else {
f(.default)
}
return
})))
}
var isMuted = false
switch threadData.notificationSettings.muteState {
case .muted:
@ -863,14 +902,6 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
})))
if threadId != 1 {
var canOpenClose = false
if channel.flags.contains(.isCreator) {
canOpenClose = true
} else if channel.hasPermission(.manageTopics) {
canOpenClose = true
} else if threadData.isOwnedByMe {
canOpenClose = true
}
if canOpenClose {
items.append(.action(ContextMenuActionItem(text: threadData.isClosed ? presentationData.strings.ChatList_Context_ReopenTopic : presentationData.strings.ChatList_Context_CloseTopic, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: threadData.isClosed ? "Chat/Context Menu/Play": "Chat/Context Menu/Pause"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
@ -882,14 +913,36 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak chatListController] _, f in
f(.default)
chatListController?.deletePeerThread(peerId: peerId, threadId: threadId)
if let chatListController = chatListController as? ChatListControllerImpl {
chatListController.deletePeerThread(peerId: peerId, threadId: threadId)
} else if let chatListController {
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 actionSheet] in
actionSheet?.dismissAnimated()
let _ = context.engine.peers.removeForumChannelThread(id: peerId, threadId: threadId).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()
})
])
])
chatListController.present(actionSheet, in: .window(.root))
}
})))
}
}
if canSelect {
if canSelect, let chatListController = chatListController as? ChatListControllerImpl {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Select, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Select, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { [weak chatListController] _, f in
f(.default)
chatListController?.selectPeerThread(peerId: peerId, threadId: threadId)
})))

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

@ -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

@ -16,8 +16,8 @@ public extension UIView {
}
}
private extension CALayer {
func animate(from: Any, to: Any, keyPath: String, duration: Double, delay: Double, curve: ComponentTransition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) {
public extension CALayer {
func animate(from: Any, to: Any, keyPath: String, duration: Double, delay: Double, curve: ComponentTransition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil, key: String? = nil) {
let timingFunction: String
let mediaTimingFunction: CAMediaTimingFunction?
switch curve {
@ -39,7 +39,8 @@ private extension CALayer {
mediaTimingFunction: mediaTimingFunction,
removeOnCompletion: removeOnCompletion,
additive: additive,
completion: completion
completion: completion,
key: key
)
}
}

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,8 +1233,16 @@ 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 {
clipStyle = .roundedRect
} 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
}

View File

@ -263,6 +263,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)?
public final var addContentOffset: ((CGFloat, ListViewItemNode?) -> Void)?
public final var shouldStopScrolling: ((CGFloat) -> Bool)?
public final var onContentsUpdated: ((ContainedViewLayoutTransition) -> Void)?
public final var updateScrollingIndicator: ((ScrollingIndicatorState?, ContainedViewLayoutTransition) -> Void)?
@ -389,6 +390,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
private var reorderScrollUpdateTimestamp: Double?
private var reorderLastTimestamp: Double?
public var reorderedItemHasShadow = true
public var reorderingRequiresLongPress = false
private let waitingForNodesDisposable = MetaDisposable()
@ -518,7 +520,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
let itemNodeFrame = itemNode.frame
let itemNodeBounds = itemNode.bounds
if itemNode.isReorderable(at: point.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY)) {
let requiresLongPress = !strongSelf.reorderedItemHasShadow
let requiresLongPress = strongSelf.reorderingRequiresLongPress
return (true, requiresLongPress, itemNode)
}
break
@ -1101,6 +1103,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
self.updateVisibleContentOffset()
self.updateVisibleItemRange()
self.updateItemNodesVisibilities(onlyPositive: false)
self.onContentsUpdated?(.immediate)
//CATransaction.commit()
}
@ -3509,6 +3512,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
}
self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, synchronousLoad: synchronousLoads, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty, animateFullTransition: animateFullTransition)
self.onContentsUpdated?(headerNodesTransition.0)
if let offset = offset, !offset.isZero {
//self.didScrollWithOffset?(-offset, headerNodesTransition.0, nil)
@ -3733,6 +3737,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
} else {
self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, synchronousLoad: synchronousLoads, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty, animateFullTransition: animateFullTransition)
self.updateItemNodesVisibilities(onlyPositive: deferredUpdateVisible)
self.onContentsUpdated?(headerNodesTransition.0)
applyHeaderNodesFullTransition()

View File

@ -129,7 +129,7 @@ public func listViewAnimationCurveFromAnimationOptions(animationOptions: UIView.
public final class ListViewAnimation {
let from: Interpolatable
let to: Interpolatable
public let to: Interpolatable
let duration: Double
let startTime: Double
let invertOffsetDirection: Bool

View File

@ -679,13 +679,16 @@ final class MutableMessageHistoryView: MutablePostboxView {
hasChanges = true
}
case let .UpdateReadState(peerId, combinedReadState):
hasChanges = true
if let transientReadStates = self.transientReadStates {
switch transientReadStates {
case let .peer(states):
var updatedStates = states
updatedStates[peerId] = combinedReadState
self.transientReadStates = .peer(updatedStates)
if case let .single(_, threadId) = self.peerIds, threadId != nil {
} else {
hasChanges = true
if let transientReadStates = self.transientReadStates {
switch transientReadStates {
case let .peer(states):
var updatedStates = states
updatedStates[peerId] = combinedReadState
self.transientReadStates = .peer(updatedStates)
}
}
}
case let .UpdateTimestamp(index, timestamp):
@ -698,8 +701,15 @@ final class MutableMessageHistoryView: MutablePostboxView {
var currentThreadId: Int64?
switch self.peerIds {
case let .single(_, threadIdValue):
case let .single(peerId, threadIdValue):
currentThreadId = threadIdValue
if let threadIdValue, transaction.updatedPeerThreadInfos.contains(MessageHistoryThreadsTable.ItemId(peerId: peerId, threadId: threadIdValue)) {
if let threadData = postbox.messageHistoryThreadIndexTable.get(peerId: peerId, threadId: threadIdValue) {
hasChanges = true
self.transientReadStates = .peer([peerId: CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: threadData.summary.maxOutgoingReadId, maxKnownId: 0, count: 0, markedUnread: false))])])
}
}
case .associated:
break
case .external:

View File

@ -5,17 +5,20 @@ public struct StoredMessageHistoryThreadInfo: Equatable, PostboxCoding {
public var totalUnreadCount: Int32
public var isMarkedUnread: Bool
public var mutedUntil: Int32?
public var maxOutgoingReadId: Int32
public init(totalUnreadCount: Int32, isMarkedUnread: Bool, mutedUntil: Int32?) {
public init(totalUnreadCount: Int32, isMarkedUnread: Bool, mutedUntil: Int32?, maxOutgoingReadId: Int32) {
self.totalUnreadCount = totalUnreadCount
self.isMarkedUnread = isMarkedUnread
self.mutedUntil = mutedUntil
self.maxOutgoingReadId = maxOutgoingReadId
}
public init(decoder: PostboxDecoder) {
self.totalUnreadCount = decoder.decodeInt32ForKey("u", orElse: 0)
self.mutedUntil = decoder.decodeOptionalInt32ForKey("m")
self.isMarkedUnread = decoder.decodeBoolForKey("mu", orElse: false)
self.maxOutgoingReadId = decoder.decodeInt32ForKey("or", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
@ -26,6 +29,7 @@ public struct StoredMessageHistoryThreadInfo: Equatable, PostboxCoding {
} else {
encoder.encodeNil(forKey: "m")
}
encoder.encodeInt32(self.maxOutgoingReadId, forKey: "or")
}
}
@ -112,7 +116,7 @@ class MessageHistoryThreadIndexTable: Table {
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
}
private struct UpdatedEntry {
struct UpdatedEntry {
var value: StoredMessageHistoryThreadInfo?
}
@ -127,7 +131,7 @@ class MessageHistoryThreadIndexTable: Table {
private let sharedKey = ValueBoxKey(length: 8 + 4 + 8 + 4 + 4)
private var updatedInfoItems: [MessageHistoryThreadsTable.ItemId: UpdatedEntry] = [:]
private(set) var updatedInfoItems: [MessageHistoryThreadsTable.ItemId: UpdatedEntry] = [:]
init(valueBox: ValueBox, table: ValueBoxTable, reverseIndexTable: MessageHistoryThreadReverseIndexTable, seedConfiguration: SeedConfiguration, useCaches: Bool) {
self.reverseIndexTable = reverseIndexTable

View File

@ -2456,6 +2456,7 @@ final class PostboxImpl {
let transactionParticipationInTotalUnreadCountUpdates = self.peerNotificationSettingsTable.transactionParticipationInTotalUnreadCountUpdates(postbox: self, transaction: currentTransaction)
let updatedMessageThreadPeerIds = self.messageHistoryThreadIndexTable.replay(threadsTable: self.messageHistoryThreadsTable, namespaces: self.seedConfiguration.chatMessagesNamespaces, updatedIds: self.messageHistoryThreadsTable.updatedIds)
let updatedPeerThreadInfos = Set(self.messageHistoryThreadIndexTable.updatedInfoItems.keys)
let alteredInitialPeerThreadsSummaries = self.peerThreadsSummaryTable.update(peerIds: updatedMessageThreadPeerIds.union(self.currentUpdatedPeerThreadCombinedStates), indexTable: self.messageHistoryThreadIndexTable, combinedStateTable: self.peerThreadCombinedStateTable, tagsSummaryTable: self.messageHistoryTagsSummaryTable)
self.chatListIndexTable.commitWithTransaction(
@ -2479,7 +2480,62 @@ final class PostboxImpl {
let updatedPeerTimeoutAttributes = self.peerTimeoutPropertiesTable.hasUpdates
let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds, updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters, updatedPeerTimeoutAttributes: updatedPeerTimeoutAttributes, updatedMessageThreadPeerIds: updatedMessageThreadPeerIds, updatedPeerThreadCombinedStates: self.currentUpdatedPeerThreadCombinedStates, updatedPeerThreadsSummaries: Set(alteredInitialPeerThreadsSummaries.keys), updatedPinnedThreads: self.currentUpdatedPinnedThreads, updatedHiddenPeerIds: self.currentUpdatedHiddenPeerIds, storyGeneralStatesEvents: self.currentStoryGeneralStatesEvents, storyPeerStatesEvents: self.currentStoryPeerStatesEvents, storySubscriptionsEvents: self.currentStorySubscriptionsEvents, storyItemsEvents: self.currentStoryItemsEvents, currentStoryTopItemEvents: self.currentStoryTopItemEvents, storyEvents: self.currentStoryEvents)
let transaction = PostboxTransaction(
currentUpdatedState: self.currentUpdatedState,
currentPeerHoleOperations: self.currentPeerHoleOperations,
currentOperationsByPeerId: self.currentOperationsByPeerId,
chatListOperations: self.currentChatListOperations,
currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions,
currentUpdatedPeers: self.currentUpdatedPeers,
currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings,
currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps,
currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData,
currentUpdatedPeerPresences: currentUpdatedPeerPresences,
currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates,
currentUpdatedTotalUnreadStates: self.currentUpdatedTotalUnreadStates,
currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries,
alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates,
currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations,
currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations,
unsentMessageOperations: self.currentUnsentOperations,
updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations,
currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations,
currentPreferencesOperations: self.currentPreferencesOperations,
currentOrderedItemListOperations: self.currentOrderedItemListOperations,
currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations,
currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations,
currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates,
currentGlobalTagsOperations: self.currentGlobalTagsOperations,
currentLocalTagsOperations: self.currentLocalTagsOperations,
updatedMedia: self.currentUpdatedMedia,
replaceRemoteContactCount: self.currentReplaceRemoteContactCount,
replaceContactPeerIds: self.currentReplacedContactPeerIds,
currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations,
currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries,
currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries,
currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries,
currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings,
replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems,
updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys,
updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys,
currentUpdatedMasterClientId: currentUpdatedMasterClientId,
updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds,
updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds,
updatedGlobalNotificationSettings: self.currentNeedsReindexUnreadCounters,
updatedPeerTimeoutAttributes: updatedPeerTimeoutAttributes,
updatedMessageThreadPeerIds: updatedMessageThreadPeerIds,
updatedPeerThreadCombinedStates: self.currentUpdatedPeerThreadCombinedStates,
updatedPeerThreadsSummaries: Set(alteredInitialPeerThreadsSummaries.keys),
updatedPeerThreadInfos: updatedPeerThreadInfos,
updatedPinnedThreads: self.currentUpdatedPinnedThreads,
updatedHiddenPeerIds: self.currentUpdatedHiddenPeerIds,
storyGeneralStatesEvents: self.currentStoryGeneralStatesEvents,
storyPeerStatesEvents: self.currentStoryPeerStatesEvents,
storySubscriptionsEvents: self.currentStorySubscriptionsEvents,
storyItemsEvents: self.currentStoryItemsEvents,
currentStoryTopItemEvents: self.currentStoryTopItemEvents,
storyEvents: self.currentStoryEvents
)
var updatedTransactionState: Int64?
var updatedMasterClientId: Int64?
if !transaction.isEmpty {
@ -3131,6 +3187,7 @@ 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):
@ -3334,8 +3391,14 @@ final class PostboxImpl {
var transientReadStates: MessageHistoryViewReadState?
switch peerIds {
case let .single(peerId, threadId):
if threadId == nil, let readState = self.readStateTable.getCombinedState(peerId) {
transientReadStates = .peer([peerId: readState])
if let threadId {
if let threadData = self.messageHistoryThreadIndexTable.get(peerId: peerId, threadId: threadId) {
transientReadStates = .peer([peerId: CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: threadData.summary.maxOutgoingReadId, maxKnownId: 0, count: 0, markedUnread: false))])])
}
} else {
if let readState = self.readStateTable.getCombinedState(peerId) {
transientReadStates = .peer([peerId: readState])
}
}
case let .associated(peerId, _):
if let readState = self.readStateTable.getCombinedState(peerId) {

View File

@ -47,6 +47,7 @@ final class PostboxTransaction {
let updatedMessageThreadPeerIds: Set<PeerId>
let updatedPeerThreadCombinedStates: Set<PeerId>
let updatedPeerThreadsSummaries: Set<PeerId>
let updatedPeerThreadInfos: Set<MessageHistoryThreadsTable.ItemId>
let updatedPinnedThreads: Set<PeerId>
let updatedHiddenPeerIds: Bool
let storyGeneralStatesEvents: [StoryGeneralStatesTable.Event]
@ -195,6 +196,9 @@ final class PostboxTransaction {
if !self.updatedPeerThreadsSummaries.isEmpty {
return false
}
if !self.updatedPeerThreadInfos.isEmpty {
return false
}
if !self.updatedPinnedThreads.isEmpty {
return false
}
@ -269,6 +273,7 @@ final class PostboxTransaction {
updatedMessageThreadPeerIds: Set<PeerId>,
updatedPeerThreadCombinedStates: Set<PeerId>,
updatedPeerThreadsSummaries: Set<PeerId>,
updatedPeerThreadInfos: Set<MessageHistoryThreadsTable.ItemId>,
updatedPinnedThreads: Set<PeerId>,
updatedHiddenPeerIds: Bool,
storyGeneralStatesEvents: [StoryGeneralStatesTable.Event],
@ -323,6 +328,7 @@ final class PostboxTransaction {
self.updatedMessageThreadPeerIds = updatedMessageThreadPeerIds
self.updatedPeerThreadCombinedStates = updatedPeerThreadCombinedStates
self.updatedPeerThreadsSummaries = updatedPeerThreadsSummaries
self.updatedPeerThreadInfos = updatedPeerThreadInfos
self.updatedPinnedThreads = updatedPinnedThreads
self.updatedHiddenPeerIds = updatedHiddenPeerIds
self.storyGeneralStatesEvents = storyGeneralStatesEvents

View File

@ -212,7 +212,7 @@ public final class SelectablePeerNode: ASDisplayNode {
public func setup(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) {
let isFirstTime = self.peer == nil
self.peer = peer
guard let mainPeer = peer.chatMainPeer else {
guard let mainPeer = peer.chatOrMonoforumMainPeer else {
return
}
@ -226,8 +226,10 @@ public final class SelectablePeerNode: ASDisplayNode {
}
var isForum = false
if let peer = peer.chatMainPeer, case let .channel(channel) = peer, channel.isForumOrMonoForum {
isForum = true
var isMonoforum = false
if let peer = peer.chatMainPeer, case let .channel(channel) = peer {
isForum = channel.isForum
isMonoforum = channel.isMonoForum
}
let text: String
@ -246,7 +248,15 @@ public final class SelectablePeerNode: ASDisplayNode {
}
self.textNode.maximumNumberOfLines = numberOfLines
self.textNode.attributedText = NSAttributedString(string: customTitle ?? text, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : defaultColor, paragraphAlignment: .center)
self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: contentSettings, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: isForum ? .roundedRect : .round, synchronousLoad: synchronousLoad)
let clipStyle: AvatarNodeClipStyle
if isMonoforum {
clipStyle = .bubble
} else if isForum {
clipStyle = .roundedRect
} else {
clipStyle = .round
}
self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: contentSettings, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad)
if let requiresStars {
let avatarBadgeOutline: UIImageView

View File

@ -760,9 +760,14 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
return
}
var isMonoforum = false
if case let .channel(channel) = mainPeer {
isMonoforum = channel.isMonoForum
}
var didPresent = false
var presentImpl: (() -> Void)?
let threads = threadList(accountPeerId: context.accountPeerId, postbox: context.stateManager.postbox, peerId: mainPeer.id)
let threads = threadList(accountPeerId: context.accountPeerId, postbox: context.stateManager.postbox, peerId: mainPeer.id, isMonoforum: isMonoforum)
|> deliverOnMainQueue
|> beforeNext { _ in
if !didPresent {
@ -1933,78 +1938,162 @@ private final class ShareContextReferenceContentSource: ContextReferenceContentS
}
}
private func threadList(accountPeerId: EnginePeer.Id, postbox: Postbox, peerId: EnginePeer.Id) -> Signal<EngineChatList, NoError> {
let viewKey: PostboxViewKey = .messageHistoryThreadIndex(
id: peerId,
summaryComponents: ChatListEntrySummaryComponents(
components: [:]
)
)
return postbox.combinedView(keys: [viewKey])
|> mapToSignal { view -> Signal<CombinedView, NoError> in
return postbox.transaction { transaction -> CombinedView in
if let peer = transaction.getPeer(accountPeerId) {
transaction.updatePeersInternal([peer]) { current, _ in
return current ?? peer
private func threadList(accountPeerId: EnginePeer.Id, postbox: Postbox, peerId: EnginePeer.Id, isMonoforum: Bool) -> Signal<EngineChatList, NoError> {
if isMonoforum {
let viewKey: PostboxViewKey = .savedMessagesIndex(peerId: peerId)
let interfaceStateKey: PostboxViewKey = .chatInterfaceState(peerId: peerId)
return postbox.combinedView(keys: [viewKey, interfaceStateKey])
|> map { views -> EngineChatList in
guard let view = views.views[viewKey] as? MessageHistorySavedMessagesIndexView else {
preconditionFailure()
}
var draft: EngineChatList.Draft?
if let interfaceStateView = views.views[interfaceStateKey] as? ChatInterfaceStateView {
if let embeddedState = interfaceStateView.value, let _ = embeddedState.overrideChatTimestamp {
if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) {
if let text = opaqueState.synchronizeableInputState?.text {
draft = EngineChatList.Draft(text: text, entities: opaqueState.synchronizeableInputState?.entities ?? [])
}
}
}
}
return view
}
}
|> map { views -> EngineChatList in
guard let view = views.views[viewKey] as? MessageHistoryThreadIndexView else {
preconditionFailure()
}
var items: [EngineChatList.Item] = []
for item in view.items {
guard let peer = view.peer else {
continue
}
guard let data = item.info.get(MessageHistoryThreadData.self) else {
continue
var items: [EngineChatList.Item] = []
for item in view.items {
guard let sourcePeer = item.peer else {
continue
}
let sourceId = PeerId(item.id)
var messages: [EngineMessage] = []
if let topMessage = item.topMessage {
messages.append(EngineMessage(topMessage))
}
let mappedMessageIndex = MessageIndex(id: MessageId(peerId: sourceId, namespace: item.index.id.namespace, id: item.index.id.id), timestamp: item.index.timestamp)
let readCounters = EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: Int32(item.unreadCount), markedUnread: item.markedUnread))]), isMuted: false)
var itemDraft: EngineChatList.Draft?
if let embeddedState = item.embeddedInterfaceState, let _ = embeddedState.overrideChatTimestamp {
if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) {
if let text = opaqueState.synchronizeableInputState?.text {
itemDraft = EngineChatList.Draft(text: text, entities: opaqueState.synchronizeableInputState?.entities ?? [])
}
}
}
items.append(EngineChatList.Item(
id: .chatList(sourceId),
index: .chatList(ChatListIndex(pinningIndex: item.pinnedIndex.flatMap(UInt16.init), messageIndex: mappedMessageIndex)),
messages: messages,
readCounters: readCounters,
isMuted: false,
draft: sourceId == accountPeerId ? draft : itemDraft,
threadData: nil,
renderedPeer: EngineRenderedPeer(peer: EnginePeer(sourcePeer)),
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
forumTopicData: nil,
topForumTopicItems: [],
hasFailed: false,
isContact: false,
autoremoveTimeout: nil,
storyStats: nil,
displayAsTopicList: false,
isPremiumRequiredToMessage: false,
mediaDraftContentType: nil
))
}
let pinnedIndex: EngineChatList.Item.PinnedIndex
if let index = item.pinnedIndex {
pinnedIndex = .index(index)
} else {
pinnedIndex = .none
}
let list = EngineChatList(
items: items.reversed(),
groupItems: [],
additionalItems: [],
hasEarlier: false,
hasLater: false,
isLoading: view.isLoading
)
items.append(EngineChatList.Item(
id: .forum(item.id),
index: .forum(pinnedIndex: pinnedIndex, timestamp: item.index.timestamp, threadId: item.id, namespace: item.index.id.namespace, id: item.index.id.id),
messages: item.topMessage.flatMap { [EngineMessage($0)] } ?? [],
readCounters: nil,
isMuted: false,
draft: nil,
threadData: data,
renderedPeer: EngineRenderedPeer(peer: EnginePeer(peer)),
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
forumTopicData: nil,
topForumTopicItems: [],
hasFailed: false,
isContact: false,
autoremoveTimeout: nil,
storyStats: nil,
displayAsTopicList: false,
isPremiumRequiredToMessage: false,
mediaDraftContentType: nil
))
return list
}
let list = EngineChatList(
items: items,
groupItems: [],
additionalItems: [],
hasEarlier: false,
hasLater: false,
isLoading: view.isLoading
} else {
let viewKey: PostboxViewKey = .messageHistoryThreadIndex(
id: peerId,
summaryComponents: ChatListEntrySummaryComponents(
components: [:]
)
)
return list
return postbox.combinedView(keys: [viewKey])
|> mapToSignal { view -> Signal<CombinedView, NoError> in
return postbox.transaction { transaction -> CombinedView in
if let peer = transaction.getPeer(accountPeerId) {
transaction.updatePeersInternal([peer]) { current, _ in
return current ?? peer
}
}
return view
}
}
|> map { views -> EngineChatList in
guard let view = views.views[viewKey] as? MessageHistoryThreadIndexView else {
preconditionFailure()
}
var items: [EngineChatList.Item] = []
for item in view.items {
guard let peer = view.peer else {
continue
}
guard let data = item.info.get(MessageHistoryThreadData.self) else {
continue
}
let pinnedIndex: EngineChatList.Item.PinnedIndex
if let index = item.pinnedIndex {
pinnedIndex = .index(index)
} else {
pinnedIndex = .none
}
items.append(EngineChatList.Item(
id: .forum(item.id),
index: .forum(pinnedIndex: pinnedIndex, timestamp: item.index.timestamp, threadId: item.id, namespace: item.index.id.namespace, id: item.index.id.id),
messages: item.topMessage.flatMap { [EngineMessage($0)] } ?? [],
readCounters: nil,
isMuted: false,
draft: nil,
threadData: data,
renderedPeer: EngineRenderedPeer(peer: EnginePeer(peer)),
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
forumTopicData: nil,
topForumTopicItems: [],
hasFailed: false,
isContact: false,
autoremoveTimeout: nil,
storyStats: nil,
displayAsTopicList: false,
isPremiumRequiredToMessage: false,
mediaDraftContentType: nil
))
}
let list = EngineChatList(
items: items,
groupItems: [],
additionalItems: [],
hasEarlier: false,
hasLater: false,
isLoading: view.isLoading
)
return list
}
}
}

View File

@ -16,14 +16,14 @@ final class ShareControllerInteraction {
var selectedPeerIds = Set<EnginePeer.Id>()
var selectedPeers: [EngineRenderedPeer] = []
var selectedTopics: [EnginePeer.Id: (Int64, MessageHistoryThreadData)] = [:]
var selectedTopics: [EnginePeer.Id: (Int64, MessageHistoryThreadData?)] = [:]
let togglePeer: (EngineRenderedPeer, Bool) -> Void
let selectTopic: (EngineRenderedPeer, Int64, MessageHistoryThreadData) -> Void
let selectTopic: (EngineRenderedPeer, Int64, MessageHistoryThreadData?) -> Void
let shareStory: (() -> Void)?
let disabledPeerSelected: (EngineRenderedPeer) -> Void
init(togglePeer: @escaping (EngineRenderedPeer, Bool) -> Void, selectTopic: @escaping (EngineRenderedPeer, Int64, MessageHistoryThreadData) -> Void, shareStory: (() -> Void)?, disabledPeerSelected: @escaping (EngineRenderedPeer) -> Void) {
init(togglePeer: @escaping (EngineRenderedPeer, Bool) -> Void, selectTopic: @escaping (EngineRenderedPeer, Int64, MessageHistoryThreadData?) -> Void, shareStory: (() -> Void)?, disabledPeerSelected: @escaping (EngineRenderedPeer) -> Void) {
self.togglePeer = togglePeer
self.selectTopic = selectTopic
self.shareStory = shareStory

View File

@ -12,22 +12,25 @@ import AccountContext
import ShimmerEffect
import ComponentFlow
import EmojiStatusComponent
import AvatarNode
final class ShareTopicGridItem: GridItem {
let environment: ShareControllerEnvironment
let context: ShareControllerAccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let basePeer: EnginePeer
let peer: EngineRenderedPeer?
let id: Int64
let threadInfo: MessageHistoryThreadData
let threadInfo: MessageHistoryThreadData?
let controllerInteraction: ShareControllerInteraction
let section: GridSection?
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer?, id: Int64, threadInfo: MessageHistoryThreadData, controllerInteraction: ShareControllerInteraction) {
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, basePeer: EnginePeer, peer: EngineRenderedPeer?, id: Int64, threadInfo: MessageHistoryThreadData?, controllerInteraction: ShareControllerInteraction) {
self.environment = environment
self.context = context
self.basePeer = basePeer
self.theme = theme
self.strings = strings
self.peer = peer
@ -52,6 +55,7 @@ final class ShareTopicGridItemNode: GridItemNode {
private let iconView: ComponentView<Empty>
private let textNode: ImmediateTextNode
private var avatarNode: AvatarNode?
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
@ -80,7 +84,11 @@ final class ShareTopicGridItemNode: GridItemNode {
@objc private func tapped() {
if let item = self.currentItem, let peer = item.peer {
item.controllerInteraction.selectTopic(peer, item.id, item.threadInfo)
if let threadInfo = item.threadInfo {
item.controllerInteraction.selectTopic(peer, item.id, threadInfo)
} else {
item.controllerInteraction.selectTopic(EngineRenderedPeer(peer: item.basePeer), item.id, nil)
}
}
}
@ -100,40 +108,59 @@ final class ShareTopicGridItemNode: GridItemNode {
}
self.currentItem = item
self.textNode.attributedText = NSAttributedString(string: item.threadInfo.info.title, font: Font.regular(11.0), textColor: item.theme.actionSheet.primaryTextColor)
if let threadInfo = item.threadInfo {
self.textNode.attributedText = NSAttributedString(string: threadInfo.info.title, font: Font.regular(11.0), textColor: item.theme.actionSheet.primaryTextColor)
let iconContent: EmojiStatusComponent.Content
if let fileId = threadInfo.info.icon {
iconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 96.0, height: 96.0), placeholderColor: item.theme.actionSheet.disabledActionTextColor, themeColor: item.theme.actionSheet.primaryTextColor, loopMode: .count(0))
} else {
iconContent = .topic(title: String(threadInfo.info.title.prefix(1)), color: threadInfo.info.iconColor, size: CGSize(width: 64.0, height: 64.0))
}
let iconSize = self.iconView.update(
transition: .easeInOut(duration: 0.2),
component: AnyComponent(EmojiStatusComponent(
postbox: item.context.stateManager.postbox,
energyUsageSettings: item.environment.energyUsageSettings,
resolveInlineStickers: item.context.resolveInlineStickers,
animationCache: item.context.animationCache,
animationRenderer: item.context.animationRenderer,
content: iconContent,
isVisibleForAnimations: true,
action: nil
)),
environment: {},
containerSize: CGSize(width: 54.0, height: 54.0)
)
if let iconComponentView = self.iconView.view {
if iconComponentView.superview == nil {
self.view.addSubview(iconComponentView)
}
iconComponentView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: 7.0), size: iconSize)
}
} else if let peer = item.peer, let mainPeer = peer.chatMainPeer {
self.textNode.attributedText = NSAttributedString(string: mainPeer.compactDisplayTitle, font: Font.regular(11.0), textColor: item.theme.actionSheet.primaryTextColor)
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0))
self.avatarNode = avatarNode
self.addSubnode(avatarNode)
}
let iconSize = CGSize(width: 54.0, height: 54.0)
avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: 7.0), size: iconSize)
avatarNode.updateSize(size: iconSize)
avatarNode.setPeer(accountPeerId: item.context.accountPeerId, postbox: item.context.stateManager.postbox, network: item.context.stateManager.network, contentSettings: ContentSettings.default, theme: item.theme, peer: mainPeer, overrideImage: nil, emptyColor: item.theme.list.mediaPlaceholderColor, clipStyle: .round, synchronousLoad: false)
}
let textSize = self.textNode.updateLayout(size)
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: 4.0 + 60.0 + 4.0), size: textSize)
self.textNode.frame = textFrame
let iconContent: EmojiStatusComponent.Content
if let fileId = item.threadInfo.info.icon {
iconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 96.0, height: 96.0), placeholderColor: item.theme.actionSheet.disabledActionTextColor, themeColor: item.theme.actionSheet.primaryTextColor, loopMode: .count(0))
} else {
iconContent = .topic(title: String(item.threadInfo.info.title.prefix(1)), color: item.threadInfo.info.iconColor, size: CGSize(width: 64.0, height: 64.0))
}
let iconSize = self.iconView.update(
transition: .easeInOut(duration: 0.2),
component: AnyComponent(EmojiStatusComponent(
postbox: item.context.stateManager.postbox,
energyUsageSettings: item.environment.energyUsageSettings,
resolveInlineStickers: item.context.resolveInlineStickers,
animationCache: item.context.animationCache,
animationRenderer: item.context.animationRenderer,
content: iconContent,
isVisibleForAnimations: true,
action: nil
)),
environment: {},
containerSize: CGSize(width: 54.0, height: 54.0)
)
if let iconComponentView = self.iconView.view {
if iconComponentView.superview == nil {
self.view.addSubview(iconComponentView)
}
iconComponentView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: 7.0), size: iconSize)
}
}
override func layout() {

View File

@ -18,9 +18,10 @@ private let subtitleFont = Font.regular(12.0)
private struct ShareTopicEntry: Comparable, Identifiable {
let index: Int32
let basePeer: EnginePeer
let peer: EngineRenderedPeer
let id: Int64
let threadData: MessageHistoryThreadData
let threadData: MessageHistoryThreadData?
let theme: PresentationTheme
let strings: PresentationStrings
@ -32,6 +33,9 @@ private struct ShareTopicEntry: Comparable, Identifiable {
if lhs.index != rhs.index {
return false
}
if lhs.basePeer != rhs.basePeer {
return false
}
if lhs.peer != rhs.peer {
return false
}
@ -53,7 +57,7 @@ private struct ShareTopicEntry: Comparable, Identifiable {
}
func item(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem {
return ShareTopicGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, peer: self.peer, id: self.id, threadInfo: self.threadData, controllerInteraction: interfaceInteraction)
return ShareTopicGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, basePeer: self.basePeer, peer: self.peer, id: self.id, threadInfo: self.threadData, controllerInteraction: interfaceInteraction)
}
}
@ -205,7 +209,10 @@ final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode {
for topic in topics {
if case let .forum(_, _, threadId, _, _) = topic.index, let threadData = topic.threadData {
entries.append(ShareTopicEntry(index: index, peer: EngineRenderedPeer(peer: peer), id: threadId, threadData: threadData, theme: theme, strings: strings))
entries.append(ShareTopicEntry(index: index, basePeer: peer, peer: EngineRenderedPeer(peer: peer), id: threadId, threadData: threadData, theme: theme, strings: strings))
index += 1
} else if case .chatList = topic.index {
entries.append(ShareTopicEntry(index: index, basePeer: peer, peer: topic.renderedPeer, id: topic.renderedPeer.peerId.toInt64(), threadData: nil, theme: theme, strings: strings))
index += 1
}
}

View File

@ -8923,13 +8923,14 @@ public extension Api.functions.messages {
}
}
public extension Api.functions.messages {
static func unpinAllMessages(flags: Int32, peer: Api.InputPeer, topMsgId: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.AffectedHistory>) {
static func unpinAllMessages(flags: Int32, peer: Api.InputPeer, topMsgId: Int32?, savedPeerId: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.AffectedHistory>) {
let buffer = Buffer()
buffer.appendInt32(-299714136)
buffer.appendInt32(103667527)
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)}
return (FunctionDescription(name: "messages.unpinAllMessages", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("topMsgId", String(describing: topMsgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.AffectedHistory? in
if Int(flags) & Int(1 << 1) != 0 {savedPeerId!.serialize(buffer, true)}
return (FunctionDescription(name: "messages.unpinAllMessages", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("topMsgId", String(describing: topMsgId)), ("savedPeerId", String(describing: savedPeerId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.AffectedHistory? in
let reader = BufferReader(buffer)
var result: Api.messages.AffectedHistory?
if let signature = reader.readInt32() {

View File

@ -161,7 +161,8 @@ extension StoredMessageHistoryThreadInfo {
self.init(data: entry, summary: Summary(
totalUnreadCount: data.incomingUnreadCount,
isMarkedUnread: data.isMarkedUnread,
mutedUntil: mutedUntil
mutedUntil: mutedUntil,
maxOutgoingReadId: data.maxOutgoingReadId
))
}
}

View File

@ -4262,7 +4262,7 @@ func replayFinalState(
} else {
updatedIncomingThreadReadStates[peerAndThreadId] = readMaxId
}
if let channel = transaction.getPeer(peerAndThreadId.peerId) as? TelegramChannel, case .group = channel.info, channel.flags.contains(.isForum) {
if let channel = transaction.getPeer(peerAndThreadId.peerId) as? TelegramChannel, case .group = channel.info, (channel.flags.contains(.isForum) || channel.flags.contains(.isMonoforum)) {
let threadId = peerAndThreadId.threadId
if var data = transaction.getMessageHistoryThreadInfo(peerId: peerAndThreadId.peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
if readMaxId > data.maxIncomingReadId {
@ -4315,7 +4315,7 @@ func replayFinalState(
} else {
updatedOutgoingThreadReadStates[peerAndThreadId] = readMaxId
}
if let channel = transaction.getPeer(peerAndThreadId.peerId) as? TelegramChannel, case .group = channel.info, channel.flags.contains(.isForum) {
if let channel = transaction.getPeer(peerAndThreadId.peerId) as? TelegramChannel, case .group = channel.info, (channel.flags.contains(.isForum) || channel.flags.contains(.isMonoforum)) {
if var data = transaction.getMessageHistoryThreadInfo(peerId: peerAndThreadId.peerId, threadId: peerAndThreadId.threadId)?.data.get(MessageHistoryThreadData.self) {
if readMaxId >= data.maxOutgoingReadId {
data.maxOutgoingReadId = readMaxId

View File

@ -2345,7 +2345,8 @@ public func messagesForNotification(transaction: Transaction, id: MessageId, alw
var notificationSettingsStack: [TelegramPeerNotificationSettings] = []
if let threadId = message.threadId, let threadData = transaction.getMessageHistoryThreadInfo(peerId: message.id.peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
if let peer = peer as? TelegramChannel, peer.isMonoForum {
} else if let threadId = message.threadId, let threadData = transaction.getMessageHistoryThreadInfo(peerId: message.id.peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
notificationSettingsStack.append(threadData.notificationSettings)
}

View File

@ -949,7 +949,9 @@ public final class PendingMessageManager {
var monoforumPeerId: Api.InputPeer?
if let threadId = messages[0].0.threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = transaction.getPeer(linkedMonoforumId) as? TelegramChannel, mainChannel.hasPermission(.sendSomething) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
}
} else {
flags |= Int32(1 << 9)
topMsgId = Int32(clamping: threadId)
@ -1043,7 +1045,9 @@ public final class PendingMessageManager {
var monoforumPeerId: Api.InputPeer?
if let threadId = messages[0].0.threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = transaction.getPeer(linkedMonoforumId) as? TelegramChannel, mainChannel.hasPermission(.sendSomething) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
}
} else {
flags |= Int32(1 << 9)
topMsgId = Int32(clamping: threadId)
@ -1352,7 +1356,9 @@ public final class PendingMessageManager {
var monoforumPeerId: Api.InputPeer?
if let threadId = message.threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = transaction.getPeer(linkedMonoforumId) as? TelegramChannel, mainChannel.hasPermission(.sendSomething) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
}
} else {
topMsgId = Int32(clamping: threadId)
}
@ -1615,7 +1621,9 @@ public final class PendingMessageManager {
var monoforumPeerId: Api.InputPeer?
if let threadId = message.threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = transaction.getPeer(linkedMonoforumId) as? TelegramChannel, mainChannel.hasPermission(.sendSomething) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
}
} else {
flags |= Int32(1 << 9)
topMsgId = Int32(clamping: threadId)

View File

@ -194,7 +194,7 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
},
automaticThreadIndexInfo: { peerId, _ in
if peerId.namespace == Namespaces.Peer.CloudUser {
return StoredMessageHistoryThreadInfo(data: CodableEntry(data: Data()), summary: StoredMessageHistoryThreadInfo.Summary(totalUnreadCount: 0, isMarkedUnread: false, mutedUntil: nil))
return StoredMessageHistoryThreadInfo(data: CodableEntry(data: Data()), summary: StoredMessageHistoryThreadInfo.Summary(totalUnreadCount: 0, isMarkedUnread: false, mutedUntil: nil, maxOutgoingReadId: 0))
} else {
return nil
}

View File

@ -204,6 +204,11 @@ public extension TelegramEngine.EngineData.Item {
return nil
}
peers[mainPeer.id] = EnginePeer(mainPeer)
} else if let channel = peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId {
guard let mainChannel = view.peers[linkedMonoforumId] else {
return nil
}
peers[mainChannel.id] = EnginePeer(mainChannel)
}
return EngineRenderedPeer(peerId: self.id, peers: peers, associatedMedia: view.media)

View File

@ -110,12 +110,18 @@ func _internal_requestUpdatePinnedMessage(account: Account, peerId: PeerId, upda
}
func _internal_requestUnpinAllMessages(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<Never, UpdatePinnedMessageError> {
return account.postbox.transaction { transaction -> (Peer?, CachedPeerData?) in
return (transaction.getPeer(peerId), transaction.getPeerCachedData(peerId: peerId))
return account.postbox.transaction { transaction -> (Peer?, Peer?, CachedPeerData?) in
let peer = transaction.getPeer(peerId)
var subPeer: Peer?
if let channel = peer as? TelegramChannel, channel.isMonoForum, let threadId {
subPeer = transaction.getPeer(PeerId(threadId))
}
return (peer, subPeer, transaction.getPeerCachedData(peerId: peerId))
}
|> mapError { _ -> UpdatePinnedMessageError in
}
|> mapToSignal { peer, cachedPeerData -> Signal<Never, UpdatePinnedMessageError> in
|> mapToSignal { peer, subPeer, cachedPeerData -> Signal<Never, UpdatePinnedMessageError> in
guard let peer = peer, let inputPeer = apiInputPeer(peer) else {
return .fail(.generic)
}
@ -148,10 +154,20 @@ func _internal_requestUnpinAllMessages(account: Account, peerId: PeerId, threadI
}
var flags: Int32 = 0
if threadId != nil {
flags |= (1 << 0)
var topMsgId: Int32?
var savedPeerId: Api.InputPeer?
if let threadId {
if let channel = peer as? TelegramChannel, channel.isMonoForum {
if let inputSubPeer = subPeer.flatMap(apiInputPeer) {
flags |= (1 << 1)
savedPeerId = inputSubPeer
}
} else {
flags |= (1 << 0)
topMsgId = Int32(clamping: threadId)
}
}
let request: Signal<Never, InternalError> = account.network.request(Api.functions.messages.unpinAllMessages(flags: flags, peer: inputPeer, topMsgId: threadId.flatMap(Int32.init(clamping:))))
let request: Signal<Never, InternalError> = account.network.request(Api.functions.messages.unpinAllMessages(flags: flags, peer: inputPeer, topMsgId: topMsgId, savedPeerId: savedPeerId))
|> mapError { error -> InternalError in
return .error(error.errorDescription)
}

View File

@ -1262,21 +1262,28 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
let starsString = strings.Notification_PaidMessagePriceChanged_Stars(Int32(stars))
if message.author?.id == accountPeerId {
let resultString: PresentationStrings.FormattedString
if broadcastMessagesAllowed {
resultString = strings.Notification_PaidMessagePriceChangedAndEnabledChannelMessageYou(starsString)
} else {
resultString = strings.Notification_PaidMessagePriceChangedYou(starsString)
}
resultString = strings.Notification_PaidMessagePriceChangedYou(starsString)
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} else {
let peerName = message.author?.compactDisplayTitle ?? ""
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])
attributes[1] = boldAttributes
let resultString: PresentationStrings.FormattedString
if broadcastMessagesAllowed {
resultString = strings.Notification_PaidMessagePriceChangedAndEnabledChannelMessage(peerName, starsString)
if stars == 0 {
resultString = strings.Notification_ChannelMessagePriceZeroChanged(peerName)
} else {
var rawString = strings.Notification_ChannelMessagePriceChanged(Int32(stars))
rawString = rawString.replacingOccurrences(of: "{name}", with: peerName)
resultString = PresentationStrings.FormattedString(string: rawString, ranges: [])
}
} else {
resultString = strings.Notification_PaidMessagePriceChanged(peerName, starsString)
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
resultString = strings.Notification_ChannelMessageDisabled(peerName)
} else {
resultString = strings.Notification_PaidMessagePriceChanged(peerName, starsString)
}
}
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes)
}

View File

@ -479,6 +479,8 @@ swift_library(
"//submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen",
"//submodules/TelegramUI/Components/ForumSettingsScreen",
"//submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel",
"//submodules/TelegramUI/Components/GifVideoLayer",
"//submodules/TelegramUI/Components/BatchVideoRendering",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AsyncListComponent",
module_name = "AsyncListComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/MergeLists",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,603 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import MergeLists
import ComponentDisplayAdapters
public final class AsyncListComponent: Component {
public protocol ItemView: UIView {
func isReorderable(at point: CGPoint) -> Bool
}
public final class OverlayContainerView: UIView {
public override init(frame: CGRect) {
super.init(frame: frame)
self.layer.anchorPoint = CGPoint()
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func updatePosition(position: CGPoint, transition: ComponentTransition) {
let previousPosition: CGPoint
var forceUpdate = false
if self.layer.animation(forKey: "positionUpdate") != nil, let presentation = self.layer.presentation() {
forceUpdate = true
previousPosition = presentation.position
if !transition.animation.isImmediate {
self.layer.removeAnimation(forKey: "positionUpdate")
}
} else {
previousPosition = self.layer.position
}
if previousPosition != position || forceUpdate {
self.center = position
if case let .curve(duration, curve) = transition.animation {
self.layer.animate(
from: NSValue(cgPoint: CGPoint(x: previousPosition.x - position.x, y: previousPosition.y - position.y)),
to: NSValue(cgPoint: CGPoint()),
keyPath: "position",
duration: duration,
delay: 0.0,
curve: curve,
removeOnCompletion: true,
additive: true,
completion: nil,
key: "positionUpdate"
)
}
}
}
}
final class ResetScrollingRequest: Equatable {
let requestId: Int
let id: AnyHashable
init(requestId: Int, id: AnyHashable) {
self.requestId = requestId
self.id = id
}
static func ==(lhs: ResetScrollingRequest, rhs: ResetScrollingRequest) -> Bool {
if lhs === rhs {
return true
}
if lhs.requestId != rhs.requestId {
return false
}
if lhs.id != rhs.id {
return false
}
return true
}
}
public final class ExternalState {
public struct Value: Equatable {
var resetScrollingRequest: ResetScrollingRequest?
public static func ==(lhs: Value, rhs: Value) -> Bool {
if lhs.resetScrollingRequest != rhs.resetScrollingRequest {
return false
}
return true
}
}
public private(set) var value: Value = Value()
private var nextId: Int = 0
public init() {
}
public func resetScrolling(id: AnyHashable) {
let requestId = self.nextId
self.nextId += 1
self.value.resetScrollingRequest = ResetScrollingRequest(requestId: requestId, id: id)
}
}
public enum Direction {
case vertical
case horizontal
}
public final class VisibleItem {
public let item: AnyComponentWithIdentity<Empty>
public let frame: CGRect
init(item: AnyComponentWithIdentity<Empty>, frame: CGRect) {
self.item = item
self.frame = frame
}
}
public final class VisibleItems: Sequence, IteratorProtocol {
private let view: AsyncListComponent.View
private var index: Int = 0
private let indices: [(Int, CGRect)]
init(view: AsyncListComponent.View, direction: Direction) {
self.view = view
var indices: [(Int, CGRect)] = []
view.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListItemNodeImpl, let index = itemNode.index {
var itemFrame = itemNode.frame
itemFrame.origin.y -= itemNode.transitionOffset
if let animation = itemNode.animationForKey("height") {
if let height = animation.to as? CGFloat {
itemFrame.size.height = height
}
}
if case .horizontal = direction {
itemFrame = CGRect(origin: CGPoint(x: itemFrame.minY, y: itemFrame.minX), size: CGSize(width: itemFrame.height, height: itemFrame.width))
}
indices.append((index, itemFrame))
}
}
indices.sort(by: { $0.0 < $1.0 })
self.indices = indices
}
public func next() -> VisibleItem? {
if self.index >= self.indices.count {
return nil
}
let index = self.index
self.index += 1
if let component = self.view.component {
let (itemIndex, itemFrame) = self.indices[index]
return VisibleItem(item: component.items[itemIndex], frame: itemFrame)
}
return nil
}
}
public let externalState: ExternalState
public let externalStateValue: ExternalState.Value
public let items: [AnyComponentWithIdentity<Empty>]
public let itemSetId: AnyHashable // Changing itemSetId supresses update animations
public let direction: Direction
public let insets: UIEdgeInsets
public let reorderItems: ((Int, Int) -> Bool)?
public let onVisibleItemsUpdated: ((VisibleItems, ComponentTransition) -> Void)?
public init(
externalState: ExternalState,
items: [AnyComponentWithIdentity<Empty>],
itemSetId: AnyHashable,
direction: Direction,
insets: UIEdgeInsets,
reorderItems: ((Int, Int) -> Bool)? = nil,
onVisibleItemsUpdated: ((VisibleItems, ComponentTransition) -> Void)? = nil
) {
self.externalState = externalState
self.externalStateValue = externalState.value
self.items = items
self.itemSetId = itemSetId
self.direction = direction
self.insets = insets
self.reorderItems = reorderItems
self.onVisibleItemsUpdated = onVisibleItemsUpdated
}
public static func ==(lhs: AsyncListComponent, rhs: AsyncListComponent) -> Bool {
if lhs.externalState !== rhs.externalState {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.itemSetId != rhs.itemSetId {
return false
}
if lhs.direction != rhs.direction {
return false
}
if lhs.insets != rhs.insets {
return false
}
if (lhs.reorderItems == nil) != (rhs.reorderItems == nil) {
return false
}
return true
}
private struct ItemEntry: Comparable, Identifiable {
let contents: AnyComponentWithIdentity<Empty>
let index: Int
var id: AnyHashable {
return self.contents.id
}
var stableId: AnyHashable {
return self.id
}
static func ==(lhs: ItemEntry, rhs: ItemEntry) -> Bool {
if lhs.contents != rhs.contents {
return false
}
if lhs.index != rhs.index {
return false
}
return true
}
static func <(lhs: ItemEntry, rhs: ItemEntry) -> Bool {
return lhs.index < rhs.index
}
func item(parentView: AsyncListComponent.View?, direction: Direction) -> ListViewItem {
return ListItemImpl(parentView: parentView, contents: self.contents, direction: direction)
}
}
private final class ListItemImpl: ListViewItem {
weak var parentView: AsyncListComponent.View?
let contents: AnyComponentWithIdentity<Empty>
let direction: Direction
let selectable: Bool = false
init(parentView: AsyncListComponent.View?, contents: AnyComponentWithIdentity<Empty>, direction: Direction) {
self.parentView = parentView
self.contents = contents
self.direction = direction
}
func nodeConfiguredForParams(
async: @escaping (@escaping () -> Void) -> Void,
params: ListViewItemLayoutParams,
synchronousLoads: Bool,
previousItem: ListViewItem?,
nextItem: ListViewItem?,
completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void
) {
async {
let impl: () -> Void = {
let node = ListItemNodeImpl()
let (nodeLayout, apply) = node.asyncLayout()(self, params)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply(false)
})
})
}
}
if Thread.isMainThread {
impl()
} else {
assert(false)
Queue.mainQueue().async {
impl()
}
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ListItemNodeImpl)
if let nodeValue = node() as? ListItemNodeImpl {
let layout = nodeValue.asyncLayout()
async {
let impl: () -> Void = {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(animation.isAnimated)
})
}
}
if Thread.isMainThread {
impl()
} else {
assert(false)
Queue.mainQueue().async {
impl()
}
}
}
}
}
}
}
private final class ListItemNodeImpl: ListViewItemNode {
private let contentsView = ComponentView<Empty>()
private(set) var item: ListItemImpl?
init() {
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
}
deinit {
}
override func isReorderable(at point: CGPoint) -> Bool {
if let itemView = self.contentsView.view as? ItemView {
return itemView.isReorderable(at: self.view.convert(point, to: itemView))
}
return false
}
override func snapshotForReordering() -> UIView? {
return self.view.snapshotView(afterScreenUpdates: false)
}
func asyncLayout() -> (ListItemImpl, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
return { item, params in
let containerSize: CGSize
switch item.direction {
case .vertical:
containerSize = CGSize(width: params.width, height: 100000.0)
case .horizontal:
containerSize = CGSize(width: 100000.0, height: params.width)
}
let contentsSize = self.contentsView.update(
transition: .immediate,
component: item.contents.component,
environment: {},
containerSize: containerSize
)
let mappedContentsSize: CGSize
switch item.direction {
case .vertical:
mappedContentsSize = CGSize(width: params.width, height: contentsSize.height)
case .horizontal:
mappedContentsSize = CGSize(width: params.width, height: contentsSize.width)
}
let itemLayout = ListViewItemNodeLayout(contentSize: mappedContentsSize, insets: UIEdgeInsets())
return (itemLayout, { animated in
self.item = item
switch item.direction {
case .vertical:
self.layer.sublayerTransform = CATransform3DIdentity
case .horizontal:
self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
}
let contentsFrame = CGRect(origin: CGPoint(), size: contentsSize)
if let contentsComponentView = self.contentsView.view {
if contentsComponentView.superview == nil {
self.view.addSubview(contentsComponentView)
}
contentsComponentView.center = CGPoint(x: mappedContentsSize.width * 0.5, y: mappedContentsSize.height * 0.5)
contentsComponentView.bounds = CGRect(origin: CGPoint(), size: contentsFrame.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
super.animateInsertion(currentTimestamp, duration: duration, options: options)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
super.animateRemoved(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
public final class View: UIView {
let listNode: ListView
private var externalStateValue: ExternalState.Value?
private var isUpdating: Bool = false
private(set) var component: AsyncListComponent?
private var currentEntries: [ItemEntry] = []
private var ignoreUpdateVisibleItems: Bool = false
public override init(frame: CGRect) {
self.listNode = ListView()
self.listNode.useMainQueueTransactions = true
self.listNode.scroller.delaysContentTouches = false
self.listNode.reorderedItemHasShadow = false
super.init(frame: frame)
self.addSubview(self.listNode.view)
self.listNode.onContentsUpdated = { [weak self] transition in
guard let self else {
return
}
self.updateVisibleItems(transition: ComponentTransition(transition))
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
public func stopScrolling() {
self.listNode.stopScrolling()
}
private func updateVisibleItems(transition: ComponentTransition) {
if self.ignoreUpdateVisibleItems {
return
}
guard let component = self.component else {
return
}
if let onVisibleItemsUpdated = component.onVisibleItemsUpdated {
onVisibleItemsUpdated(VisibleItems(view: self, direction: component.direction), transition)
}
}
func update(component: AsyncListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
let listSize: CGSize
let listInsets: UIEdgeInsets
switch component.direction {
case .vertical:
self.listNode.transform = CATransform3DIdentity
listSize = CGSize(width: availableSize.width, height: availableSize.height)
listInsets = component.insets
case .horizontal:
self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
listSize = CGSize(width: availableSize.height, height: availableSize.width)
listInsets = UIEdgeInsets(top: component.insets.left, left: component.insets.top, bottom: component.insets.right, right: component.insets.bottom)
}
var updateSizeAndInsets = ListViewUpdateSizeAndInsets(
size: listSize,
insets: listInsets,
duration: 0.0,
curve: .Default(duration: nil)
)
var animateTransition = false
var transactionOptions: ListViewDeleteAndInsertOptions = []
if !transition.animation.isImmediate, let previousComponent {
if previousComponent.itemSetId == component.itemSetId {
transactionOptions.insert(.AnimateInsertion)
}
animateTransition = true
switch transition.animation {
case .none:
break
case let .curve(duration, curve):
updateSizeAndInsets.duration = duration
switch curve {
case .linear, .easeInOut:
updateSizeAndInsets.curve = .Default(duration: duration)
case .spring:
updateSizeAndInsets.curve = .Spring(duration: duration)
case let .custom(a, b, c, d):
updateSizeAndInsets.curve = .Custom(duration: duration, a, b, c, d)
}
}
}
var entries: [ItemEntry] = []
for item in component.items {
entries.append(ItemEntry(
contents: item,
index: entries.count
))
}
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 }) {
scrollToItem = ListViewScrollToItem(
index: index,
position: .visible,
animated: animateTransition,
curve: updateSizeAndInsets.curve,
directionHint: .Down
)
}
}
self.ignoreUpdateVisibleItems = true
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries)
self.currentEntries = entries
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(parentView: self, direction: component.direction), directionHint: .Down) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(parentView: self, direction: component.direction), directionHint: nil) }
transactionOptions.insert(.Synchronous)
self.listNode.transaction(
deleteIndices: deletions,
insertIndicesAndItems: insertions,
updateIndicesAndItems: updates,
options: transactionOptions,
scrollToItem: scrollToItem,
updateSizeAndInsets: updateSizeAndInsets,
stationaryItemRange: nil,
updateOpaqueState: nil,
completion: { _ in }
)
let mappedListFrame: CGRect
switch component.direction {
case .vertical:
mappedListFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5), size: listSize)
case .horizontal:
mappedListFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5), size: listSize)
}
self.listNode.position = mappedListFrame.origin
self.listNode.bounds = CGRect(origin: CGPoint(), size: mappedListFrame.size)
self.listNode.reorderItem = { [weak self] fromIndex, toIndex, _ in
guard let self, let component = self.component else {
return .single(false)
}
guard let reorderItems = component.reorderItems else {
return .single(false)
}
if reorderItems(fromIndex, toIndex) {
return .single(true)
} else {
return .single(false)
}
}
self.ignoreUpdateVisibleItems = false
self.updateVisibleItems(transition: transition)
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -382,7 +382,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = presentationData
//TODO:localize
let text: String = "Tap here to suggest a message"
let text: String = "Tap here to send a message"
let tooltipController = TooltipScreen(
account: context.account,

View File

@ -19,7 +19,8 @@ swift_library(
"//submodules/AppBundle",
"//submodules/ContextUI",
"//submodules/SoftwareVideo",
"//submodules/TelegramUI/Components/MultiplexedVideoNode",
"//submodules/TelegramUI/Components/BatchVideoRendering",
"//submodules/TelegramUI/Components/GifVideoLayer",
],
visibility = [
"//visibility:public",

View File

@ -10,17 +10,21 @@ import PhotoResources
import AppBundle
import ContextUI
import SoftwareVideo
import MultiplexedVideoNode
import BatchVideoRendering
import GifVideoLayer
import AccountContext
public final class ChatContextResultPeekContent: PeekControllerContent {
public let account: Account
public let context: AccountContext
public let contextResult: ChatContextResult
public let menu: [ContextMenuItem]
public let batchVideoContext: BatchVideoRenderingContext
public init(account: Account, contextResult: ChatContextResult, menu: [ContextMenuItem]) {
self.account = account
public init(context: AccountContext, contextResult: ChatContextResult, menu: [ContextMenuItem], batchVideoContext: BatchVideoRenderingContext) {
self.context = context
self.contextResult = contextResult
self.menu = menu
self.batchVideoContext = batchVideoContext
}
public func presentation() -> PeekControllerContentPresentation {
@ -36,7 +40,7 @@ public final class ChatContextResultPeekContent: PeekControllerContent {
}
public func node() -> PeekControllerContentNode & ASDisplayNode {
return ChatContextResultPeekNode(account: self.account, contextResult: self.contextResult)
return ChatContextResultPeekNode(context: self.context, contextResult: self.contextResult, batchVideoContext: self.batchVideoContext)
}
public func topAccessoryNode() -> ASDisplayNode? {
@ -62,62 +66,29 @@ public final class ChatContextResultPeekContent: PeekControllerContent {
}
private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerContentNode {
private let account: Account
private let context: AccountContext
private let contextResult: ChatContextResult
private let batchVideoContext: BatchVideoRenderingContext
private let imageNodeBackground: ASDisplayNode
private let imageNode: TransformImageNode
private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
private var videoLayer: GifVideoLayer?
private var currentImageResource: TelegramMediaResource?
private var currentVideoFile: TelegramMediaFile?
private let timebase: CMTimebase
private var displayLink: CADisplayLink?
private var ticking: Bool = false {
didSet {
if self.ticking != oldValue {
if self.ticking {
class DisplayLinkProxy: NSObject {
weak var target: ChatContextResultPeekNode?
init(target: ChatContextResultPeekNode) {
self.target = target
}
@objc func displayLinkEvent() {
self.target?.displayLinkEvent()
}
}
let displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
self.displayLink = displayLink
displayLink.add(to: RunLoop.main, forMode: .common)
if #available(iOS 10.0, *) {
displayLink.preferredFramesPerSecond = 25
} else {
displayLink.frameInterval = 2
}
displayLink.isPaused = false
CMTimebaseSetRate(self.timebase, rate: 1.0)
} else if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.isPaused = true
displayLink.invalidate()
CMTimebaseSetRate(self.timebase, rate: 0.0)
}
self.videoLayer?.shouldBeAnimating = self.ticking
}
}
}
private func displayLinkEvent() {
let timestamp = CMTimebaseGetTime(self.timebase).seconds
self.videoLayer?.1.tick(timestamp: timestamp)
}
init(account: Account, contextResult: ChatContextResult) {
self.account = account
init(context: AccountContext, contextResult: ChatContextResult, batchVideoContext: BatchVideoRenderingContext) {
self.context = context
self.contextResult = contextResult
self.batchVideoContext = batchVideoContext
self.imageNodeBackground = ASDisplayNode()
self.imageNodeBackground.isLayerBacked = true
@ -128,11 +99,6 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
self.imageNode.displaysAsynchronously = false
var timebase: CMTimebase?
CMTimebaseCreateWithSourceClock(allocator: nil, sourceClock: CMClockGetHostTimeClock(), timebaseOut: &timebase)
CMTimebaseSetRate(timebase!, rate: 0.0)
self.timebase = timebase!
super.init()
self.addSubnode(self.imageNodeBackground)
@ -142,10 +108,6 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
}
deinit {
if let displayLink = self.displayLink {
displayLink.isPaused = true
displayLink.invalidate()
}
}
func ready() -> Signal<Bool, NoError> {
@ -236,7 +198,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
if let imageResource = imageResource {
let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: Int32(fittedImageDimensions.width * 2.0), height: Int32(fittedImageDimensions.height * 2.0)), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
updateImageSignal = chatMessagePhoto(postbox: self.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage))
updateImageSignal = chatMessagePhoto(postbox: self.context.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage))
} else {
updateImageSignal = .complete()
}
@ -256,33 +218,26 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
}
if updatedVideoFile {
if let (thumbnailLayer, _, layer) = self.videoLayer {
if let videoLayer = self.videoLayer {
self.videoLayer = nil
thumbnailLayer.removeFromSupernode()
layer.layer.removeFromSuperlayer()
videoLayer.removeFromSuperlayer()
}
if let videoFileReference = videoFileReference {
let thumbnailLayer = SoftwareVideoThumbnailNode(account: self.account, fileReference: videoFileReference, synchronousLoad: false)
self.addSubnode(thumbnailLayer)
let layerHolder = takeSampleBufferLayer()
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.layer.addSublayer(layerHolder.layer)
let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .other, fileReference: videoFileReference, layerHolder: layerHolder)
self.videoLayer = (thumbnailLayer, manager, layerHolder)
thumbnailLayer.ready = { [weak self, weak thumbnailLayer, weak manager] in
if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager {
if strongSelf.videoLayer?.0 === thumbnailLayer && strongSelf.videoLayer?.1 === manager {
manager.start()
}
}
}
if let videoFileReference {
let videoLayer = GifVideoLayer(
context: self.context,
batchVideoContext: self.batchVideoContext,
userLocation: .other,
file: videoFileReference,
synchronousLoad: false
)
self.videoLayer = videoLayer
self.layer.addSublayer(videoLayer)
}
}
if let (thumbnailLayer, _, layer) = self.videoLayer {
thumbnailLayer.frame = CGRect(origin: CGPoint(), size: croppedImageDimensions)
layer.layer.frame = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
if let videoLayer = self.videoLayer {
videoLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
}
if !self.ticking {

View File

@ -1597,7 +1597,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
if isMonoForum {
if case .replyThread = item.chatLocation {
if case let .replyThread(replyThreadMessage) = item.chatLocation, firstMessage.effectivelyIncoming(item.context.account.peerId), item.effectiveAuthorId == PeerId(replyThreadMessage.threadId) {
displayAuthorInfo = false
}
}

View File

@ -24,6 +24,13 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/AvatarNode",
"//submodules/ChatListUI",
"//submodules/ContextUI",
"//submodules/TelegramUI/Components/AsyncListComponent",
"//submodules/TelegramUI/Components/TextBadgeComponent",
"//submodules/TelegramUI/Components/MaskedContainerComponent",
"//submodules/AppBundle",
"//submodules/PresentationDataUtils",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MaskedContainerComponent",
module_name = "MaskedContainerComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,94 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public final class MaskedContainerView: UIView {
public struct Item: Equatable {
public enum Shape: Equatable {
case ellipse
case roundedRect(cornerRadius: CGFloat)
}
public var frame: CGRect
public var shape: Shape
public init(frame: CGRect, shape: Shape) {
self.frame = frame
self.shape = shape
}
}
private struct Params: Equatable {
let size: CGSize
let items: [Item]
let isInverted: Bool
init(size: CGSize, items: [Item], isInverted: Bool) {
self.size = size
self.items = items
self.isInverted = isInverted
}
}
public let contentView: UIView
public let contentMaskView: UIImageView
private var params: Params?
override public init(frame: CGRect) {
self.contentView = UIView()
self.contentMaskView = UIImageView()
super.init(frame: frame)
self.addSubview(self.contentView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(size: CGSize, items: [Item], isInverted: Bool) {
let params = Params(size: size, items: items, isInverted: isInverted)
if self.params == params {
return
}
self.params = params
self.contentView.frame = CGRect(origin: CGPoint(), size: size)
self.contentMaskView.frame = CGRect(origin: CGPoint(), size: size)
if items.isEmpty {
self.contentMaskView.image = nil
self.contentView.mask = nil
} else {
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: size))
let image = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
if isInverted {
context.cgContext.setFillColor(UIColor.black.cgColor)
context.cgContext.fill(CGRect(origin: CGPoint(), size: size))
context.cgContext.setFillColor(UIColor.clear.cgColor)
context.cgContext.setBlendMode(.copy)
}
for item in items {
switch item.shape {
case .ellipse:
context.cgContext.fillEllipse(in: item.frame)
case let .roundedRect(cornerRadius):
context.cgContext.addPath(UIBezierPath(roundedRect: item.frame, cornerRadius: cornerRadius).cgPath)
context.cgContext.fillPath()
}
}
UIGraphicsPopContext()
}
self.contentMaskView.image = image
self.contentView.mask = self.contentMaskView
}
}
}

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

@ -2220,7 +2220,7 @@ private func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostSt
}))
//TODO:localize
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPostSuggestionsSettings, label: .text(channel.linkedMonoforumId == nil ? "Off" : "On"), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Message Channel", icon: UIImage(bundleImageName: "Chat/Info/PostSuggestionsIcon"), action: {
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: {
interaction.editingOpenPostSuggestionsSetup()
}))
}

View File

@ -124,7 +124,7 @@ final class PostSuggestionsSettingsScreenComponent: Component {
}
if component.initialPrice != currentAmount {
let _ = component.context.engine.peers.updateChannelPaidMessagesStars(peerId: peer.id, stars: currentAmount, broadcastMessagesAllowed: true).startStandalone()
let _ = component.context.engine.peers.updateChannelPaidMessagesStars(peerId: peer.id, stars: currentAmount, broadcastMessagesAllowed: currentAmount != nil).startStandalone()
}
return true
@ -200,11 +200,10 @@ final class PostSuggestionsSettingsScreenComponent: Component {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Post Suggestion", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
text: .plain(NSAttributedString(string: environment.strings.ChannelMessages_Title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
@ -249,8 +248,7 @@ final class PostSuggestionsSettingsScreenComponent: Component {
contentHeight += 129.0
//TODO:localize
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Allow users to suggest posts for your channel.", attributes: MarkdownAttributes(
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.ChannelMessages_Info, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
@ -301,7 +299,7 @@ final class PostSuggestionsSettingsScreenComponent: Component {
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Allow Post Suggestions",
string: environment.strings.ChannelMessages_SwitchTitle,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
@ -385,26 +383,6 @@ final class PostSuggestionsSettingsScreenComponent: Component {
),
params: ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
))))
/*contentSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent(
theme: environment.theme,
content: .discrete(ListItemSliderSelectorComponent.Discrete(
values: sliderValueList.map { item in
return item
},
markPositions: false,
selectedIndex: max(0, min(sliderValueList.count - 1, self.starCount - 1)),
title: sliderTitle,
secondaryTitle: sliderSecondaryTitle,
selectedIndexUpdated: { [weak self] index in
guard let self else {
return
}
let index = max(0, min(sliderValueList.count, index))
self.starCount = index
self.state?.updated(transition: .immediate)
}
))
))))*/
let contentSectionSize = self.contentSection.update(
transition: transition,
@ -412,7 +390,7 @@ final class PostSuggestionsSettingsScreenComponent: Component {
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "PRICE FOR EACH SUGGESTION",
string: environment.strings.ChannelMessages_PriceSectionTitle,
font: Font.regular(13.0),
textColor: environment.theme.list.freeTextColor
)),
@ -420,7 +398,7 @@ final class PostSuggestionsSettingsScreenComponent: Component {
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "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.",
string: environment.strings.ChannelMessages_PriceSectionFooter,
font: Font.regular(13.0),
textColor: environment.theme.list.freeTextColor
)),

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,28 +277,45 @@ 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 controller = PeerSelectionControllerImpl(
PeerSelectionControllerParams(
context: strongSelf.context,
updatedPresentationData: nil,
filter: strongSelf.filter,
forumPeerId: peer.id,
hasFilters: false,
hasChatListSelector: false,
hasContactSelector: false,
hasGlobalSearch: false,
title: EnginePeer(peer).compactDisplayTitle,
attemptSelection: strongSelf.attemptSelection,
createNewGroup: nil,
pretendPresentedInModal: false,
multipleSelection: false,
forwardedMessageIds: [],
hasTypeHeaders: false,
selectForumThreads: false
let mainPeer: Signal<EnginePeer?, NoError>
if peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId {
mainPeer = strongSelf.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: linkedMonoforumId)
)
)
controller.peerSelected = strongSelf.peerSelected
strongSelf.push(controller)
} 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, peer.isMonoForum),
hasFilters: false,
hasChatListSelector: false,
hasContactSelector: false,
hasGlobalSearch: false,
title: displayPeer.compactDisplayTitle,
attemptSelection: strongSelf.attemptSelection,
createNewGroup: nil,
pretendPresentedInModal: false,
multipleSelection: false,
forwardedMessageIds: [],
hasTypeHeaders: false,
selectForumThreads: false
)
)
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 {
chatListLocation = .forum(peerId: 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 {
chatListLocation = .forum(peerId: 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))
}

View File

@ -198,7 +198,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec
}
private final class ThemeCarouselThemeItemIconNode : ListViewItemNode {
private final class ThemeCarouselThemeItemIconNode: ListViewItemNode {
private let containerNode: ASDisplayNode
private let emojiContainerNode: ASDisplayNode
private let imageNode: TransformImageNode

View File

@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "TextBadgeComponent",
module_name = "TextBadgeComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,171 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public final class TextBadgeComponent: Component {
public let text: String
public let font: UIFont
public let background: UIColor
public let foreground: UIColor
public let insets: UIEdgeInsets
public init(
text: String,
font: UIFont,
background: UIColor,
foreground: UIColor,
insets: UIEdgeInsets
) {
self.text = text
self.font = font
self.background = background
self.foreground = foreground
self.insets = insets
}
public static func ==(lhs: TextBadgeComponent, rhs: TextBadgeComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.font != rhs.font {
return false
}
if lhs.background != rhs.background {
return false
}
if lhs.foreground != rhs.foreground {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
private struct TextLayout {
var size: CGSize
var opticalBounds: CGRect
init(size: CGSize, opticalBounds: CGRect) {
self.size = size
self.opticalBounds = opticalBounds
}
}
public final class View: UIView {
private let backgroundView: UIImageView
private let textContentsView: UIImageView
private var textLayout: TextLayout?
private var component: TextBadgeComponent?
override public init(frame: CGRect) {
self.backgroundView = UIImageView()
self.textContentsView = UIImageView()
self.textContentsView.layer.anchorPoint = CGPoint()
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.addSubview(self.textContentsView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: TextBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
if component.text != previousComponent?.text || component.font != previousComponent?.font {
let attributedText = NSAttributedString(string: component.text, attributes: [
NSAttributedString.Key.font: component.font,
NSAttributedString.Key.foregroundColor: UIColor.white
])
var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil)
boundingRect.size.width = ceil(boundingRect.size.width)
boundingRect.size.height = ceil(boundingRect.size.height)
if let context = DrawingContext(size: boundingRect.size, scale: 0.0, opaque: false, clear: true) {
context.withContext { c in
UIGraphicsPushContext(c)
defer {
UIGraphicsPopContext()
}
attributedText.draw(at: CGPoint())
}
var minFilledLineY = Int(context.scaledSize.height) - 1
var maxFilledLineY = 0
var minFilledLineX = Int(context.scaledSize.width) - 1
var maxFilledLineX = 0
for y in 0 ..< Int(context.scaledSize.height) {
let linePtr = context.bytes.advanced(by: max(0, y) * context.bytesPerRow).assumingMemoryBound(to: UInt32.self)
for x in 0 ..< Int(context.scaledSize.width) {
let pixelPtr = linePtr.advanced(by: x)
if pixelPtr.pointee != 0 {
minFilledLineY = min(y, minFilledLineY)
maxFilledLineY = max(y, maxFilledLineY)
minFilledLineX = min(x, minFilledLineX)
maxFilledLineX = max(x, maxFilledLineX)
}
}
}
var opticalBounds = CGRect()
if minFilledLineX <= maxFilledLineX && minFilledLineY <= maxFilledLineY {
opticalBounds.origin.x = CGFloat(minFilledLineX) / context.scale
opticalBounds.origin.y = CGFloat(minFilledLineY) / context.scale
opticalBounds.size.width = CGFloat(maxFilledLineX - minFilledLineX) / context.scale
opticalBounds.size.height = CGFloat(maxFilledLineY - minFilledLineY) / context.scale
}
self.textContentsView.image = context.generateImage()?.withRenderingMode(.alwaysTemplate)
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: opticalBounds)
} else {
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: CGRect(origin: CGPoint(), size: boundingRect.size))
}
}
let textSize = self.textLayout?.size ?? CGSize(width: 1.0, height: 1.0)
var size = CGSize(width: textSize.width + component.insets.left + component.insets.right, height: textSize.height + component.insets.top + component.insets.bottom)
size.width = max(size.width, size.height)
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) * 0.5), y: component.insets.top + UIScreenPixel), size: textSize)
/*if let textLayout = self.textLayout {
textFrame.origin.x = textLayout.opticalBounds.minX + floorToScreenPixels((backgroundFrame.width - textLayout.opticalBounds.width) * 0.5)
textFrame.origin.y = textLayout.opticalBounds.minY + floorToScreenPixels((backgroundFrame.height - textLayout.opticalBounds.height) * 0.5)
}*/
transition.setPosition(view: self.textContentsView, position: textFrame.origin)
self.textContentsView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
if size.height != self.backgroundView.image?.size.height {
self.backgroundView.image = generateStretchableFilledCircleImage(diameter: size.height, color: .white)?.withRenderingMode(.alwaysTemplate)
}
self.backgroundView.tintColor = component.background
self.textContentsView.tintColor = component.foreground
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

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

@ -7835,8 +7835,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum {
attributes.removeAll(where: { $0 is SendAsMessageAttribute })
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = self.presentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.sendSomething), let sendAsPeerId = self.presentationInterfaceState.currentSendAsPeerId {
attributes.append(SendAsMessageAttribute(peerId: sendAsPeerId))
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = self.presentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.sendSomething) {
if let sendAsPeerId = self.presentationInterfaceState.currentSendAsPeerId {
attributes.append(SendAsMessageAttribute(peerId: sendAsPeerId))
} else {
attributes.append(SendAsMessageAttribute(peerId: linkedMonoforumId))
}
}
}
if let sendAsPeerId = self.presentationInterfaceState.currentSendAsPeerId {

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)
}
@ -1447,11 +1447,25 @@ extension ChatControllerImpl {
)
}
var currentSendAsPeerId: PeerId?
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData {
if peer.isMonoForum {
if let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = peerView.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.sendSomething) {
currentSendAsPeerId = peer.linkedMonoforumId
} else {
currentSendAsPeerId = nil
}
} else {
currentSendAsPeerId = cachedData.sendAsPeerId
}
}
strongSelf.state.renderedPeer = renderedPeer
strongSelf.state.savedMessagesTopicPeer = savedMessagesPeer?.peer
strongSelf.state.hasSearchTags = hasSearchTags
strongSelf.state.hasSavedChats = hasSavedChats
strongSelf.state.hasScheduledMessages = hasScheduledMessages
strongSelf.state.currentSendAsPeerId = currentSendAsPeerId
} else {
let message = messageAndTopic.message

View File

@ -80,7 +80,7 @@ extension ChatControllerImpl {
if let value = channel.hasBannedPermission(.banSendText, ignoreDefault: canByPassRestrictions) {
banSendText = value
}
if channel.hasBannedPermission(.banSendPolls, ignoreDefault: canByPassRestrictions) != nil {
if channel.hasBannedPermission(.banSendPolls, ignoreDefault: canByPassRestrictions) != nil || channel.isMonoForum {
canSendPolls = false
}
} else if let group = peer as? TelegramGroup {
@ -764,7 +764,7 @@ extension ChatControllerImpl {
if let value = channel.hasBannedPermission(.banSendMedia) {
bannedSendMedia = value
}
if channel.hasBannedPermission(.banSendPolls) != nil {
if channel.hasBannedPermission(.banSendPolls) != nil || channel.isMonoForum {
canSendPolls = false
}
} else if let group = peer as? TelegramGroup {

View File

@ -169,7 +169,7 @@ extension ChatControllerImpl {
let _ = (self.context.engine.data.get(
EngineDataList(
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
peerIds.map(TelegramEngine.EngineData.Item.Peer.RenderedPeer.init)
)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in
@ -184,17 +184,17 @@ extension ChatControllerImpl {
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many
savedMessages = true
} else {
if peers.count == 1, let peer = peers.first {
if peers.count == 1, let peer = peers.first?.chatOrMonoforumMainPeer {
var peerName = peer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
peerName = peerName.replacingOccurrences(of: "**", with: "")
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
} else if peers.count == 2, let firstPeer = peers.first?.chatOrMonoforumMainPeer, let secondPeer = peers.last?.chatOrMonoforumMainPeer {
var firstPeerName = firstPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "")
var secondPeerName = secondPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "")
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string
} else if let peer = peers.first {
} else if let peer = peers.first?.chatOrMonoforumMainPeer {
var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
peerName = peerName.replacingOccurrences(of: "**", with: "")
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(peers.count - 1)").string

View File

@ -271,9 +271,13 @@ func sidePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState
context: context,
theme: chatPresentationInterfaceState.theme,
strings: chatPresentationInterfaceState.strings,
location: .side,
peerId: peerId,
isMonoforum: true,
topicId: chatPresentationInterfaceState.chatLocation.threadId,
controller: { [weak interfaceInteraction] in
return interfaceInteraction?.chatController()
},
togglePanel: { [weak interfaceInteraction] in
interfaceInteraction?.toggleChatSidebarMode()
},
@ -292,9 +296,13 @@ func sidePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState
context: context,
theme: chatPresentationInterfaceState.theme,
strings: chatPresentationInterfaceState.strings,
location: .side,
peerId: peerId,
isMonoforum: false,
topicId: chatPresentationInterfaceState.chatLocation.threadId,
controller: { [weak interfaceInteraction] in
return interfaceInteraction?.chatController()
},
togglePanel: { [weak interfaceInteraction] in
interfaceInteraction?.toggleChatSidebarMode()
},

View File

@ -14,173 +14,9 @@ import EmojiStatusComponent
import SwiftSignalKit
import BundleIconComponent
import AvatarNode
private final class CustomBadgeComponent: Component {
public let text: String
public let font: UIFont
public let background: UIColor
public let foreground: UIColor
public let insets: UIEdgeInsets
public init(
text: String,
font: UIFont,
background: UIColor,
foreground: UIColor,
insets: UIEdgeInsets
) {
self.text = text
self.font = font
self.background = background
self.foreground = foreground
self.insets = insets
}
public static func ==(lhs: CustomBadgeComponent, rhs: CustomBadgeComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.font != rhs.font {
return false
}
if lhs.background != rhs.background {
return false
}
if lhs.foreground != rhs.foreground {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
private struct TextLayout {
var size: CGSize
var opticalBounds: CGRect
init(size: CGSize, opticalBounds: CGRect) {
self.size = size
self.opticalBounds = opticalBounds
}
}
public final class View: UIView {
private let backgroundView: UIImageView
private let textContentsView: UIImageView
private var textLayout: TextLayout?
private var component: CustomBadgeComponent?
override public init(frame: CGRect) {
self.backgroundView = UIImageView()
self.textContentsView = UIImageView()
self.textContentsView.layer.anchorPoint = CGPoint()
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.addSubview(self.textContentsView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: CustomBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
if component.text != previousComponent?.text || component.font != previousComponent?.font {
let attributedText = NSAttributedString(string: component.text, attributes: [
NSAttributedString.Key.font: component.font,
NSAttributedString.Key.foregroundColor: UIColor.white
])
var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil)
boundingRect.size.width = ceil(boundingRect.size.width)
boundingRect.size.height = ceil(boundingRect.size.height)
if let context = DrawingContext(size: boundingRect.size, scale: 0.0, opaque: false, clear: true) {
context.withContext { c in
UIGraphicsPushContext(c)
defer {
UIGraphicsPopContext()
}
attributedText.draw(at: CGPoint())
}
var minFilledLineY = Int(context.scaledSize.height) - 1
var maxFilledLineY = 0
var minFilledLineX = Int(context.scaledSize.width) - 1
var maxFilledLineX = 0
for y in 0 ..< Int(context.scaledSize.height) {
let linePtr = context.bytes.advanced(by: max(0, y) * context.bytesPerRow).assumingMemoryBound(to: UInt32.self)
for x in 0 ..< Int(context.scaledSize.width) {
let pixelPtr = linePtr.advanced(by: x)
if pixelPtr.pointee != 0 {
minFilledLineY = min(y, minFilledLineY)
maxFilledLineY = max(y, maxFilledLineY)
minFilledLineX = min(x, minFilledLineX)
maxFilledLineX = max(x, maxFilledLineX)
}
}
}
var opticalBounds = CGRect()
if minFilledLineX <= maxFilledLineX && minFilledLineY <= maxFilledLineY {
opticalBounds.origin.x = CGFloat(minFilledLineX) / context.scale
opticalBounds.origin.y = CGFloat(minFilledLineY) / context.scale
opticalBounds.size.width = CGFloat(maxFilledLineX - minFilledLineX) / context.scale
opticalBounds.size.height = CGFloat(maxFilledLineY - minFilledLineY) / context.scale
}
self.textContentsView.image = context.generateImage()?.withRenderingMode(.alwaysTemplate)
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: opticalBounds)
} else {
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: CGRect(origin: CGPoint(), size: boundingRect.size))
}
}
let textSize = self.textLayout?.size ?? CGSize(width: 1.0, height: 1.0)
var size = CGSize(width: textSize.width + component.insets.left + component.insets.right, height: textSize.height + component.insets.top + component.insets.bottom)
size.width = max(size.width, size.height)
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) * 0.5), y: component.insets.top + UIScreenPixel), size: textSize)
/*if let textLayout = self.textLayout {
textFrame.origin.x = textLayout.opticalBounds.minX + floorToScreenPixels((backgroundFrame.width - textLayout.opticalBounds.width) * 0.5)
textFrame.origin.y = textLayout.opticalBounds.minY + floorToScreenPixels((backgroundFrame.height - textLayout.opticalBounds.height) * 0.5)
}*/
transition.setPosition(view: self.textContentsView, position: textFrame.origin)
self.textContentsView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
if size.height != self.backgroundView.image?.size.height {
self.backgroundView.image = generateStretchableFilledCircleImage(diameter: size.height, color: .white)?.withRenderingMode(.alwaysTemplate)
}
self.backgroundView.tintColor = component.background
self.textContentsView.tintColor = component.foreground
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
import TextBadgeComponent
import ChatSideTopicsPanel
import ComponentDisplayAdapters
final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, ChatControllerCustomNavigationPanelNode {
private struct Params: Equatable {
@ -213,31 +49,7 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
}
}
private final class Item: Equatable {
typealias Id = EngineChatList.Item.Id
let item: EngineChatList.Item
var id: Id {
return self.item.id
}
init(item: EngineChatList.Item) {
self.item = item
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs === rhs {
return true
}
if lhs.item != rhs.item {
return false
}
return true
}
}
private final class ItemView: UIView {
/*private final class ItemView: UIView {
private let context: AccountContext
private let action: () -> Void
@ -396,11 +208,11 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
badgeSize = badge.update(
transition: badgeTransition,
component: AnyComponent(CustomBadgeComponent(
text: "\(readCounters.count)",
component: AnyComponent(TextBadgeComponent(
text: countString(Int64(readCounters.count)),
font: Font.regular(12.0),
background: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
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: {},
@ -715,99 +527,26 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
return size
}
}
private final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
private enum ScrollId: Equatable {
case all
case topic(Int64)
}
private let context: AccountContext
private let isMonoforum: Bool
private let scrollView: ScrollView
private let scrollViewContainer: UIView
private let scrollViewMask: UIImageView
}*/
private var params: Params?
private var items: [Item] = []
private var itemViews: [Item.Id: ItemView] = [:]
private var allItemView: AllItemView?
private var tabItemView: TabItemView?
private let selectedLineView: UIImageView
private var itemsDisposable: Disposable?
private var appliedScrollToId: ScrollId?
private let context: AccountContext
private let peerId: EnginePeer.Id
private let isMonoforum: Bool
private let panel = ComponentView<ChatSidePanelEnvironment>()
init(context: AccountContext, peerId: EnginePeer.Id, isMonoforum: Bool) {
self.context = context
self.peerId = peerId
self.isMonoforum = isMonoforum
self.selectedLineView = UIImageView()
self.scrollView = ScrollView(frame: CGRect())
self.scrollViewMask = UIImageView(image: generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [
UIColor(white: 1.0, alpha: 0.0),
UIColor(white: 1.0, alpha: 1.0)
], locations: [0.0, 1.0], direction: .horizontal)?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 0))
self.scrollViewContainer = UIView()
super.init()
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = true
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = false
self.scrollView.scrollsToTop = false
self.scrollViewContainer.addSubview(self.scrollView)
self.scrollViewContainer.mask = self.scrollViewMask
self.view.addSubview(self.scrollViewContainer)
self.scrollView.addSubview(self.selectedLineView)
self.scrollView.disablesInteractiveTransitionGestureRecognizer = true
let threadListSignal: Signal<EngineChatList, NoError> = context.sharedContext.subscribeChatListData(context: context, location: isMonoforum ? .savedMessagesChats(peerId: peerId) : .forum(peerId: peerId))
self.itemsDisposable = (threadListSignal
|> deliverOnMainQueue).startStrict(next: { [weak self] chatList in
guard let self else {
return
}
self.items.removeAll()
for item in chatList.items.reversed() {
self.items.append(Item(item: item))
}
self.update(transition: .immediate)
})
}
deinit {
self.itemsDisposable?.dispose()
}
private func update(transition: ContainedViewLayoutTransition) {
@ -819,14 +558,6 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult {
let params = Params(width: width, leftInset: leftInset, rightInset: rightInset, interfaceState: interfaceState)
if self.params != params {
if self.params?.interfaceState.theme !== params.interfaceState.theme {
self.selectedLineView.image = generateImage(CGSize(width: 7.0, height: 4.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(params.interfaceState.theme.rootController.navigationBar.accentTextColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width)))
})?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 1)
}
self.params = params
self.update(params: params, transition: transition)
}
@ -841,14 +572,54 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
}
private func update(params: Params, transition: ContainedViewLayoutTransition) {
let hadItemViews = !self.itemViews.isEmpty
let panelHeight: CGFloat = 44.0
var transition = transition
if !hadItemViews {
transition = .immediate
let panelFrame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: panelHeight))
let _ = self.panel.update(
transition: ComponentTransition(transition),
component: AnyComponent(ChatSideTopicsPanel(
context: self.context,
theme: params.interfaceState.theme,
strings: params.interfaceState.strings,
location: .top,
peerId: self.peerId,
isMonoforum: self.isMonoforum,
topicId: params.interfaceState.chatLocation.threadId,
controller: { [weak self] in
return self?.interfaceInteraction?.chatController()
},
togglePanel: { [weak self] in
guard let self else {
return
}
self.interfaceInteraction?.toggleChatSidebarMode()
},
updateTopicId: { [weak self] topicId, direction in
guard let self else {
return
}
self.interfaceInteraction?.updateChatLocationThread(topicId, direction ? .right : .left)
}
)),
environment: {
ChatSidePanelEnvironment(insets: UIEdgeInsets(
top: 0.0,
left: params.leftInset,
bottom: 0.0,
right: params.rightInset
))
},
containerSize: panelFrame.size
)
if let panelView = self.panel.view {
if panelView.superview == nil {
panelView.disablesInteractiveTransitionGestureRecognizer = true
self.view.addSubview(panelView)
}
transition.updateFrame(view: panelView, frame: panelFrame)
}
let panelHeight: CGFloat = 44.0
/*
let containerInsets = UIEdgeInsets(top: 0.0, left: params.leftInset + 16.0, bottom: 0.0, right: params.rightInset + 16.0)
let itemSpacing: CGFloat = 24.0
@ -1068,17 +839,24 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
} else {
self.appliedScrollToId = scrollToId
}
}
}*/
}
public func updateGlobalOffset(globalOffset: CGFloat, transition: ComponentTransition) {
if let tabItemView = self.tabItemView {
transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(0.0, -globalOffset, 0.0))
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))
}
}
public func topicIndex(threadId: Int64?) -> Int? {
if let threadId {
if let panelView = self.panel.view as? ChatSideTopicsPanel.View {
return panelView.topicIndex(threadId: threadId)
} else {
return nil
}
/*if let threadId {
if let value = self.items.firstIndex(where: { item in
if item.id == .chatList(PeerId(threadId)) {
return true
@ -1094,6 +872,6 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
}
} else {
return 0
}
}*/
}
}

View File

@ -18,6 +18,7 @@ import PremiumUI
import ChatControllerInteraction
import ChatContextResultPeekContent
import ChatInputContextPanelNode
import BatchVideoRendering
private struct ChatContextResultStableId: Hashable {
let result: ChatContextResult
@ -48,8 +49,8 @@ private struct HorizontalListContextResultsChatInputContextPanelEntry: Comparabl
return lhs.index < rhs.index
}
func item(context: AccountContext, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> ListViewItem {
return HorizontalListContextResultsChatInputPanelItem(context: context, theme: self.theme, result: self.result, resultSelected: resultSelected)
func item(context: AccountContext, batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> ListViewItem {
return HorizontalListContextResultsChatInputPanelItem(context: context, theme: self.theme, result: self.result, batchVideoContext: batchVideoContext, resultSelected: resultSelected)
}
}
@ -71,12 +72,12 @@ private final class HorizontalListContextResultsOpaqueState {
}
}
private func preparedTransition(from fromEntries: [HorizontalListContextResultsChatInputContextPanelEntry], to toEntries: [HorizontalListContextResultsChatInputContextPanelEntry], hasMore: Bool, context: AccountContext, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> HorizontalListContextResultsChatInputContextPanelTransition {
private func preparedTransition(from fromEntries: [HorizontalListContextResultsChatInputContextPanelEntry], to toEntries: [HorizontalListContextResultsChatInputContextPanelEntry], hasMore: Bool, context: AccountContext, batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> HorizontalListContextResultsChatInputContextPanelTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, resultSelected: resultSelected), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, resultSelected: resultSelected), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, batchVideoContext: batchVideoContext, resultSelected: resultSelected), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, batchVideoContext: batchVideoContext, resultSelected: resultSelected), directionHint: nil) }
return HorizontalListContextResultsChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates, entryCount: toEntries.count, hasMore: hasMore)
}
@ -93,6 +94,8 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
private var enqueuedTransitions: [(HorizontalListContextResultsChatInputContextPanelTransition, Bool)] = []
private var hasValidLayout = false
private let batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>
override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) {
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
@ -108,6 +111,10 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
return strings.VoiceOver_ScrollStatus(row, count).string
}
self.batchVideoContext = QueueLocalObject(queue: .mainQueue(), generate: {
return BatchVideoRenderingContext(context: context)
})
super.init(context: context, theme: theme, strings: strings, fontSize: fontSize, chatPresentationContext: chatPresentationContext)
self.isOpaque = false
@ -136,7 +143,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
self.listView.view.disablesInteractiveTransitionGestureRecognizer = true
self.listView.view.disablesInteractiveKeyboardGestureRecognizer = true
self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? in
if let strongSelf = self {
let convertedPoint = strongSelf.listView.view.convert(point, from: strongSelf.view)
@ -183,7 +190,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
let controller = PremiumIntroScreen(context: strongSelf.context, source: .stickers)
strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(controller)
}))
} else {
} else if let batchVideoContext = strongSelf.batchVideoContext.unsafeGet() {
var menuItems: [ContextMenuItem] = []
if case let .internalReference(internalReference) = item.result, let file = internalReference.file, file.isAnimated {
menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Preview_SaveGif, icon: { theme in
@ -229,7 +236,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
f(.default)
let _ = item.resultSelected(item.result, itemNode, itemNode.bounds)
})))
selectedItemNodeAndContent = (itemNode.view, itemNode.bounds, ChatContextResultPeekContent(account: item.context.account, contextResult: item.result, menu: menuItems))
selectedItemNodeAndContent = (itemNode.view, itemNode.bounds, ChatContextResultPeekContent(context: item.context, contextResult: item.result, menu: menuItems, batchVideoContext: batchVideoContext))
}
}
}
@ -312,7 +319,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
}
let firstTime = self.currentEntries == nil
let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, hasMore: results.nextOffset != nil, context: self.context, resultSelected: { [weak self] result, node, rect in
let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, hasMore: results.nextOffset != nil, context: self.context, batchVideoContext: self.batchVideoContext, resultSelected: { [weak self] result, node, rect in
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
return interfaceInteraction.sendContextResult(results, result, node, rect)
} else {

View File

@ -15,20 +15,23 @@ import TelegramPresentationData
import AccountContext
import ShimmerEffect
import SoftwareVideo
import MultiplexedVideoNode
import BatchVideoRendering
import GifVideoLayer
final class HorizontalListContextResultsChatInputPanelItem: ListViewItem {
let context: AccountContext
let theme: PresentationTheme
let result: ChatContextResult
let batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>
let resultSelected: (ChatContextResult, ASDisplayNode, CGRect) -> Bool
let selectable: Bool = true
public init(context: AccountContext, theme: PresentationTheme, result: ChatContextResult, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) {
public init(context: AccountContext, theme: PresentationTheme, result: ChatContextResult, batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) {
self.context = context
self.theme = theme
self.result = result
self.batchVideoContext = batchVideoContext
self.resultSelected = resultSelected
}
@ -90,7 +93,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private var placeholderNode: StickerShimmerEffectNode?
private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
private var videoLayer: GifVideoLayer?
private var currentImageResource: TelegramMediaResource?
private var currentVideoFile: TelegramMediaFile?
private var currentAnimatedStickerFile: TelegramMediaFile?
@ -103,58 +106,17 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
override var visibility: ListViewItemNodeVisibility {
didSet {
switch visibility {
case .visible:
self.ticking = true
default:
self.ticking = false
switch self.visibility {
case .visible:
self.videoLayer?.shouldBeAnimating = true
case .none:
self.videoLayer?.shouldBeAnimating = false
}
}
}
private let timebase: CMTimebase
private var displayLink: CADisplayLink?
private var ticking: Bool = false {
didSet {
if self.ticking != oldValue {
if self.ticking {
class DisplayLinkProxy: NSObject {
weak var target: HorizontalListContextResultsChatInputPanelItemNode?
init(target: HorizontalListContextResultsChatInputPanelItemNode) {
self.target = target
}
@objc func displayLinkEvent() {
self.target?.displayLinkEvent()
}
}
let displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
self.displayLink = displayLink
displayLink.add(to: RunLoop.main, forMode: .common)
if #available(iOS 10.0, *) {
displayLink.preferredFramesPerSecond = 25
} else {
displayLink.frameInterval = 2
}
displayLink.isPaused = false
CMTimebaseSetRate(self.timebase, rate: 1.0)
} else if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.isPaused = true
displayLink.invalidate()
CMTimebaseSetRate(self.timebase, rate: 0.0)
}
}
}
}
private func displayLinkEvent() {
let timestamp = CMTimebaseGetTime(self.timebase).seconds
self.videoLayer?.1.tick(timestamp: timestamp)
}
init() {
self.imageNodeBackground = ASDisplayNode()
self.imageNodeBackground.isLayerBacked = true
@ -197,10 +159,6 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
}
deinit {
if let displayLink = self.displayLink {
displayLink.isPaused = true
displayLink.invalidate()
}
self.statusDisposable.dispose()
self.fetchDisposable.dispose()
}
@ -384,30 +342,25 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
}
if updatedVideoFile {
if let (thumbnailLayer, _, layer) = strongSelf.videoLayer {
if let videoLayer = strongSelf.videoLayer {
strongSelf.videoLayer = nil
thumbnailLayer.removeFromSupernode()
layer.layer.removeFromSuperlayer()
videoLayer.removeFromSuperlayer()
}
if let videoFile = videoFile {
let thumbnailLayer = SoftwareVideoThumbnailNode(account: item.context.account, fileReference: .standalone(media: videoFile), synchronousLoad: synchronousLoads)
thumbnailLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
strongSelf.addSubnode(thumbnailLayer)
let layerHolder = takeSampleBufferLayer()
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
strongSelf.layer.addSublayer(layerHolder.layer)
if let videoFile, let batchVideoContext = item.batchVideoContext.unsafeGet() {
let videoLayer = GifVideoLayer(
context: item.context,
batchVideoContext: batchVideoContext,
userLocation: .other,
file: .standalone(media: videoFile),
synchronousLoad: synchronousLoads
)
videoLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
videoLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
strongSelf.layer.addSublayer(videoLayer)
let manager = SoftwareVideoLayerFrameManager(account: item.context.account, userLocation: .other, userContentType: .other, fileReference: .standalone(media: videoFile), layerHolder: layerHolder)
strongSelf.videoLayer = (thumbnailLayer, manager, layerHolder)
thumbnailLayer.ready = { [weak thumbnailLayer, weak manager] in
if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager {
if strongSelf.videoLayer?.0 === thumbnailLayer && strongSelf.videoLayer?.1 === manager {
manager.start()
}
}
}
strongSelf.videoLayer = videoLayer
videoLayer.shouldBeAnimating = strongSelf.visibility != .none
}
}
@ -477,11 +430,9 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
strongSelf.statusNode.transitionToState(.none, completion: { })
}
if let (thumbnailLayer, _, layer) = strongSelf.videoLayer {
thumbnailLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
thumbnailLayer.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset)
layer.layer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
layer.layer.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset)
if let videoLayer = strongSelf.videoLayer {
videoLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
videoLayer.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset)
}
if let animationNode = strongSelf.animationNode {

View File

@ -85,6 +85,7 @@ import GiftStoreScreen
import SendInviteLinkScreen
import PostSuggestionsSettingsScreen
import ForumSettingsScreen
import ForumCreateTopicScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -2609,6 +2610,25 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
}
public func makeEditForumTopicScreen(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, threadInfo: EngineMessageHistoryThread.Info, isHidden: Bool) -> ViewController {
let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .edit(threadId: threadId, threadInfo: threadInfo, isHidden: isHidden))
controller.navigationPresentation = .modal
controller.completion = { [weak controller] title, fileId, _, isHidden in
let _ = (context.engine.peers.editForumChannelTopic(id: peerId, threadId: threadId, title: title, iconFileId: fileId)
|> deliverOnMainQueue).startStandalone(completed: {
controller?.dismiss()
})
if let isHidden {
let _ = (context.engine.peers.setForumChannelTopicHidden(id: peerId, threadId: threadId, isHidden: isHidden)
|> deliverOnMainQueue).startStandalone(completed: {
controller?.dismiss()
})
}
}
return controller
}
private func mapIntroSource(source: PremiumIntroSource) -> PremiumSource {
let mappedSource: PremiumSource
switch source {