mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
ecaae4c29a
@ -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.";
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -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)
|
||||
})))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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": [],
|
||||
|
||||
23
submodules/TelegramUI/Components/AsyncListComponent/BUILD
Normal file
23
submodules/TelegramUI/Components/AsyncListComponent/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
],
|
||||
)
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}))
|
||||
}
|
||||
|
||||
@ -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
|
||||
)),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
19
submodules/TelegramUI/Components/TextBadgeComponent/BUILD
Normal file
19
submodules/TelegramUI/Components/TextBadgeComponent/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
},
|
||||
|
||||
@ -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
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user