mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
Monoforums
This commit is contained in:
parent
a464698638
commit
40e26fc25c
@ -1118,6 +1118,7 @@ public protocol SharedAccountContext: AnyObject {
|
|||||||
func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController
|
func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController
|
||||||
func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal<CollectibleItemInfoScreenInitialData?, NoError>
|
func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal<CollectibleItemInfoScreenInitialData?, NoError>
|
||||||
func makeBotSettingsScreen(context: AccountContext, peerId: EnginePeer.Id?) -> ViewController
|
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 navigateToChatController(_ params: NavigateToChatControllerParams)
|
||||||
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
|
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
|
||||||
|
@ -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 presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||||
let strings = presentationData.strings
|
let strings = presentationData.strings
|
||||||
|
|
||||||
@ -603,8 +603,17 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
|||||||
|
|
||||||
if let isClosed = isClosed, isClosed && threadId != 1 {
|
if let isClosed = isClosed, isClosed && threadId != 1 {
|
||||||
} else {
|
} else {
|
||||||
if let isPinned = isPinned, channel.hasPermission(.manageTopics) {
|
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: { _, f in
|
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)
|
f(.default)
|
||||||
|
|
||||||
let _ = (context.engine.peers.toggleForumChannelTopicPinned(id: peerId, threadId: threadId)
|
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
|
var isMuted = false
|
||||||
switch threadData.notificationSettings.muteState {
|
switch threadData.notificationSettings.muteState {
|
||||||
case .muted:
|
case .muted:
|
||||||
@ -863,14 +902,6 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
|||||||
})))
|
})))
|
||||||
|
|
||||||
if threadId != 1 {
|
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 {
|
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
|
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)
|
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
|
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)
|
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(.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)
|
f(.default)
|
||||||
chatListController?.selectPeerThread(peerId: peerId, threadId: threadId)
|
chatListController?.selectPeerThread(peerId: peerId, threadId: threadId)
|
||||||
})))
|
})))
|
||||||
|
@ -16,8 +16,8 @@ public extension UIView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension CALayer {
|
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) {
|
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 timingFunction: String
|
||||||
let mediaTimingFunction: CAMediaTimingFunction?
|
let mediaTimingFunction: CAMediaTimingFunction?
|
||||||
switch curve {
|
switch curve {
|
||||||
@ -39,7 +39,8 @@ private extension CALayer {
|
|||||||
mediaTimingFunction: mediaTimingFunction,
|
mediaTimingFunction: mediaTimingFunction,
|
||||||
removeOnCompletion: removeOnCompletion,
|
removeOnCompletion: removeOnCompletion,
|
||||||
additive: additive,
|
additive: additive,
|
||||||
completion: completion
|
completion: completion,
|
||||||
|
key: key
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -263,6 +263,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
|
|||||||
public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)?
|
public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)?
|
||||||
public final var addContentOffset: ((CGFloat, ListViewItemNode?) -> Void)?
|
public final var addContentOffset: ((CGFloat, ListViewItemNode?) -> Void)?
|
||||||
public final var shouldStopScrolling: ((CGFloat) -> Bool)?
|
public final var shouldStopScrolling: ((CGFloat) -> Bool)?
|
||||||
|
public final var onContentsUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||||
|
|
||||||
public final var updateScrollingIndicator: ((ScrollingIndicatorState?, 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 reorderScrollUpdateTimestamp: Double?
|
||||||
private var reorderLastTimestamp: Double?
|
private var reorderLastTimestamp: Double?
|
||||||
public var reorderedItemHasShadow = true
|
public var reorderedItemHasShadow = true
|
||||||
|
public var reorderingRequiresLongPress = false
|
||||||
|
|
||||||
private let waitingForNodesDisposable = MetaDisposable()
|
private let waitingForNodesDisposable = MetaDisposable()
|
||||||
|
|
||||||
@ -518,7 +520,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
|
|||||||
let itemNodeFrame = itemNode.frame
|
let itemNodeFrame = itemNode.frame
|
||||||
let itemNodeBounds = itemNode.bounds
|
let itemNodeBounds = itemNode.bounds
|
||||||
if itemNode.isReorderable(at: point.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY)) {
|
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)
|
return (true, requiresLongPress, itemNode)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@ -1101,6 +1103,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
|
|||||||
self.updateVisibleContentOffset()
|
self.updateVisibleContentOffset()
|
||||||
self.updateVisibleItemRange()
|
self.updateVisibleItemRange()
|
||||||
self.updateItemNodesVisibilities(onlyPositive: false)
|
self.updateItemNodesVisibilities(onlyPositive: false)
|
||||||
|
self.onContentsUpdated?(.immediate)
|
||||||
|
|
||||||
//CATransaction.commit()
|
//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.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 {
|
if let offset = offset, !offset.isZero {
|
||||||
//self.didScrollWithOffset?(-offset, headerNodesTransition.0, nil)
|
//self.didScrollWithOffset?(-offset, headerNodesTransition.0, nil)
|
||||||
@ -3733,6 +3737,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
|
|||||||
} else {
|
} else {
|
||||||
self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, synchronousLoad: synchronousLoads, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty, animateFullTransition: animateFullTransition)
|
self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, synchronousLoad: synchronousLoads, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty, animateFullTransition: animateFullTransition)
|
||||||
self.updateItemNodesVisibilities(onlyPositive: deferredUpdateVisible)
|
self.updateItemNodesVisibilities(onlyPositive: deferredUpdateVisible)
|
||||||
|
self.onContentsUpdated?(headerNodesTransition.0)
|
||||||
|
|
||||||
applyHeaderNodesFullTransition()
|
applyHeaderNodesFullTransition()
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ public func listViewAnimationCurveFromAnimationOptions(animationOptions: UIView.
|
|||||||
|
|
||||||
public final class ListViewAnimation {
|
public final class ListViewAnimation {
|
||||||
let from: Interpolatable
|
let from: Interpolatable
|
||||||
let to: Interpolatable
|
public let to: Interpolatable
|
||||||
let duration: Double
|
let duration: Double
|
||||||
let startTime: Double
|
let startTime: Double
|
||||||
let invertOffsetDirection: Bool
|
let invertOffsetDirection: Bool
|
||||||
|
@ -8923,13 +8923,14 @@ public extension Api.functions.messages {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
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()
|
let buffer = Buffer()
|
||||||
buffer.appendInt32(-299714136)
|
buffer.appendInt32(103667527)
|
||||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
peer.serialize(buffer, true)
|
peer.serialize(buffer, true)
|
||||||
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)}
|
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)
|
let reader = BufferReader(buffer)
|
||||||
var result: Api.messages.AffectedHistory?
|
var result: Api.messages.AffectedHistory?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
|
@ -2345,7 +2345,8 @@ public func messagesForNotification(transaction: Transaction, id: MessageId, alw
|
|||||||
|
|
||||||
var notificationSettingsStack: [TelegramPeerNotificationSettings] = []
|
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)
|
notificationSettingsStack.append(threadData.notificationSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,12 +110,18 @@ func _internal_requestUpdatePinnedMessage(account: Account, peerId: PeerId, upda
|
|||||||
}
|
}
|
||||||
|
|
||||||
func _internal_requestUnpinAllMessages(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<Never, UpdatePinnedMessageError> {
|
func _internal_requestUnpinAllMessages(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<Never, UpdatePinnedMessageError> {
|
||||||
return account.postbox.transaction { transaction -> (Peer?, CachedPeerData?) in
|
return account.postbox.transaction { transaction -> (Peer?, Peer?, CachedPeerData?) in
|
||||||
return (transaction.getPeer(peerId), transaction.getPeerCachedData(peerId: peerId))
|
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
|
|> 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 {
|
guard let peer = peer, let inputPeer = apiInputPeer(peer) else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
}
|
}
|
||||||
@ -148,10 +154,20 @@ func _internal_requestUnpinAllMessages(account: Account, peerId: PeerId, threadI
|
|||||||
}
|
}
|
||||||
|
|
||||||
var flags: Int32 = 0
|
var flags: Int32 = 0
|
||||||
if threadId != nil {
|
var topMsgId: Int32?
|
||||||
flags |= (1 << 0)
|
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
|
|> mapError { error -> InternalError in
|
||||||
return .error(error.errorDescription)
|
return .error(error.errorDescription)
|
||||||
}
|
}
|
||||||
|
@ -479,6 +479,8 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen",
|
"//submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen",
|
||||||
"//submodules/TelegramUI/Components/ForumSettingsScreen",
|
"//submodules/TelegramUI/Components/ForumSettingsScreen",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel",
|
"//submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel",
|
||||||
|
"//submodules/TelegramUI/Components/GifVideoLayer",
|
||||||
|
"//submodules/TelegramUI/Components/BatchVideoRendering",
|
||||||
] + select({
|
] + select({
|
||||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||||
"//build-system:ios_sim_arm64": [],
|
"//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)
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,8 @@ swift_library(
|
|||||||
"//submodules/AppBundle",
|
"//submodules/AppBundle",
|
||||||
"//submodules/ContextUI",
|
"//submodules/ContextUI",
|
||||||
"//submodules/SoftwareVideo",
|
"//submodules/SoftwareVideo",
|
||||||
"//submodules/TelegramUI/Components/MultiplexedVideoNode",
|
"//submodules/TelegramUI/Components/BatchVideoRendering",
|
||||||
|
"//submodules/TelegramUI/Components/GifVideoLayer",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -10,17 +10,21 @@ import PhotoResources
|
|||||||
import AppBundle
|
import AppBundle
|
||||||
import ContextUI
|
import ContextUI
|
||||||
import SoftwareVideo
|
import SoftwareVideo
|
||||||
import MultiplexedVideoNode
|
import BatchVideoRendering
|
||||||
|
import GifVideoLayer
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
public final class ChatContextResultPeekContent: PeekControllerContent {
|
public final class ChatContextResultPeekContent: PeekControllerContent {
|
||||||
public let account: Account
|
public let context: AccountContext
|
||||||
public let contextResult: ChatContextResult
|
public let contextResult: ChatContextResult
|
||||||
public let menu: [ContextMenuItem]
|
public let menu: [ContextMenuItem]
|
||||||
|
public let batchVideoContext: BatchVideoRenderingContext
|
||||||
|
|
||||||
public init(account: Account, contextResult: ChatContextResult, menu: [ContextMenuItem]) {
|
public init(context: AccountContext, contextResult: ChatContextResult, menu: [ContextMenuItem], batchVideoContext: BatchVideoRenderingContext) {
|
||||||
self.account = account
|
self.context = context
|
||||||
self.contextResult = contextResult
|
self.contextResult = contextResult
|
||||||
self.menu = menu
|
self.menu = menu
|
||||||
|
self.batchVideoContext = batchVideoContext
|
||||||
}
|
}
|
||||||
|
|
||||||
public func presentation() -> PeekControllerContentPresentation {
|
public func presentation() -> PeekControllerContentPresentation {
|
||||||
@ -36,7 +40,7 @@ public final class ChatContextResultPeekContent: PeekControllerContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func node() -> PeekControllerContentNode & ASDisplayNode {
|
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? {
|
public func topAccessoryNode() -> ASDisplayNode? {
|
||||||
@ -62,62 +66,29 @@ public final class ChatContextResultPeekContent: PeekControllerContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerContentNode {
|
private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerContentNode {
|
||||||
private let account: Account
|
private let context: AccountContext
|
||||||
private let contextResult: ChatContextResult
|
private let contextResult: ChatContextResult
|
||||||
|
private let batchVideoContext: BatchVideoRenderingContext
|
||||||
|
|
||||||
private let imageNodeBackground: ASDisplayNode
|
private let imageNodeBackground: ASDisplayNode
|
||||||
private let imageNode: TransformImageNode
|
private let imageNode: TransformImageNode
|
||||||
private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
|
private var videoLayer: GifVideoLayer?
|
||||||
|
|
||||||
private var currentImageResource: TelegramMediaResource?
|
private var currentImageResource: TelegramMediaResource?
|
||||||
private var currentVideoFile: TelegramMediaFile?
|
private var currentVideoFile: TelegramMediaFile?
|
||||||
|
|
||||||
private let timebase: CMTimebase
|
|
||||||
|
|
||||||
private var displayLink: CADisplayLink?
|
|
||||||
private var ticking: Bool = false {
|
private var ticking: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
if self.ticking != oldValue {
|
if self.ticking != oldValue {
|
||||||
if self.ticking {
|
self.videoLayer?.shouldBeAnimating = 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func displayLinkEvent() {
|
init(context: AccountContext, contextResult: ChatContextResult, batchVideoContext: BatchVideoRenderingContext) {
|
||||||
let timestamp = CMTimebaseGetTime(self.timebase).seconds
|
self.context = context
|
||||||
self.videoLayer?.1.tick(timestamp: timestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(account: Account, contextResult: ChatContextResult) {
|
|
||||||
self.account = account
|
|
||||||
self.contextResult = contextResult
|
self.contextResult = contextResult
|
||||||
|
self.batchVideoContext = batchVideoContext
|
||||||
|
|
||||||
self.imageNodeBackground = ASDisplayNode()
|
self.imageNodeBackground = ASDisplayNode()
|
||||||
self.imageNodeBackground.isLayerBacked = true
|
self.imageNodeBackground.isLayerBacked = true
|
||||||
@ -128,11 +99,6 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
|
|||||||
self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
|
self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
|
||||||
self.imageNode.displaysAsynchronously = false
|
self.imageNode.displaysAsynchronously = false
|
||||||
|
|
||||||
var timebase: CMTimebase?
|
|
||||||
CMTimebaseCreateWithSourceClock(allocator: nil, sourceClock: CMClockGetHostTimeClock(), timebaseOut: &timebase)
|
|
||||||
CMTimebaseSetRate(timebase!, rate: 0.0)
|
|
||||||
self.timebase = timebase!
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.imageNodeBackground)
|
self.addSubnode(self.imageNodeBackground)
|
||||||
@ -142,10 +108,6 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
if let displayLink = self.displayLink {
|
|
||||||
displayLink.isPaused = true
|
|
||||||
displayLink.invalidate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ready() -> Signal<Bool, NoError> {
|
func ready() -> Signal<Bool, NoError> {
|
||||||
@ -236,7 +198,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
|
|||||||
if let imageResource = imageResource {
|
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 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: [])
|
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 {
|
} else {
|
||||||
updateImageSignal = .complete()
|
updateImageSignal = .complete()
|
||||||
}
|
}
|
||||||
@ -256,33 +218,26 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
if updatedVideoFile {
|
if updatedVideoFile {
|
||||||
if let (thumbnailLayer, _, layer) = self.videoLayer {
|
if let videoLayer = self.videoLayer {
|
||||||
self.videoLayer = nil
|
self.videoLayer = nil
|
||||||
thumbnailLayer.removeFromSupernode()
|
videoLayer.removeFromSuperlayer()
|
||||||
layer.layer.removeFromSuperlayer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let videoFileReference = videoFileReference {
|
if let videoFileReference {
|
||||||
let thumbnailLayer = SoftwareVideoThumbnailNode(account: self.account, fileReference: videoFileReference, synchronousLoad: false)
|
let videoLayer = GifVideoLayer(
|
||||||
self.addSubnode(thumbnailLayer)
|
context: self.context,
|
||||||
let layerHolder = takeSampleBufferLayer()
|
batchVideoContext: self.batchVideoContext,
|
||||||
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
userLocation: .other,
|
||||||
self.layer.addSublayer(layerHolder.layer)
|
file: videoFileReference,
|
||||||
let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .other, fileReference: videoFileReference, layerHolder: layerHolder)
|
synchronousLoad: false
|
||||||
self.videoLayer = (thumbnailLayer, manager, layerHolder)
|
)
|
||||||
thumbnailLayer.ready = { [weak self, weak thumbnailLayer, weak manager] in
|
self.videoLayer = videoLayer
|
||||||
if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager {
|
self.layer.addSublayer(videoLayer)
|
||||||
if strongSelf.videoLayer?.0 === thumbnailLayer && strongSelf.videoLayer?.1 === manager {
|
|
||||||
manager.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (thumbnailLayer, _, layer) = self.videoLayer {
|
if let videoLayer = self.videoLayer {
|
||||||
thumbnailLayer.frame = CGRect(origin: CGPoint(), size: croppedImageDimensions)
|
videoLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
|
||||||
layer.layer.frame = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.ticking {
|
if !self.ticking {
|
||||||
|
@ -24,6 +24,13 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||||
"//submodules/Components/BundleIconComponent",
|
"//submodules/Components/BundleIconComponent",
|
||||||
"//submodules/AvatarNode",
|
"//submodules/AvatarNode",
|
||||||
|
"//submodules/ChatListUI",
|
||||||
|
"//submodules/ContextUI",
|
||||||
|
"//submodules/TelegramUI/Components/AsyncListComponent",
|
||||||
|
"//submodules/TelegramUI/Components/TextBadgeComponent",
|
||||||
|
"//submodules/TelegramUI/Components/MaskedContainerComponent",
|
||||||
|
"//submodules/AppBundle",
|
||||||
|
"//submodules/PresentationDataUtils",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 containerNode: ASDisplayNode
|
||||||
private let emojiContainerNode: ASDisplayNode
|
private let emojiContainerNode: ASDisplayNode
|
||||||
private let imageNode: TransformImageNode
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -271,9 +271,13 @@ func sidePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState
|
|||||||
context: context,
|
context: context,
|
||||||
theme: chatPresentationInterfaceState.theme,
|
theme: chatPresentationInterfaceState.theme,
|
||||||
strings: chatPresentationInterfaceState.strings,
|
strings: chatPresentationInterfaceState.strings,
|
||||||
|
location: .side,
|
||||||
peerId: peerId,
|
peerId: peerId,
|
||||||
isMonoforum: true,
|
isMonoforum: true,
|
||||||
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
||||||
|
controller: { [weak interfaceInteraction] in
|
||||||
|
return interfaceInteraction?.chatController()
|
||||||
|
},
|
||||||
togglePanel: { [weak interfaceInteraction] in
|
togglePanel: { [weak interfaceInteraction] in
|
||||||
interfaceInteraction?.toggleChatSidebarMode()
|
interfaceInteraction?.toggleChatSidebarMode()
|
||||||
},
|
},
|
||||||
@ -292,9 +296,13 @@ func sidePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState
|
|||||||
context: context,
|
context: context,
|
||||||
theme: chatPresentationInterfaceState.theme,
|
theme: chatPresentationInterfaceState.theme,
|
||||||
strings: chatPresentationInterfaceState.strings,
|
strings: chatPresentationInterfaceState.strings,
|
||||||
|
location: .side,
|
||||||
peerId: peerId,
|
peerId: peerId,
|
||||||
isMonoforum: false,
|
isMonoforum: false,
|
||||||
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
||||||
|
controller: { [weak interfaceInteraction] in
|
||||||
|
return interfaceInteraction?.chatController()
|
||||||
|
},
|
||||||
togglePanel: { [weak interfaceInteraction] in
|
togglePanel: { [weak interfaceInteraction] in
|
||||||
interfaceInteraction?.toggleChatSidebarMode()
|
interfaceInteraction?.toggleChatSidebarMode()
|
||||||
},
|
},
|
||||||
|
@ -14,173 +14,9 @@ import EmojiStatusComponent
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import BundleIconComponent
|
import BundleIconComponent
|
||||||
import AvatarNode
|
import AvatarNode
|
||||||
|
import TextBadgeComponent
|
||||||
private final class CustomBadgeComponent: Component {
|
import ChatSideTopicsPanel
|
||||||
public let text: String
|
import ComponentDisplayAdapters
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, ChatControllerCustomNavigationPanelNode {
|
final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, ChatControllerCustomNavigationPanelNode {
|
||||||
private struct Params: Equatable {
|
private struct Params: Equatable {
|
||||||
@ -213,31 +49,7 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class Item: Equatable {
|
/*private final class ItemView: UIView {
|
||||||
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 let context: AccountContext
|
private let context: AccountContext
|
||||||
private let action: () -> Void
|
private let action: () -> Void
|
||||||
|
|
||||||
@ -396,11 +208,11 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
|||||||
|
|
||||||
badgeSize = badge.update(
|
badgeSize = badge.update(
|
||||||
transition: badgeTransition,
|
transition: badgeTransition,
|
||||||
component: AnyComponent(CustomBadgeComponent(
|
component: AnyComponent(TextBadgeComponent(
|
||||||
text: "\(readCounters.count)",
|
text: countString(Int64(readCounters.count)),
|
||||||
font: Font.regular(12.0),
|
font: Font.regular(12.0),
|
||||||
background: theme.list.itemCheckColors.fillColor,
|
background: item.item.isMuted ? theme.chatList.unreadBadgeInactiveBackgroundColor : theme.chatList.unreadBadgeActiveBackgroundColor,
|
||||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
foreground: item.item.isMuted ? theme.chatList.unreadBadgeInactiveTextColor : theme.chatList.unreadBadgeActiveTextColor,
|
||||||
insets: UIEdgeInsets(top: 1.0, left: 5.0, bottom: 2.0, right: 5.0)
|
insets: UIEdgeInsets(top: 1.0, left: 5.0, bottom: 2.0, right: 5.0)
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
@ -715,99 +527,26 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
|||||||
|
|
||||||
return size
|
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 params: Params?
|
||||||
|
|
||||||
private var items: [Item] = []
|
private let context: AccountContext
|
||||||
private var itemViews: [Item.Id: ItemView] = [:]
|
private let peerId: EnginePeer.Id
|
||||||
private var allItemView: AllItemView?
|
private let isMonoforum: Bool
|
||||||
private var tabItemView: TabItemView?
|
private let panel = ComponentView<ChatSidePanelEnvironment>()
|
||||||
private let selectedLineView: UIImageView
|
|
||||||
|
|
||||||
private var itemsDisposable: Disposable?
|
|
||||||
|
|
||||||
private var appliedScrollToId: ScrollId?
|
|
||||||
|
|
||||||
init(context: AccountContext, peerId: EnginePeer.Id, isMonoforum: Bool) {
|
init(context: AccountContext, peerId: EnginePeer.Id, isMonoforum: Bool) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.peerId = peerId
|
||||||
self.isMonoforum = isMonoforum
|
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()
|
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 {
|
deinit {
|
||||||
self.itemsDisposable?.dispose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func update(transition: ContainedViewLayoutTransition) {
|
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 {
|
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)
|
let params = Params(width: width, leftInset: leftInset, rightInset: rightInset, interfaceState: interfaceState)
|
||||||
if self.params != params {
|
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.params = params
|
||||||
self.update(params: params, transition: transition)
|
self.update(params: params, transition: transition)
|
||||||
}
|
}
|
||||||
@ -841,14 +572,54 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func update(params: Params, transition: ContainedViewLayoutTransition) {
|
private func update(params: Params, transition: ContainedViewLayoutTransition) {
|
||||||
let hadItemViews = !self.itemViews.isEmpty
|
let panelHeight: CGFloat = 44.0
|
||||||
|
|
||||||
var transition = transition
|
let panelFrame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: panelHeight))
|
||||||
if !hadItemViews {
|
let _ = self.panel.update(
|
||||||
transition = .immediate
|
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 containerInsets = UIEdgeInsets(top: 0.0, left: params.leftInset + 16.0, bottom: 0.0, right: params.rightInset + 16.0)
|
||||||
let itemSpacing: CGFloat = 24.0
|
let itemSpacing: CGFloat = 24.0
|
||||||
@ -1068,17 +839,24 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
|||||||
} else {
|
} else {
|
||||||
self.appliedScrollToId = scrollToId
|
self.appliedScrollToId = scrollToId
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateGlobalOffset(globalOffset: CGFloat, transition: ComponentTransition) {
|
public func updateGlobalOffset(globalOffset: CGFloat, transition: ComponentTransition) {
|
||||||
if let tabItemView = self.tabItemView {
|
if let panelView = self.panel.view as? ChatSideTopicsPanel.View {
|
||||||
transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(0.0, -globalOffset, 0.0))
|
panelView.updateGlobalOffset(globalOffset: globalOffset, transition: transition)
|
||||||
|
//transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(0.0, -globalOffset, 0.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func topicIndex(threadId: Int64?) -> Int? {
|
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 let value = self.items.firstIndex(where: { item in
|
||||||
if item.id == .chatList(PeerId(threadId)) {
|
if item.id == .chatList(PeerId(threadId)) {
|
||||||
return true
|
return true
|
||||||
@ -1094,6 +872,6 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import PremiumUI
|
|||||||
import ChatControllerInteraction
|
import ChatControllerInteraction
|
||||||
import ChatContextResultPeekContent
|
import ChatContextResultPeekContent
|
||||||
import ChatInputContextPanelNode
|
import ChatInputContextPanelNode
|
||||||
|
import BatchVideoRendering
|
||||||
|
|
||||||
private struct ChatContextResultStableId: Hashable {
|
private struct ChatContextResultStableId: Hashable {
|
||||||
let result: ChatContextResult
|
let result: ChatContextResult
|
||||||
@ -48,8 +49,8 @@ private struct HorizontalListContextResultsChatInputContextPanelEntry: Comparabl
|
|||||||
return lhs.index < rhs.index
|
return lhs.index < rhs.index
|
||||||
}
|
}
|
||||||
|
|
||||||
func item(context: AccountContext, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> ListViewItem {
|
func item(context: AccountContext, batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> ListViewItem {
|
||||||
return HorizontalListContextResultsChatInputPanelItem(context: context, theme: self.theme, result: self.result, resultSelected: resultSelected)
|
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 (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||||
|
|
||||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
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 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, 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)
|
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 enqueuedTransitions: [(HorizontalListContextResultsChatInputContextPanelTransition, Bool)] = []
|
||||||
private var hasValidLayout = false
|
private var hasValidLayout = false
|
||||||
|
|
||||||
|
private let batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>
|
||||||
|
|
||||||
override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) {
|
override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) {
|
||||||
self.separatorNode = ASDisplayNode()
|
self.separatorNode = ASDisplayNode()
|
||||||
self.separatorNode.isLayerBacked = true
|
self.separatorNode.isLayerBacked = true
|
||||||
@ -108,6 +111,10 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
|
|||||||
return strings.VoiceOver_ScrollStatus(row, count).string
|
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)
|
super.init(context: context, theme: theme, strings: strings, fontSize: fontSize, chatPresentationContext: chatPresentationContext)
|
||||||
|
|
||||||
self.isOpaque = false
|
self.isOpaque = false
|
||||||
@ -136,7 +143,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
|
|||||||
|
|
||||||
self.listView.view.disablesInteractiveTransitionGestureRecognizer = true
|
self.listView.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||||
self.listView.view.disablesInteractiveKeyboardGestureRecognizer = 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 {
|
if let strongSelf = self {
|
||||||
let convertedPoint = strongSelf.listView.view.convert(point, from: strongSelf.view)
|
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)
|
let controller = PremiumIntroScreen(context: strongSelf.context, source: .stickers)
|
||||||
strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(controller)
|
strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(controller)
|
||||||
}))
|
}))
|
||||||
} else {
|
} else if let batchVideoContext = strongSelf.batchVideoContext.unsafeGet() {
|
||||||
var menuItems: [ContextMenuItem] = []
|
var menuItems: [ContextMenuItem] = []
|
||||||
if case let .internalReference(internalReference) = item.result, let file = internalReference.file, file.isAnimated {
|
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
|
menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Preview_SaveGif, icon: { theme in
|
||||||
@ -229,7 +236,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
|
|||||||
f(.default)
|
f(.default)
|
||||||
let _ = item.resultSelected(item.result, itemNode, itemNode.bounds)
|
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 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 {
|
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
||||||
return interfaceInteraction.sendContextResult(results, result, node, rect)
|
return interfaceInteraction.sendContextResult(results, result, node, rect)
|
||||||
} else {
|
} else {
|
||||||
|
@ -15,20 +15,23 @@ import TelegramPresentationData
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import ShimmerEffect
|
import ShimmerEffect
|
||||||
import SoftwareVideo
|
import SoftwareVideo
|
||||||
import MultiplexedVideoNode
|
import BatchVideoRendering
|
||||||
|
import GifVideoLayer
|
||||||
|
|
||||||
final class HorizontalListContextResultsChatInputPanelItem: ListViewItem {
|
final class HorizontalListContextResultsChatInputPanelItem: ListViewItem {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let result: ChatContextResult
|
let result: ChatContextResult
|
||||||
|
let batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>
|
||||||
let resultSelected: (ChatContextResult, ASDisplayNode, CGRect) -> Bool
|
let resultSelected: (ChatContextResult, ASDisplayNode, CGRect) -> Bool
|
||||||
|
|
||||||
let selectable: Bool = true
|
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.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.result = result
|
self.result = result
|
||||||
|
self.batchVideoContext = batchVideoContext
|
||||||
self.resultSelected = resultSelected
|
self.resultSelected = resultSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +93,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
|||||||
private let imageNode: TransformImageNode
|
private let imageNode: TransformImageNode
|
||||||
private var animationNode: AnimatedStickerNode?
|
private var animationNode: AnimatedStickerNode?
|
||||||
private var placeholderNode: StickerShimmerEffectNode?
|
private var placeholderNode: StickerShimmerEffectNode?
|
||||||
private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
|
private var videoLayer: GifVideoLayer?
|
||||||
private var currentImageResource: TelegramMediaResource?
|
private var currentImageResource: TelegramMediaResource?
|
||||||
private var currentVideoFile: TelegramMediaFile?
|
private var currentVideoFile: TelegramMediaFile?
|
||||||
private var currentAnimatedStickerFile: TelegramMediaFile?
|
private var currentAnimatedStickerFile: TelegramMediaFile?
|
||||||
@ -103,58 +106,17 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
|||||||
|
|
||||||
override var visibility: ListViewItemNodeVisibility {
|
override var visibility: ListViewItemNodeVisibility {
|
||||||
didSet {
|
didSet {
|
||||||
switch visibility {
|
switch self.visibility {
|
||||||
case .visible:
|
case .visible:
|
||||||
self.ticking = true
|
self.videoLayer?.shouldBeAnimating = true
|
||||||
default:
|
case .none:
|
||||||
self.ticking = false
|
self.videoLayer?.shouldBeAnimating = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let timebase: CMTimebase
|
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() {
|
init() {
|
||||||
self.imageNodeBackground = ASDisplayNode()
|
self.imageNodeBackground = ASDisplayNode()
|
||||||
self.imageNodeBackground.isLayerBacked = true
|
self.imageNodeBackground.isLayerBacked = true
|
||||||
@ -197,10 +159,6 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
if let displayLink = self.displayLink {
|
|
||||||
displayLink.isPaused = true
|
|
||||||
displayLink.invalidate()
|
|
||||||
}
|
|
||||||
self.statusDisposable.dispose()
|
self.statusDisposable.dispose()
|
||||||
self.fetchDisposable.dispose()
|
self.fetchDisposable.dispose()
|
||||||
}
|
}
|
||||||
@ -384,30 +342,25 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
if updatedVideoFile {
|
if updatedVideoFile {
|
||||||
if let (thumbnailLayer, _, layer) = strongSelf.videoLayer {
|
if let videoLayer = strongSelf.videoLayer {
|
||||||
strongSelf.videoLayer = nil
|
strongSelf.videoLayer = nil
|
||||||
thumbnailLayer.removeFromSupernode()
|
videoLayer.removeFromSuperlayer()
|
||||||
layer.layer.removeFromSuperlayer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let videoFile = videoFile {
|
if let videoFile, let batchVideoContext = item.batchVideoContext.unsafeGet() {
|
||||||
let thumbnailLayer = SoftwareVideoThumbnailNode(account: item.context.account, fileReference: .standalone(media: videoFile), synchronousLoad: synchronousLoads)
|
let videoLayer = GifVideoLayer(
|
||||||
thumbnailLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
context: item.context,
|
||||||
strongSelf.addSubnode(thumbnailLayer)
|
batchVideoContext: batchVideoContext,
|
||||||
let layerHolder = takeSampleBufferLayer()
|
userLocation: .other,
|
||||||
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
file: .standalone(media: videoFile),
|
||||||
layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
synchronousLoad: synchronousLoads
|
||||||
strongSelf.layer.addSublayer(layerHolder.layer)
|
)
|
||||||
|
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 = videoLayer
|
||||||
strongSelf.videoLayer = (thumbnailLayer, manager, layerHolder)
|
videoLayer.shouldBeAnimating = strongSelf.visibility != .none
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,11 +430,9 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
|||||||
strongSelf.statusNode.transitionToState(.none, completion: { })
|
strongSelf.statusNode.transitionToState(.none, completion: { })
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (thumbnailLayer, _, layer) = strongSelf.videoLayer {
|
if let videoLayer = strongSelf.videoLayer {
|
||||||
thumbnailLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
|
videoLayer.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)
|
videoLayer.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 animationNode = strongSelf.animationNode {
|
if let animationNode = strongSelf.animationNode {
|
||||||
|
@ -85,6 +85,7 @@ import GiftStoreScreen
|
|||||||
import SendInviteLinkScreen
|
import SendInviteLinkScreen
|
||||||
import PostSuggestionsSettingsScreen
|
import PostSuggestionsSettingsScreen
|
||||||
import ForumSettingsScreen
|
import ForumSettingsScreen
|
||||||
|
import ForumCreateTopicScreen
|
||||||
|
|
||||||
private final class AccountUserInterfaceInUseContext {
|
private final class AccountUserInterfaceInUseContext {
|
||||||
let subscribers = Bag<(Bool) -> Void>()
|
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 {
|
private func mapIntroSource(source: PremiumIntroSource) -> PremiumSource {
|
||||||
let mappedSource: PremiumSource
|
let mappedSource: PremiumSource
|
||||||
switch source {
|
switch source {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user