mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-01 16:06:59 +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 makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal<CollectibleItemInfoScreenInitialData?, NoError>
|
||||
func makeBotSettingsScreen(context: AccountContext, peerId: EnginePeer.Id?) -> ViewController
|
||||
func makeEditForumTopicScreen(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, threadInfo: EngineMessageHistoryThread.Info, isHidden: Bool) -> ViewController
|
||||
|
||||
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
||||
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
|
||||
|
@ -581,7 +581,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
||||
}
|
||||
}
|
||||
|
||||
func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: Int64, isPinned: Bool?, isClosed: Bool?, chatListController: ChatListControllerImpl?, joined: Bool, canSelect: Bool) -> Signal<[ContextMenuItem], NoError> {
|
||||
public func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: Int64, isPinned: Bool?, isClosed: Bool?, chatListController: ViewController?, joined: Bool, canSelect: Bool, customEdit: ((ContextController) -> Void)? = nil, customPinUnpin: ((ContextController) -> Void)? = nil, reorder: (() -> Void)? = nil) -> Signal<[ContextMenuItem], NoError> {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let strings = presentationData.strings
|
||||
|
||||
@ -603,8 +603,17 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
||||
|
||||
if let isClosed = isClosed, isClosed && threadId != 1 {
|
||||
} else {
|
||||
if let isPinned = isPinned, channel.hasPermission(.manageTopics) {
|
||||
items.append(.action(ContextMenuActionItem(text: isPinned ? presentationData.strings.ChatList_Context_Unpin : presentationData.strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
if let isPinned, channel.hasPermission(.manageTopics) {
|
||||
items.append(.action(ContextMenuActionItem(text: isPinned ? presentationData.strings.ChatList_Context_Unpin : presentationData.strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { c, f in
|
||||
if let customPinUnpin {
|
||||
if let c = c as? ContextController {
|
||||
customPinUnpin(c)
|
||||
} else {
|
||||
f(.default)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
f(.default)
|
||||
|
||||
let _ = (context.engine.peers.toggleForumChannelTopicPinned(id: peerId, threadId: threadId)
|
||||
@ -621,6 +630,15 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
||||
}
|
||||
})
|
||||
})))
|
||||
|
||||
if isPinned, let reorder {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
|
||||
c?.dismiss(completion: {
|
||||
})
|
||||
reorder()
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -636,6 +654,27 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
||||
})))
|
||||
}
|
||||
|
||||
var canOpenClose = false
|
||||
if channel.flags.contains(.isCreator) {
|
||||
canOpenClose = true
|
||||
} else if channel.hasPermission(.manageTopics) {
|
||||
canOpenClose = true
|
||||
} else if threadData.isOwnedByMe {
|
||||
canOpenClose = true
|
||||
}
|
||||
|
||||
if threadId != 1, canOpenClose, let customEdit {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Edit", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { c, f in
|
||||
if let c = c as? ContextController {
|
||||
customEdit(c)
|
||||
} else {
|
||||
f(.default)
|
||||
}
|
||||
return
|
||||
})))
|
||||
}
|
||||
|
||||
var isMuted = false
|
||||
switch threadData.notificationSettings.muteState {
|
||||
case .muted:
|
||||
@ -863,14 +902,6 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
||||
})))
|
||||
|
||||
if threadId != 1 {
|
||||
var canOpenClose = false
|
||||
if channel.flags.contains(.isCreator) {
|
||||
canOpenClose = true
|
||||
} else if channel.hasPermission(.manageTopics) {
|
||||
canOpenClose = true
|
||||
} else if threadData.isOwnedByMe {
|
||||
canOpenClose = true
|
||||
}
|
||||
if canOpenClose {
|
||||
items.append(.action(ContextMenuActionItem(text: threadData.isClosed ? presentationData.strings.ChatList_Context_ReopenTopic : presentationData.strings.ChatList_Context_CloseTopic, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: threadData.isClosed ? "Chat/Context Menu/Play": "Chat/Context Menu/Pause"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
f(.default)
|
||||
@ -882,14 +913,36 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak chatListController] _, f in
|
||||
f(.default)
|
||||
|
||||
chatListController?.deletePeerThread(peerId: peerId, threadId: threadId)
|
||||
if let chatListController = chatListController as? ChatListControllerImpl {
|
||||
chatListController.deletePeerThread(peerId: peerId, threadId: threadId)
|
||||
} else if let chatListController {
|
||||
let actionSheet = ActionSheetController(presentationData: presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
items.append(ActionSheetTextItem(title: presentationData.strings.ChatList_DeleteTopicConfirmationText, parseMarkdown: true))
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.ChatList_DeleteTopicConfirmationAction, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
let _ = context.engine.peers.removeForumChannelThread(id: peerId, threadId: threadId).startStandalone(completed: {
|
||||
})
|
||||
}))
|
||||
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
chatListController.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
if canSelect {
|
||||
if canSelect, let chatListController = chatListController as? ChatListControllerImpl {
|
||||
items.append(.separator)
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Select, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Select, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { [weak chatListController] _, f in
|
||||
f(.default)
|
||||
chatListController?.selectPeerThread(peerId: peerId, threadId: threadId)
|
||||
})))
|
||||
|
@ -16,8 +16,8 @@ public extension UIView {
|
||||
}
|
||||
}
|
||||
|
||||
private extension CALayer {
|
||||
func animate(from: Any, to: Any, keyPath: String, duration: Double, delay: Double, curve: ComponentTransition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) {
|
||||
public extension CALayer {
|
||||
func animate(from: Any, to: Any, keyPath: String, duration: Double, delay: Double, curve: ComponentTransition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil, key: String? = nil) {
|
||||
let timingFunction: String
|
||||
let mediaTimingFunction: CAMediaTimingFunction?
|
||||
switch curve {
|
||||
@ -39,7 +39,8 @@ private extension CALayer {
|
||||
mediaTimingFunction: mediaTimingFunction,
|
||||
removeOnCompletion: removeOnCompletion,
|
||||
additive: additive,
|
||||
completion: completion
|
||||
completion: completion,
|
||||
key: key
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -263,6 +263,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
|
||||
public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)?
|
||||
public final var addContentOffset: ((CGFloat, ListViewItemNode?) -> Void)?
|
||||
public final var shouldStopScrolling: ((CGFloat) -> Bool)?
|
||||
public final var onContentsUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
public final var updateScrollingIndicator: ((ScrollingIndicatorState?, ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
@ -389,6 +390,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
|
||||
private var reorderScrollUpdateTimestamp: Double?
|
||||
private var reorderLastTimestamp: Double?
|
||||
public var reorderedItemHasShadow = true
|
||||
public var reorderingRequiresLongPress = false
|
||||
|
||||
private let waitingForNodesDisposable = MetaDisposable()
|
||||
|
||||
@ -518,7 +520,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
|
||||
let itemNodeFrame = itemNode.frame
|
||||
let itemNodeBounds = itemNode.bounds
|
||||
if itemNode.isReorderable(at: point.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY)) {
|
||||
let requiresLongPress = !strongSelf.reorderedItemHasShadow
|
||||
let requiresLongPress = strongSelf.reorderingRequiresLongPress
|
||||
return (true, requiresLongPress, itemNode)
|
||||
}
|
||||
break
|
||||
@ -1101,6 +1103,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
|
||||
self.updateVisibleContentOffset()
|
||||
self.updateVisibleItemRange()
|
||||
self.updateItemNodesVisibilities(onlyPositive: false)
|
||||
self.onContentsUpdated?(.immediate)
|
||||
|
||||
//CATransaction.commit()
|
||||
}
|
||||
@ -3509,6 +3512,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
|
||||
}
|
||||
|
||||
self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, synchronousLoad: synchronousLoads, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty, animateFullTransition: animateFullTransition)
|
||||
self.onContentsUpdated?(headerNodesTransition.0)
|
||||
|
||||
if let offset = offset, !offset.isZero {
|
||||
//self.didScrollWithOffset?(-offset, headerNodesTransition.0, nil)
|
||||
@ -3733,6 +3737,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
|
||||
} else {
|
||||
self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, synchronousLoad: synchronousLoads, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty, animateFullTransition: animateFullTransition)
|
||||
self.updateItemNodesVisibilities(onlyPositive: deferredUpdateVisible)
|
||||
self.onContentsUpdated?(headerNodesTransition.0)
|
||||
|
||||
applyHeaderNodesFullTransition()
|
||||
|
||||
|
@ -129,7 +129,7 @@ public func listViewAnimationCurveFromAnimationOptions(animationOptions: UIView.
|
||||
|
||||
public final class ListViewAnimation {
|
||||
let from: Interpolatable
|
||||
let to: Interpolatable
|
||||
public let to: Interpolatable
|
||||
let duration: Double
|
||||
let startTime: Double
|
||||
let invertOffsetDirection: Bool
|
||||
|
@ -8923,13 +8923,14 @@ public extension Api.functions.messages {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func unpinAllMessages(flags: Int32, peer: Api.InputPeer, topMsgId: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.AffectedHistory>) {
|
||||
static func unpinAllMessages(flags: Int32, peer: Api.InputPeer, topMsgId: Int32?, savedPeerId: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.AffectedHistory>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-299714136)
|
||||
buffer.appendInt32(103667527)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
peer.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)}
|
||||
return (FunctionDescription(name: "messages.unpinAllMessages", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("topMsgId", String(describing: topMsgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.AffectedHistory? in
|
||||
if Int(flags) & Int(1 << 1) != 0 {savedPeerId!.serialize(buffer, true)}
|
||||
return (FunctionDescription(name: "messages.unpinAllMessages", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("topMsgId", String(describing: topMsgId)), ("savedPeerId", String(describing: savedPeerId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.AffectedHistory? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.messages.AffectedHistory?
|
||||
if let signature = reader.readInt32() {
|
||||
|
@ -2345,7 +2345,8 @@ public func messagesForNotification(transaction: Transaction, id: MessageId, alw
|
||||
|
||||
var notificationSettingsStack: [TelegramPeerNotificationSettings] = []
|
||||
|
||||
if let threadId = message.threadId, let threadData = transaction.getMessageHistoryThreadInfo(peerId: message.id.peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
|
||||
if let peer = peer as? TelegramChannel, peer.isMonoForum {
|
||||
} else if let threadId = message.threadId, let threadData = transaction.getMessageHistoryThreadInfo(peerId: message.id.peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
|
||||
notificationSettingsStack.append(threadData.notificationSettings)
|
||||
}
|
||||
|
||||
|
@ -110,12 +110,18 @@ func _internal_requestUpdatePinnedMessage(account: Account, peerId: PeerId, upda
|
||||
}
|
||||
|
||||
func _internal_requestUnpinAllMessages(account: Account, peerId: PeerId, threadId: Int64?) -> Signal<Never, UpdatePinnedMessageError> {
|
||||
return account.postbox.transaction { transaction -> (Peer?, CachedPeerData?) in
|
||||
return (transaction.getPeer(peerId), transaction.getPeerCachedData(peerId: peerId))
|
||||
return account.postbox.transaction { transaction -> (Peer?, Peer?, CachedPeerData?) in
|
||||
let peer = transaction.getPeer(peerId)
|
||||
var subPeer: Peer?
|
||||
if let channel = peer as? TelegramChannel, channel.isMonoForum, let threadId {
|
||||
subPeer = transaction.getPeer(PeerId(threadId))
|
||||
}
|
||||
|
||||
return (peer, subPeer, transaction.getPeerCachedData(peerId: peerId))
|
||||
}
|
||||
|> mapError { _ -> UpdatePinnedMessageError in
|
||||
}
|
||||
|> mapToSignal { peer, cachedPeerData -> Signal<Never, UpdatePinnedMessageError> in
|
||||
|> mapToSignal { peer, subPeer, cachedPeerData -> Signal<Never, UpdatePinnedMessageError> in
|
||||
guard let peer = peer, let inputPeer = apiInputPeer(peer) else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
@ -148,10 +154,20 @@ func _internal_requestUnpinAllMessages(account: Account, peerId: PeerId, threadI
|
||||
}
|
||||
|
||||
var flags: Int32 = 0
|
||||
if threadId != nil {
|
||||
flags |= (1 << 0)
|
||||
var topMsgId: Int32?
|
||||
var savedPeerId: Api.InputPeer?
|
||||
if let threadId {
|
||||
if let channel = peer as? TelegramChannel, channel.isMonoForum {
|
||||
if let inputSubPeer = subPeer.flatMap(apiInputPeer) {
|
||||
flags |= (1 << 1)
|
||||
savedPeerId = inputSubPeer
|
||||
}
|
||||
} else {
|
||||
flags |= (1 << 0)
|
||||
topMsgId = Int32(clamping: threadId)
|
||||
}
|
||||
}
|
||||
let request: Signal<Never, InternalError> = account.network.request(Api.functions.messages.unpinAllMessages(flags: flags, peer: inputPeer, topMsgId: threadId.flatMap(Int32.init(clamping:))))
|
||||
let request: Signal<Never, InternalError> = account.network.request(Api.functions.messages.unpinAllMessages(flags: flags, peer: inputPeer, topMsgId: topMsgId, savedPeerId: savedPeerId))
|
||||
|> mapError { error -> InternalError in
|
||||
return .error(error.errorDescription)
|
||||
}
|
||||
|
@ -479,6 +479,8 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen",
|
||||
"//submodules/TelegramUI/Components/ForumSettingsScreen",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel",
|
||||
"//submodules/TelegramUI/Components/GifVideoLayer",
|
||||
"//submodules/TelegramUI/Components/BatchVideoRendering",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
23
submodules/TelegramUI/Components/AsyncListComponent/BUILD
Normal file
23
submodules/TelegramUI/Components/AsyncListComponent/BUILD
Normal file
@ -0,0 +1,23 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AsyncListComponent",
|
||||
module_name = "AsyncListComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/MergeLists",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,603 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import MergeLists
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
public final class AsyncListComponent: Component {
|
||||
public protocol ItemView: UIView {
|
||||
func isReorderable(at point: CGPoint) -> Bool
|
||||
}
|
||||
|
||||
public final class OverlayContainerView: UIView {
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.anchorPoint = CGPoint()
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func updatePosition(position: CGPoint, transition: ComponentTransition) {
|
||||
let previousPosition: CGPoint
|
||||
var forceUpdate = false
|
||||
if self.layer.animation(forKey: "positionUpdate") != nil, let presentation = self.layer.presentation() {
|
||||
forceUpdate = true
|
||||
previousPosition = presentation.position
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
self.layer.removeAnimation(forKey: "positionUpdate")
|
||||
}
|
||||
} else {
|
||||
previousPosition = self.layer.position
|
||||
}
|
||||
|
||||
if previousPosition != position || forceUpdate {
|
||||
self.center = position
|
||||
if case let .curve(duration, curve) = transition.animation {
|
||||
self.layer.animate(
|
||||
from: NSValue(cgPoint: CGPoint(x: previousPosition.x - position.x, y: previousPosition.y - position.y)),
|
||||
to: NSValue(cgPoint: CGPoint()),
|
||||
keyPath: "position",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: true,
|
||||
completion: nil,
|
||||
key: "positionUpdate"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ResetScrollingRequest: Equatable {
|
||||
let requestId: Int
|
||||
let id: AnyHashable
|
||||
|
||||
init(requestId: Int, id: AnyHashable) {
|
||||
self.requestId = requestId
|
||||
self.id = id
|
||||
}
|
||||
|
||||
static func ==(lhs: ResetScrollingRequest, rhs: ResetScrollingRequest) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.requestId != rhs.requestId {
|
||||
return false
|
||||
}
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class ExternalState {
|
||||
public struct Value: Equatable {
|
||||
var resetScrollingRequest: ResetScrollingRequest?
|
||||
|
||||
public static func ==(lhs: Value, rhs: Value) -> Bool {
|
||||
if lhs.resetScrollingRequest != rhs.resetScrollingRequest {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public private(set) var value: Value = Value()
|
||||
private var nextId: Int = 0
|
||||
|
||||
public init() {
|
||||
|
||||
}
|
||||
|
||||
public func resetScrolling(id: AnyHashable) {
|
||||
let requestId = self.nextId
|
||||
self.nextId += 1
|
||||
self.value.resetScrollingRequest = ResetScrollingRequest(requestId: requestId, id: id)
|
||||
}
|
||||
}
|
||||
|
||||
public enum Direction {
|
||||
case vertical
|
||||
case horizontal
|
||||
}
|
||||
|
||||
public final class VisibleItem {
|
||||
public let item: AnyComponentWithIdentity<Empty>
|
||||
public let frame: CGRect
|
||||
|
||||
init(item: AnyComponentWithIdentity<Empty>, frame: CGRect) {
|
||||
self.item = item
|
||||
self.frame = frame
|
||||
}
|
||||
}
|
||||
|
||||
public final class VisibleItems: Sequence, IteratorProtocol {
|
||||
private let view: AsyncListComponent.View
|
||||
private var index: Int = 0
|
||||
private let indices: [(Int, CGRect)]
|
||||
|
||||
init(view: AsyncListComponent.View, direction: Direction) {
|
||||
self.view = view
|
||||
var indices: [(Int, CGRect)] = []
|
||||
view.listNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ListItemNodeImpl, let index = itemNode.index {
|
||||
var itemFrame = itemNode.frame
|
||||
itemFrame.origin.y -= itemNode.transitionOffset
|
||||
if let animation = itemNode.animationForKey("height") {
|
||||
if let height = animation.to as? CGFloat {
|
||||
itemFrame.size.height = height
|
||||
}
|
||||
}
|
||||
|
||||
if case .horizontal = direction {
|
||||
itemFrame = CGRect(origin: CGPoint(x: itemFrame.minY, y: itemFrame.minX), size: CGSize(width: itemFrame.height, height: itemFrame.width))
|
||||
}
|
||||
|
||||
indices.append((index, itemFrame))
|
||||
}
|
||||
}
|
||||
indices.sort(by: { $0.0 < $1.0 })
|
||||
self.indices = indices
|
||||
}
|
||||
|
||||
public func next() -> VisibleItem? {
|
||||
if self.index >= self.indices.count {
|
||||
return nil
|
||||
}
|
||||
let index = self.index
|
||||
self.index += 1
|
||||
|
||||
if let component = self.view.component {
|
||||
let (itemIndex, itemFrame) = self.indices[index]
|
||||
return VisibleItem(item: component.items[itemIndex], frame: itemFrame)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public let externalState: ExternalState
|
||||
public let externalStateValue: ExternalState.Value
|
||||
public let items: [AnyComponentWithIdentity<Empty>]
|
||||
public let itemSetId: AnyHashable // Changing itemSetId supresses update animations
|
||||
public let direction: Direction
|
||||
public let insets: UIEdgeInsets
|
||||
public let reorderItems: ((Int, Int) -> Bool)?
|
||||
public let onVisibleItemsUpdated: ((VisibleItems, ComponentTransition) -> Void)?
|
||||
|
||||
public init(
|
||||
externalState: ExternalState,
|
||||
items: [AnyComponentWithIdentity<Empty>],
|
||||
itemSetId: AnyHashable,
|
||||
direction: Direction,
|
||||
insets: UIEdgeInsets,
|
||||
reorderItems: ((Int, Int) -> Bool)? = nil,
|
||||
onVisibleItemsUpdated: ((VisibleItems, ComponentTransition) -> Void)? = nil
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.externalStateValue = externalState.value
|
||||
self.items = items
|
||||
self.itemSetId = itemSetId
|
||||
self.direction = direction
|
||||
self.insets = insets
|
||||
self.reorderItems = reorderItems
|
||||
self.onVisibleItemsUpdated = onVisibleItemsUpdated
|
||||
}
|
||||
|
||||
public static func ==(lhs: AsyncListComponent, rhs: AsyncListComponent) -> Bool {
|
||||
if lhs.externalState !== rhs.externalState {
|
||||
return false
|
||||
}
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.itemSetId != rhs.itemSetId {
|
||||
return false
|
||||
}
|
||||
if lhs.direction != rhs.direction {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
if (lhs.reorderItems == nil) != (rhs.reorderItems == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ItemEntry: Comparable, Identifiable {
|
||||
let contents: AnyComponentWithIdentity<Empty>
|
||||
let index: Int
|
||||
|
||||
var id: AnyHashable {
|
||||
return self.contents.id
|
||||
}
|
||||
|
||||
var stableId: AnyHashable {
|
||||
return self.id
|
||||
}
|
||||
|
||||
static func ==(lhs: ItemEntry, rhs: ItemEntry) -> Bool {
|
||||
if lhs.contents != rhs.contents {
|
||||
return false
|
||||
}
|
||||
if lhs.index != rhs.index {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func <(lhs: ItemEntry, rhs: ItemEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(parentView: AsyncListComponent.View?, direction: Direction) -> ListViewItem {
|
||||
return ListItemImpl(parentView: parentView, contents: self.contents, direction: direction)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ListItemImpl: ListViewItem {
|
||||
weak var parentView: AsyncListComponent.View?
|
||||
let contents: AnyComponentWithIdentity<Empty>
|
||||
let direction: Direction
|
||||
|
||||
let selectable: Bool = false
|
||||
|
||||
init(parentView: AsyncListComponent.View?, contents: AnyComponentWithIdentity<Empty>, direction: Direction) {
|
||||
self.parentView = parentView
|
||||
self.contents = contents
|
||||
self.direction = direction
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(
|
||||
async: @escaping (@escaping () -> Void) -> Void,
|
||||
params: ListViewItemLayoutParams,
|
||||
synchronousLoads: Bool,
|
||||
previousItem: ListViewItem?,
|
||||
nextItem: ListViewItem?,
|
||||
completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void
|
||||
) {
|
||||
async {
|
||||
let impl: () -> Void = {
|
||||
let node = ListItemNodeImpl()
|
||||
let (nodeLayout, apply) = node.asyncLayout()(self, params)
|
||||
node.insets = nodeLayout.insets
|
||||
node.contentSize = nodeLayout.contentSize
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in
|
||||
apply(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if Thread.isMainThread {
|
||||
impl()
|
||||
} else {
|
||||
assert(false)
|
||||
Queue.mainQueue().async {
|
||||
impl()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
assert(node() is ListItemNodeImpl)
|
||||
if let nodeValue = node() as? ListItemNodeImpl {
|
||||
let layout = nodeValue.asyncLayout()
|
||||
async {
|
||||
let impl: () -> Void = {
|
||||
let (nodeLayout, apply) = layout(self, params)
|
||||
Queue.mainQueue().async {
|
||||
completion(nodeLayout, { _ in
|
||||
apply(animation.isAnimated)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if Thread.isMainThread {
|
||||
impl()
|
||||
} else {
|
||||
assert(false)
|
||||
Queue.mainQueue().async {
|
||||
impl()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ListItemNodeImpl: ListViewItemNode {
|
||||
private let contentsView = ComponentView<Empty>()
|
||||
private(set) var item: ListItemImpl?
|
||||
|
||||
init() {
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override func isReorderable(at point: CGPoint) -> Bool {
|
||||
if let itemView = self.contentsView.view as? ItemView {
|
||||
return itemView.isReorderable(at: self.view.convert(point, to: itemView))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func snapshotForReordering() -> UIView? {
|
||||
return self.view.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (ListItemImpl, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
return { item, params in
|
||||
let containerSize: CGSize
|
||||
switch item.direction {
|
||||
case .vertical:
|
||||
containerSize = CGSize(width: params.width, height: 100000.0)
|
||||
case .horizontal:
|
||||
containerSize = CGSize(width: 100000.0, height: params.width)
|
||||
}
|
||||
|
||||
let contentsSize = self.contentsView.update(
|
||||
transition: .immediate,
|
||||
component: item.contents.component,
|
||||
environment: {},
|
||||
containerSize: containerSize
|
||||
)
|
||||
|
||||
let mappedContentsSize: CGSize
|
||||
switch item.direction {
|
||||
case .vertical:
|
||||
mappedContentsSize = CGSize(width: params.width, height: contentsSize.height)
|
||||
case .horizontal:
|
||||
mappedContentsSize = CGSize(width: params.width, height: contentsSize.width)
|
||||
}
|
||||
|
||||
let itemLayout = ListViewItemNodeLayout(contentSize: mappedContentsSize, insets: UIEdgeInsets())
|
||||
return (itemLayout, { animated in
|
||||
self.item = item
|
||||
|
||||
switch item.direction {
|
||||
case .vertical:
|
||||
self.layer.sublayerTransform = CATransform3DIdentity
|
||||
case .horizontal:
|
||||
self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
}
|
||||
|
||||
let contentsFrame = CGRect(origin: CGPoint(), size: contentsSize)
|
||||
|
||||
if let contentsComponentView = self.contentsView.view {
|
||||
if contentsComponentView.superview == nil {
|
||||
self.view.addSubview(contentsComponentView)
|
||||
}
|
||||
contentsComponentView.center = CGPoint(x: mappedContentsSize.width * 0.5, y: mappedContentsSize.height * 0.5)
|
||||
contentsComponentView.bounds = CGRect(origin: CGPoint(), size: contentsFrame.size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration, options: options)
|
||||
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateRemoved(currentTimestamp, duration: duration)
|
||||
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateAdded(currentTimestamp, duration: duration)
|
||||
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
let listNode: ListView
|
||||
|
||||
private var externalStateValue: ExternalState.Value?
|
||||
private var isUpdating: Bool = false
|
||||
private(set) var component: AsyncListComponent?
|
||||
|
||||
private var currentEntries: [ItemEntry] = []
|
||||
|
||||
private var ignoreUpdateVisibleItems: Bool = false
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.listNode = ListView()
|
||||
self.listNode.useMainQueueTransactions = true
|
||||
self.listNode.scroller.delaysContentTouches = false
|
||||
self.listNode.reorderedItemHasShadow = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.listNode.view)
|
||||
|
||||
self.listNode.onContentsUpdated = { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateVisibleItems(transition: ComponentTransition(transition))
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
public func stopScrolling() {
|
||||
self.listNode.stopScrolling()
|
||||
}
|
||||
|
||||
private func updateVisibleItems(transition: ComponentTransition) {
|
||||
if self.ignoreUpdateVisibleItems {
|
||||
return
|
||||
}
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
if let onVisibleItemsUpdated = component.onVisibleItemsUpdated {
|
||||
onVisibleItemsUpdated(VisibleItems(view: self, direction: component.direction), transition)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: AsyncListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
||||
let listSize: CGSize
|
||||
let listInsets: UIEdgeInsets
|
||||
switch component.direction {
|
||||
case .vertical:
|
||||
self.listNode.transform = CATransform3DIdentity
|
||||
listSize = CGSize(width: availableSize.width, height: availableSize.height)
|
||||
listInsets = component.insets
|
||||
case .horizontal:
|
||||
self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
listSize = CGSize(width: availableSize.height, height: availableSize.width)
|
||||
listInsets = UIEdgeInsets(top: component.insets.left, left: component.insets.top, bottom: component.insets.right, right: component.insets.bottom)
|
||||
}
|
||||
|
||||
var updateSizeAndInsets = ListViewUpdateSizeAndInsets(
|
||||
size: listSize,
|
||||
insets: listInsets,
|
||||
duration: 0.0,
|
||||
curve: .Default(duration: nil)
|
||||
)
|
||||
|
||||
var animateTransition = false
|
||||
var transactionOptions: ListViewDeleteAndInsertOptions = []
|
||||
|
||||
if !transition.animation.isImmediate, let previousComponent {
|
||||
if previousComponent.itemSetId == component.itemSetId {
|
||||
transactionOptions.insert(.AnimateInsertion)
|
||||
}
|
||||
animateTransition = true
|
||||
|
||||
switch transition.animation {
|
||||
case .none:
|
||||
break
|
||||
case let .curve(duration, curve):
|
||||
updateSizeAndInsets.duration = duration
|
||||
switch curve {
|
||||
case .linear, .easeInOut:
|
||||
updateSizeAndInsets.curve = .Default(duration: duration)
|
||||
case .spring:
|
||||
updateSizeAndInsets.curve = .Spring(duration: duration)
|
||||
case let .custom(a, b, c, d):
|
||||
updateSizeAndInsets.curve = .Custom(duration: duration, a, b, c, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var entries: [ItemEntry] = []
|
||||
for item in component.items {
|
||||
entries.append(ItemEntry(
|
||||
contents: item,
|
||||
index: entries.count
|
||||
))
|
||||
}
|
||||
|
||||
var scrollToItem: ListViewScrollToItem?
|
||||
if let resetScrollingRequest = component.externalStateValue.resetScrollingRequest, previousComponent?.externalStateValue.resetScrollingRequest != component.externalStateValue.resetScrollingRequest {
|
||||
//TODO:release calculate direction hint
|
||||
if let index = entries.firstIndex(where: { $0.id == resetScrollingRequest.id }) {
|
||||
scrollToItem = ListViewScrollToItem(
|
||||
index: index,
|
||||
position: .visible,
|
||||
animated: animateTransition,
|
||||
curve: updateSizeAndInsets.curve,
|
||||
directionHint: .Down
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
self.ignoreUpdateVisibleItems = true
|
||||
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries)
|
||||
self.currentEntries = entries
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(parentView: self, direction: component.direction), directionHint: .Down) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(parentView: self, direction: component.direction), directionHint: nil) }
|
||||
|
||||
transactionOptions.insert(.Synchronous)
|
||||
|
||||
self.listNode.transaction(
|
||||
deleteIndices: deletions,
|
||||
insertIndicesAndItems: insertions,
|
||||
updateIndicesAndItems: updates,
|
||||
options: transactionOptions,
|
||||
scrollToItem: scrollToItem,
|
||||
updateSizeAndInsets: updateSizeAndInsets,
|
||||
stationaryItemRange: nil,
|
||||
updateOpaqueState: nil,
|
||||
completion: { _ in }
|
||||
)
|
||||
|
||||
let mappedListFrame: CGRect
|
||||
switch component.direction {
|
||||
case .vertical:
|
||||
mappedListFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5), size: listSize)
|
||||
case .horizontal:
|
||||
mappedListFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5), size: listSize)
|
||||
}
|
||||
self.listNode.position = mappedListFrame.origin
|
||||
self.listNode.bounds = CGRect(origin: CGPoint(), size: mappedListFrame.size)
|
||||
|
||||
self.listNode.reorderItem = { [weak self] fromIndex, toIndex, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return .single(false)
|
||||
}
|
||||
guard let reorderItems = component.reorderItems else {
|
||||
return .single(false)
|
||||
}
|
||||
|
||||
if reorderItems(fromIndex, toIndex) {
|
||||
return .single(true)
|
||||
} else {
|
||||
return .single(false)
|
||||
}
|
||||
}
|
||||
|
||||
self.ignoreUpdateVisibleItems = false
|
||||
|
||||
self.updateVisibleItems(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -19,7 +19,8 @@ swift_library(
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/SoftwareVideo",
|
||||
"//submodules/TelegramUI/Components/MultiplexedVideoNode",
|
||||
"//submodules/TelegramUI/Components/BatchVideoRendering",
|
||||
"//submodules/TelegramUI/Components/GifVideoLayer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -10,17 +10,21 @@ import PhotoResources
|
||||
import AppBundle
|
||||
import ContextUI
|
||||
import SoftwareVideo
|
||||
import MultiplexedVideoNode
|
||||
import BatchVideoRendering
|
||||
import GifVideoLayer
|
||||
import AccountContext
|
||||
|
||||
public final class ChatContextResultPeekContent: PeekControllerContent {
|
||||
public let account: Account
|
||||
public let context: AccountContext
|
||||
public let contextResult: ChatContextResult
|
||||
public let menu: [ContextMenuItem]
|
||||
public let batchVideoContext: BatchVideoRenderingContext
|
||||
|
||||
public init(account: Account, contextResult: ChatContextResult, menu: [ContextMenuItem]) {
|
||||
self.account = account
|
||||
public init(context: AccountContext, contextResult: ChatContextResult, menu: [ContextMenuItem], batchVideoContext: BatchVideoRenderingContext) {
|
||||
self.context = context
|
||||
self.contextResult = contextResult
|
||||
self.menu = menu
|
||||
self.batchVideoContext = batchVideoContext
|
||||
}
|
||||
|
||||
public func presentation() -> PeekControllerContentPresentation {
|
||||
@ -36,7 +40,7 @@ public final class ChatContextResultPeekContent: PeekControllerContent {
|
||||
}
|
||||
|
||||
public func node() -> PeekControllerContentNode & ASDisplayNode {
|
||||
return ChatContextResultPeekNode(account: self.account, contextResult: self.contextResult)
|
||||
return ChatContextResultPeekNode(context: self.context, contextResult: self.contextResult, batchVideoContext: self.batchVideoContext)
|
||||
}
|
||||
|
||||
public func topAccessoryNode() -> ASDisplayNode? {
|
||||
@ -62,62 +66,29 @@ public final class ChatContextResultPeekContent: PeekControllerContent {
|
||||
}
|
||||
|
||||
private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerContentNode {
|
||||
private let account: Account
|
||||
private let context: AccountContext
|
||||
private let contextResult: ChatContextResult
|
||||
private let batchVideoContext: BatchVideoRenderingContext
|
||||
|
||||
private let imageNodeBackground: ASDisplayNode
|
||||
private let imageNode: TransformImageNode
|
||||
private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
|
||||
private var videoLayer: GifVideoLayer?
|
||||
|
||||
private var currentImageResource: TelegramMediaResource?
|
||||
private var currentVideoFile: TelegramMediaFile?
|
||||
|
||||
private let timebase: CMTimebase
|
||||
|
||||
private var displayLink: CADisplayLink?
|
||||
private var ticking: Bool = false {
|
||||
didSet {
|
||||
if self.ticking != oldValue {
|
||||
if self.ticking {
|
||||
class DisplayLinkProxy: NSObject {
|
||||
weak var target: ChatContextResultPeekNode?
|
||||
init(target: ChatContextResultPeekNode) {
|
||||
self.target = target
|
||||
}
|
||||
|
||||
@objc func displayLinkEvent() {
|
||||
self.target?.displayLinkEvent()
|
||||
}
|
||||
}
|
||||
|
||||
let displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
|
||||
self.displayLink = displayLink
|
||||
displayLink.add(to: RunLoop.main, forMode: .common)
|
||||
if #available(iOS 10.0, *) {
|
||||
displayLink.preferredFramesPerSecond = 25
|
||||
} else {
|
||||
displayLink.frameInterval = 2
|
||||
}
|
||||
displayLink.isPaused = false
|
||||
CMTimebaseSetRate(self.timebase, rate: 1.0)
|
||||
} else if let displayLink = self.displayLink {
|
||||
self.displayLink = nil
|
||||
displayLink.isPaused = true
|
||||
displayLink.invalidate()
|
||||
CMTimebaseSetRate(self.timebase, rate: 0.0)
|
||||
}
|
||||
self.videoLayer?.shouldBeAnimating = self.ticking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func displayLinkEvent() {
|
||||
let timestamp = CMTimebaseGetTime(self.timebase).seconds
|
||||
self.videoLayer?.1.tick(timestamp: timestamp)
|
||||
}
|
||||
|
||||
init(account: Account, contextResult: ChatContextResult) {
|
||||
self.account = account
|
||||
init(context: AccountContext, contextResult: ChatContextResult, batchVideoContext: BatchVideoRenderingContext) {
|
||||
self.context = context
|
||||
self.contextResult = contextResult
|
||||
self.batchVideoContext = batchVideoContext
|
||||
|
||||
self.imageNodeBackground = ASDisplayNode()
|
||||
self.imageNodeBackground.isLayerBacked = true
|
||||
@ -128,11 +99,6 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
|
||||
self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
|
||||
var timebase: CMTimebase?
|
||||
CMTimebaseCreateWithSourceClock(allocator: nil, sourceClock: CMClockGetHostTimeClock(), timebaseOut: &timebase)
|
||||
CMTimebaseSetRate(timebase!, rate: 0.0)
|
||||
self.timebase = timebase!
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNodeBackground)
|
||||
@ -142,10 +108,6 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let displayLink = self.displayLink {
|
||||
displayLink.isPaused = true
|
||||
displayLink.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func ready() -> Signal<Bool, NoError> {
|
||||
@ -236,7 +198,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
|
||||
if let imageResource = imageResource {
|
||||
let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: Int32(fittedImageDimensions.width * 2.0), height: Int32(fittedImageDimensions.height * 2.0)), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)
|
||||
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
|
||||
updateImageSignal = chatMessagePhoto(postbox: self.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage))
|
||||
updateImageSignal = chatMessagePhoto(postbox: self.context.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage))
|
||||
} else {
|
||||
updateImageSignal = .complete()
|
||||
}
|
||||
@ -256,33 +218,26 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
|
||||
}
|
||||
|
||||
if updatedVideoFile {
|
||||
if let (thumbnailLayer, _, layer) = self.videoLayer {
|
||||
if let videoLayer = self.videoLayer {
|
||||
self.videoLayer = nil
|
||||
thumbnailLayer.removeFromSupernode()
|
||||
layer.layer.removeFromSuperlayer()
|
||||
videoLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
if let videoFileReference = videoFileReference {
|
||||
let thumbnailLayer = SoftwareVideoThumbnailNode(account: self.account, fileReference: videoFileReference, synchronousLoad: false)
|
||||
self.addSubnode(thumbnailLayer)
|
||||
let layerHolder = takeSampleBufferLayer()
|
||||
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
||||
self.layer.addSublayer(layerHolder.layer)
|
||||
let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .other, fileReference: videoFileReference, layerHolder: layerHolder)
|
||||
self.videoLayer = (thumbnailLayer, manager, layerHolder)
|
||||
thumbnailLayer.ready = { [weak self, weak thumbnailLayer, weak manager] in
|
||||
if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager {
|
||||
if strongSelf.videoLayer?.0 === thumbnailLayer && strongSelf.videoLayer?.1 === manager {
|
||||
manager.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
if let videoFileReference {
|
||||
let videoLayer = GifVideoLayer(
|
||||
context: self.context,
|
||||
batchVideoContext: self.batchVideoContext,
|
||||
userLocation: .other,
|
||||
file: videoFileReference,
|
||||
synchronousLoad: false
|
||||
)
|
||||
self.videoLayer = videoLayer
|
||||
self.layer.addSublayer(videoLayer)
|
||||
}
|
||||
}
|
||||
|
||||
if let (thumbnailLayer, _, layer) = self.videoLayer {
|
||||
thumbnailLayer.frame = CGRect(origin: CGPoint(), size: croppedImageDimensions)
|
||||
layer.layer.frame = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
|
||||
if let videoLayer = self.videoLayer {
|
||||
videoLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
|
||||
}
|
||||
|
||||
if !self.ticking {
|
||||
|
@ -24,6 +24,13 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/ChatListUI",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/TelegramUI/Components/AsyncListComponent",
|
||||
"//submodules/TelegramUI/Components/TextBadgeComponent",
|
||||
"//submodules/TelegramUI/Components/MaskedContainerComponent",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/PresentationDataUtils",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,19 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "MaskedContainerComponent",
|
||||
module_name = "MaskedContainerComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,94 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
public final class MaskedContainerView: UIView {
|
||||
public struct Item: Equatable {
|
||||
public enum Shape: Equatable {
|
||||
case ellipse
|
||||
case roundedRect(cornerRadius: CGFloat)
|
||||
}
|
||||
|
||||
public var frame: CGRect
|
||||
public var shape: Shape
|
||||
|
||||
public init(frame: CGRect, shape: Shape) {
|
||||
self.frame = frame
|
||||
self.shape = shape
|
||||
}
|
||||
}
|
||||
|
||||
private struct Params: Equatable {
|
||||
let size: CGSize
|
||||
let items: [Item]
|
||||
let isInverted: Bool
|
||||
|
||||
init(size: CGSize, items: [Item], isInverted: Bool) {
|
||||
self.size = size
|
||||
self.items = items
|
||||
self.isInverted = isInverted
|
||||
}
|
||||
}
|
||||
|
||||
public let contentView: UIView
|
||||
public let contentMaskView: UIImageView
|
||||
|
||||
private var params: Params?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.contentView = UIView()
|
||||
self.contentMaskView = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.contentView)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func update(size: CGSize, items: [Item], isInverted: Bool) {
|
||||
let params = Params(size: size, items: items, isInverted: isInverted)
|
||||
if self.params == params {
|
||||
return
|
||||
}
|
||||
self.params = params
|
||||
self.contentView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.contentMaskView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
if items.isEmpty {
|
||||
self.contentMaskView.image = nil
|
||||
self.contentView.mask = nil
|
||||
} else {
|
||||
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: size))
|
||||
let image = renderer.image { context in
|
||||
UIGraphicsPushContext(context.cgContext)
|
||||
|
||||
if isInverted {
|
||||
context.cgContext.setFillColor(UIColor.black.cgColor)
|
||||
context.cgContext.fill(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.cgContext.setFillColor(UIColor.clear.cgColor)
|
||||
context.cgContext.setBlendMode(.copy)
|
||||
}
|
||||
|
||||
for item in items {
|
||||
switch item.shape {
|
||||
case .ellipse:
|
||||
context.cgContext.fillEllipse(in: item.frame)
|
||||
case let .roundedRect(cornerRadius):
|
||||
context.cgContext.addPath(UIBezierPath(roundedRect: item.frame, cornerRadius: cornerRadius).cgPath)
|
||||
context.cgContext.fillPath()
|
||||
}
|
||||
}
|
||||
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
self.contentMaskView.image = image
|
||||
|
||||
self.contentView.mask = self.contentMaskView
|
||||
}
|
||||
}
|
||||
}
|
@ -198,7 +198,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec
|
||||
}
|
||||
|
||||
|
||||
private final class ThemeCarouselThemeItemIconNode : ListViewItemNode {
|
||||
private final class ThemeCarouselThemeItemIconNode: ListViewItemNode {
|
||||
private let containerNode: ASDisplayNode
|
||||
private let emojiContainerNode: ASDisplayNode
|
||||
private let imageNode: TransformImageNode
|
||||
|
19
submodules/TelegramUI/Components/TextBadgeComponent/BUILD
Normal file
19
submodules/TelegramUI/Components/TextBadgeComponent/BUILD
Normal file
@ -0,0 +1,19 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "TextBadgeComponent",
|
||||
module_name = "TextBadgeComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,171 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
public final class TextBadgeComponent: Component {
|
||||
public let text: String
|
||||
public let font: UIFont
|
||||
public let background: UIColor
|
||||
public let foreground: UIColor
|
||||
public let insets: UIEdgeInsets
|
||||
|
||||
public init(
|
||||
text: String,
|
||||
font: UIFont,
|
||||
background: UIColor,
|
||||
foreground: UIColor,
|
||||
insets: UIEdgeInsets
|
||||
) {
|
||||
self.text = text
|
||||
self.font = font
|
||||
self.background = background
|
||||
self.foreground = foreground
|
||||
self.insets = insets
|
||||
}
|
||||
|
||||
public static func ==(lhs: TextBadgeComponent, rhs: TextBadgeComponent) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.font != rhs.font {
|
||||
return false
|
||||
}
|
||||
if lhs.background != rhs.background {
|
||||
return false
|
||||
}
|
||||
if lhs.foreground != rhs.foreground {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct TextLayout {
|
||||
var size: CGSize
|
||||
var opticalBounds: CGRect
|
||||
|
||||
init(size: CGSize, opticalBounds: CGRect) {
|
||||
self.size = size
|
||||
self.opticalBounds = opticalBounds
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let backgroundView: UIImageView
|
||||
private let textContentsView: UIImageView
|
||||
|
||||
private var textLayout: TextLayout?
|
||||
|
||||
private var component: TextBadgeComponent?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.backgroundView = UIImageView()
|
||||
|
||||
self.textContentsView = UIImageView()
|
||||
self.textContentsView.layer.anchorPoint = CGPoint()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.textContentsView)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: TextBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
||||
if component.text != previousComponent?.text || component.font != previousComponent?.font {
|
||||
let attributedText = NSAttributedString(string: component.text, attributes: [
|
||||
NSAttributedString.Key.font: component.font,
|
||||
NSAttributedString.Key.foregroundColor: UIColor.white
|
||||
])
|
||||
|
||||
var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil)
|
||||
boundingRect.size.width = ceil(boundingRect.size.width)
|
||||
boundingRect.size.height = ceil(boundingRect.size.height)
|
||||
|
||||
if let context = DrawingContext(size: boundingRect.size, scale: 0.0, opaque: false, clear: true) {
|
||||
context.withContext { c in
|
||||
UIGraphicsPushContext(c)
|
||||
defer {
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
|
||||
attributedText.draw(at: CGPoint())
|
||||
}
|
||||
var minFilledLineY = Int(context.scaledSize.height) - 1
|
||||
var maxFilledLineY = 0
|
||||
var minFilledLineX = Int(context.scaledSize.width) - 1
|
||||
var maxFilledLineX = 0
|
||||
for y in 0 ..< Int(context.scaledSize.height) {
|
||||
let linePtr = context.bytes.advanced(by: max(0, y) * context.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
|
||||
for x in 0 ..< Int(context.scaledSize.width) {
|
||||
let pixelPtr = linePtr.advanced(by: x)
|
||||
if pixelPtr.pointee != 0 {
|
||||
minFilledLineY = min(y, minFilledLineY)
|
||||
maxFilledLineY = max(y, maxFilledLineY)
|
||||
minFilledLineX = min(x, minFilledLineX)
|
||||
maxFilledLineX = max(x, maxFilledLineX)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var opticalBounds = CGRect()
|
||||
if minFilledLineX <= maxFilledLineX && minFilledLineY <= maxFilledLineY {
|
||||
opticalBounds.origin.x = CGFloat(minFilledLineX) / context.scale
|
||||
opticalBounds.origin.y = CGFloat(minFilledLineY) / context.scale
|
||||
opticalBounds.size.width = CGFloat(maxFilledLineX - minFilledLineX) / context.scale
|
||||
opticalBounds.size.height = CGFloat(maxFilledLineY - minFilledLineY) / context.scale
|
||||
}
|
||||
|
||||
self.textContentsView.image = context.generateImage()?.withRenderingMode(.alwaysTemplate)
|
||||
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: opticalBounds)
|
||||
} else {
|
||||
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: CGRect(origin: CGPoint(), size: boundingRect.size))
|
||||
}
|
||||
}
|
||||
|
||||
let textSize = self.textLayout?.size ?? CGSize(width: 1.0, height: 1.0)
|
||||
|
||||
var size = CGSize(width: textSize.width + component.insets.left + component.insets.right, height: textSize.height + component.insets.top + component.insets.bottom)
|
||||
size.width = max(size.width, size.height)
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) * 0.5), y: component.insets.top + UIScreenPixel), size: textSize)
|
||||
/*if let textLayout = self.textLayout {
|
||||
textFrame.origin.x = textLayout.opticalBounds.minX + floorToScreenPixels((backgroundFrame.width - textLayout.opticalBounds.width) * 0.5)
|
||||
textFrame.origin.y = textLayout.opticalBounds.minY + floorToScreenPixels((backgroundFrame.height - textLayout.opticalBounds.height) * 0.5)
|
||||
}*/
|
||||
|
||||
transition.setPosition(view: self.textContentsView, position: textFrame.origin)
|
||||
self.textContentsView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
|
||||
if size.height != self.backgroundView.image?.size.height {
|
||||
self.backgroundView.image = generateStretchableFilledCircleImage(diameter: size.height, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
|
||||
self.backgroundView.tintColor = component.background
|
||||
self.textContentsView.tintColor = component.foreground
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -271,9 +271,13 @@ func sidePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState
|
||||
context: context,
|
||||
theme: chatPresentationInterfaceState.theme,
|
||||
strings: chatPresentationInterfaceState.strings,
|
||||
location: .side,
|
||||
peerId: peerId,
|
||||
isMonoforum: true,
|
||||
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
||||
controller: { [weak interfaceInteraction] in
|
||||
return interfaceInteraction?.chatController()
|
||||
},
|
||||
togglePanel: { [weak interfaceInteraction] in
|
||||
interfaceInteraction?.toggleChatSidebarMode()
|
||||
},
|
||||
@ -292,9 +296,13 @@ func sidePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState
|
||||
context: context,
|
||||
theme: chatPresentationInterfaceState.theme,
|
||||
strings: chatPresentationInterfaceState.strings,
|
||||
location: .side,
|
||||
peerId: peerId,
|
||||
isMonoforum: false,
|
||||
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
||||
controller: { [weak interfaceInteraction] in
|
||||
return interfaceInteraction?.chatController()
|
||||
},
|
||||
togglePanel: { [weak interfaceInteraction] in
|
||||
interfaceInteraction?.toggleChatSidebarMode()
|
||||
},
|
||||
|
@ -14,173 +14,9 @@ import EmojiStatusComponent
|
||||
import SwiftSignalKit
|
||||
import BundleIconComponent
|
||||
import AvatarNode
|
||||
|
||||
private final class CustomBadgeComponent: Component {
|
||||
public let text: String
|
||||
public let font: UIFont
|
||||
public let background: UIColor
|
||||
public let foreground: UIColor
|
||||
public let insets: UIEdgeInsets
|
||||
|
||||
public init(
|
||||
text: String,
|
||||
font: UIFont,
|
||||
background: UIColor,
|
||||
foreground: UIColor,
|
||||
insets: UIEdgeInsets
|
||||
) {
|
||||
self.text = text
|
||||
self.font = font
|
||||
self.background = background
|
||||
self.foreground = foreground
|
||||
self.insets = insets
|
||||
}
|
||||
|
||||
public static func ==(lhs: CustomBadgeComponent, rhs: CustomBadgeComponent) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.font != rhs.font {
|
||||
return false
|
||||
}
|
||||
if lhs.background != rhs.background {
|
||||
return false
|
||||
}
|
||||
if lhs.foreground != rhs.foreground {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct TextLayout {
|
||||
var size: CGSize
|
||||
var opticalBounds: CGRect
|
||||
|
||||
init(size: CGSize, opticalBounds: CGRect) {
|
||||
self.size = size
|
||||
self.opticalBounds = opticalBounds
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let backgroundView: UIImageView
|
||||
private let textContentsView: UIImageView
|
||||
|
||||
private var textLayout: TextLayout?
|
||||
|
||||
private var component: CustomBadgeComponent?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.backgroundView = UIImageView()
|
||||
|
||||
self.textContentsView = UIImageView()
|
||||
self.textContentsView.layer.anchorPoint = CGPoint()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.textContentsView)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: CustomBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
||||
if component.text != previousComponent?.text || component.font != previousComponent?.font {
|
||||
let attributedText = NSAttributedString(string: component.text, attributes: [
|
||||
NSAttributedString.Key.font: component.font,
|
||||
NSAttributedString.Key.foregroundColor: UIColor.white
|
||||
])
|
||||
|
||||
var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil)
|
||||
boundingRect.size.width = ceil(boundingRect.size.width)
|
||||
boundingRect.size.height = ceil(boundingRect.size.height)
|
||||
|
||||
if let context = DrawingContext(size: boundingRect.size, scale: 0.0, opaque: false, clear: true) {
|
||||
context.withContext { c in
|
||||
UIGraphicsPushContext(c)
|
||||
defer {
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
|
||||
attributedText.draw(at: CGPoint())
|
||||
}
|
||||
var minFilledLineY = Int(context.scaledSize.height) - 1
|
||||
var maxFilledLineY = 0
|
||||
var minFilledLineX = Int(context.scaledSize.width) - 1
|
||||
var maxFilledLineX = 0
|
||||
for y in 0 ..< Int(context.scaledSize.height) {
|
||||
let linePtr = context.bytes.advanced(by: max(0, y) * context.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
|
||||
for x in 0 ..< Int(context.scaledSize.width) {
|
||||
let pixelPtr = linePtr.advanced(by: x)
|
||||
if pixelPtr.pointee != 0 {
|
||||
minFilledLineY = min(y, minFilledLineY)
|
||||
maxFilledLineY = max(y, maxFilledLineY)
|
||||
minFilledLineX = min(x, minFilledLineX)
|
||||
maxFilledLineX = max(x, maxFilledLineX)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var opticalBounds = CGRect()
|
||||
if minFilledLineX <= maxFilledLineX && minFilledLineY <= maxFilledLineY {
|
||||
opticalBounds.origin.x = CGFloat(minFilledLineX) / context.scale
|
||||
opticalBounds.origin.y = CGFloat(minFilledLineY) / context.scale
|
||||
opticalBounds.size.width = CGFloat(maxFilledLineX - minFilledLineX) / context.scale
|
||||
opticalBounds.size.height = CGFloat(maxFilledLineY - minFilledLineY) / context.scale
|
||||
}
|
||||
|
||||
self.textContentsView.image = context.generateImage()?.withRenderingMode(.alwaysTemplate)
|
||||
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: opticalBounds)
|
||||
} else {
|
||||
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: CGRect(origin: CGPoint(), size: boundingRect.size))
|
||||
}
|
||||
}
|
||||
|
||||
let textSize = self.textLayout?.size ?? CGSize(width: 1.0, height: 1.0)
|
||||
|
||||
var size = CGSize(width: textSize.width + component.insets.left + component.insets.right, height: textSize.height + component.insets.top + component.insets.bottom)
|
||||
size.width = max(size.width, size.height)
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) * 0.5), y: component.insets.top + UIScreenPixel), size: textSize)
|
||||
/*if let textLayout = self.textLayout {
|
||||
textFrame.origin.x = textLayout.opticalBounds.minX + floorToScreenPixels((backgroundFrame.width - textLayout.opticalBounds.width) * 0.5)
|
||||
textFrame.origin.y = textLayout.opticalBounds.minY + floorToScreenPixels((backgroundFrame.height - textLayout.opticalBounds.height) * 0.5)
|
||||
}*/
|
||||
|
||||
transition.setPosition(view: self.textContentsView, position: textFrame.origin)
|
||||
self.textContentsView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
|
||||
if size.height != self.backgroundView.image?.size.height {
|
||||
self.backgroundView.image = generateStretchableFilledCircleImage(diameter: size.height, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
|
||||
self.backgroundView.tintColor = component.background
|
||||
self.textContentsView.tintColor = component.foreground
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
import TextBadgeComponent
|
||||
import ChatSideTopicsPanel
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, ChatControllerCustomNavigationPanelNode {
|
||||
private struct Params: Equatable {
|
||||
@ -213,31 +49,7 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
||||
}
|
||||
}
|
||||
|
||||
private final class Item: Equatable {
|
||||
typealias Id = EngineChatList.Item.Id
|
||||
|
||||
let item: EngineChatList.Item
|
||||
|
||||
var id: Id {
|
||||
return self.item.id
|
||||
}
|
||||
|
||||
init(item: EngineChatList.Item) {
|
||||
self.item = item
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.item != rhs.item {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemView: UIView {
|
||||
/*private final class ItemView: UIView {
|
||||
private let context: AccountContext
|
||||
private let action: () -> Void
|
||||
|
||||
@ -396,11 +208,11 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
||||
|
||||
badgeSize = badge.update(
|
||||
transition: badgeTransition,
|
||||
component: AnyComponent(CustomBadgeComponent(
|
||||
text: "\(readCounters.count)",
|
||||
component: AnyComponent(TextBadgeComponent(
|
||||
text: countString(Int64(readCounters.count)),
|
||||
font: Font.regular(12.0),
|
||||
background: theme.list.itemCheckColors.fillColor,
|
||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||
background: item.item.isMuted ? theme.chatList.unreadBadgeInactiveBackgroundColor : theme.chatList.unreadBadgeActiveBackgroundColor,
|
||||
foreground: item.item.isMuted ? theme.chatList.unreadBadgeInactiveTextColor : theme.chatList.unreadBadgeActiveTextColor,
|
||||
insets: UIEdgeInsets(top: 1.0, left: 5.0, bottom: 2.0, right: 5.0)
|
||||
)),
|
||||
environment: {},
|
||||
@ -715,99 +527,26 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private enum ScrollId: Equatable {
|
||||
case all
|
||||
case topic(Int64)
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let isMonoforum: Bool
|
||||
|
||||
private let scrollView: ScrollView
|
||||
private let scrollViewContainer: UIView
|
||||
private let scrollViewMask: UIImageView
|
||||
}*/
|
||||
|
||||
private var params: Params?
|
||||
|
||||
private var items: [Item] = []
|
||||
private var itemViews: [Item.Id: ItemView] = [:]
|
||||
private var allItemView: AllItemView?
|
||||
private var tabItemView: TabItemView?
|
||||
private let selectedLineView: UIImageView
|
||||
|
||||
private var itemsDisposable: Disposable?
|
||||
|
||||
private var appliedScrollToId: ScrollId?
|
||||
private let context: AccountContext
|
||||
private let peerId: EnginePeer.Id
|
||||
private let isMonoforum: Bool
|
||||
private let panel = ComponentView<ChatSidePanelEnvironment>()
|
||||
|
||||
init(context: AccountContext, peerId: EnginePeer.Id, isMonoforum: Bool) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.isMonoforum = isMonoforum
|
||||
|
||||
self.selectedLineView = UIImageView()
|
||||
|
||||
self.scrollView = ScrollView(frame: CGRect())
|
||||
self.scrollViewMask = UIImageView(image: generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [
|
||||
UIColor(white: 1.0, alpha: 0.0),
|
||||
UIColor(white: 1.0, alpha: 1.0)
|
||||
], locations: [0.0, 1.0], direction: .horizontal)?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 0))
|
||||
|
||||
self.scrollViewContainer = UIView()
|
||||
|
||||
super.init()
|
||||
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.clipsToBounds = true
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.alwaysBounceHorizontal = false
|
||||
self.scrollView.alwaysBounceVertical = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
|
||||
self.scrollViewContainer.addSubview(self.scrollView)
|
||||
self.scrollViewContainer.mask = self.scrollViewMask
|
||||
|
||||
self.view.addSubview(self.scrollViewContainer)
|
||||
|
||||
self.scrollView.addSubview(self.selectedLineView)
|
||||
|
||||
self.scrollView.disablesInteractiveTransitionGestureRecognizer = true
|
||||
|
||||
let threadListSignal: Signal<EngineChatList, NoError> = context.sharedContext.subscribeChatListData(context: context, location: isMonoforum ? .savedMessagesChats(peerId: peerId) : .forum(peerId: peerId))
|
||||
|
||||
self.itemsDisposable = (threadListSignal
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] chatList in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.items.removeAll()
|
||||
|
||||
for item in chatList.items.reversed() {
|
||||
self.items.append(Item(item: item))
|
||||
}
|
||||
|
||||
self.update(transition: .immediate)
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.itemsDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func update(transition: ContainedViewLayoutTransition) {
|
||||
@ -819,14 +558,6 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
||||
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult {
|
||||
let params = Params(width: width, leftInset: leftInset, rightInset: rightInset, interfaceState: interfaceState)
|
||||
if self.params != params {
|
||||
if self.params?.interfaceState.theme !== params.interfaceState.theme {
|
||||
self.selectedLineView.image = generateImage(CGSize(width: 7.0, height: 4.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(params.interfaceState.theme.rootController.navigationBar.accentTextColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width)))
|
||||
})?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 1)
|
||||
}
|
||||
|
||||
self.params = params
|
||||
self.update(params: params, transition: transition)
|
||||
}
|
||||
@ -841,14 +572,54 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
||||
}
|
||||
|
||||
private func update(params: Params, transition: ContainedViewLayoutTransition) {
|
||||
let hadItemViews = !self.itemViews.isEmpty
|
||||
let panelHeight: CGFloat = 44.0
|
||||
|
||||
var transition = transition
|
||||
if !hadItemViews {
|
||||
transition = .immediate
|
||||
let panelFrame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: panelHeight))
|
||||
let _ = self.panel.update(
|
||||
transition: ComponentTransition(transition),
|
||||
component: AnyComponent(ChatSideTopicsPanel(
|
||||
context: self.context,
|
||||
theme: params.interfaceState.theme,
|
||||
strings: params.interfaceState.strings,
|
||||
location: .top,
|
||||
peerId: self.peerId,
|
||||
isMonoforum: self.isMonoforum,
|
||||
topicId: params.interfaceState.chatLocation.threadId,
|
||||
controller: { [weak self] in
|
||||
return self?.interfaceInteraction?.chatController()
|
||||
},
|
||||
togglePanel: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.interfaceInteraction?.toggleChatSidebarMode()
|
||||
},
|
||||
updateTopicId: { [weak self] topicId, direction in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.interfaceInteraction?.updateChatLocationThread(topicId, direction ? .right : .left)
|
||||
}
|
||||
)),
|
||||
environment: {
|
||||
ChatSidePanelEnvironment(insets: UIEdgeInsets(
|
||||
top: 0.0,
|
||||
left: params.leftInset,
|
||||
bottom: 0.0,
|
||||
right: params.rightInset
|
||||
))
|
||||
},
|
||||
containerSize: panelFrame.size
|
||||
)
|
||||
if let panelView = self.panel.view {
|
||||
if panelView.superview == nil {
|
||||
panelView.disablesInteractiveTransitionGestureRecognizer = true
|
||||
self.view.addSubview(panelView)
|
||||
}
|
||||
transition.updateFrame(view: panelView, frame: panelFrame)
|
||||
}
|
||||
|
||||
let panelHeight: CGFloat = 44.0
|
||||
/*
|
||||
|
||||
let containerInsets = UIEdgeInsets(top: 0.0, left: params.leftInset + 16.0, bottom: 0.0, right: params.rightInset + 16.0)
|
||||
let itemSpacing: CGFloat = 24.0
|
||||
@ -1068,17 +839,24 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
||||
} else {
|
||||
self.appliedScrollToId = scrollToId
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
public func updateGlobalOffset(globalOffset: CGFloat, transition: ComponentTransition) {
|
||||
if let tabItemView = self.tabItemView {
|
||||
transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(0.0, -globalOffset, 0.0))
|
||||
if let panelView = self.panel.view as? ChatSideTopicsPanel.View {
|
||||
panelView.updateGlobalOffset(globalOffset: globalOffset, transition: transition)
|
||||
//transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(0.0, -globalOffset, 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
public func topicIndex(threadId: Int64?) -> Int? {
|
||||
if let threadId {
|
||||
if let panelView = self.panel.view as? ChatSideTopicsPanel.View {
|
||||
return panelView.topicIndex(threadId: threadId)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
/*if let threadId {
|
||||
if let value = self.items.firstIndex(where: { item in
|
||||
if item.id == .chatList(PeerId(threadId)) {
|
||||
return true
|
||||
@ -1094,6 +872,6 @@ final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, C
|
||||
}
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import PremiumUI
|
||||
import ChatControllerInteraction
|
||||
import ChatContextResultPeekContent
|
||||
import ChatInputContextPanelNode
|
||||
import BatchVideoRendering
|
||||
|
||||
private struct ChatContextResultStableId: Hashable {
|
||||
let result: ChatContextResult
|
||||
@ -48,8 +49,8 @@ private struct HorizontalListContextResultsChatInputContextPanelEntry: Comparabl
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(context: AccountContext, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> ListViewItem {
|
||||
return HorizontalListContextResultsChatInputPanelItem(context: context, theme: self.theme, result: self.result, resultSelected: resultSelected)
|
||||
func item(context: AccountContext, batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> ListViewItem {
|
||||
return HorizontalListContextResultsChatInputPanelItem(context: context, theme: self.theme, result: self.result, batchVideoContext: batchVideoContext, resultSelected: resultSelected)
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,12 +72,12 @@ private final class HorizontalListContextResultsOpaqueState {
|
||||
}
|
||||
}
|
||||
|
||||
private func preparedTransition(from fromEntries: [HorizontalListContextResultsChatInputContextPanelEntry], to toEntries: [HorizontalListContextResultsChatInputContextPanelEntry], hasMore: Bool, context: AccountContext, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> HorizontalListContextResultsChatInputContextPanelTransition {
|
||||
private func preparedTransition(from fromEntries: [HorizontalListContextResultsChatInputContextPanelEntry], to toEntries: [HorizontalListContextResultsChatInputContextPanelEntry], hasMore: Bool, context: AccountContext, batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> HorizontalListContextResultsChatInputContextPanelTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, resultSelected: resultSelected), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, resultSelected: resultSelected), directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, batchVideoContext: batchVideoContext, resultSelected: resultSelected), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, batchVideoContext: batchVideoContext, resultSelected: resultSelected), directionHint: nil) }
|
||||
|
||||
return HorizontalListContextResultsChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates, entryCount: toEntries.count, hasMore: hasMore)
|
||||
}
|
||||
@ -93,6 +94,8 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
|
||||
private var enqueuedTransitions: [(HorizontalListContextResultsChatInputContextPanelTransition, Bool)] = []
|
||||
private var hasValidLayout = false
|
||||
|
||||
private let batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>
|
||||
|
||||
override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) {
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
@ -108,6 +111,10 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
|
||||
return strings.VoiceOver_ScrollStatus(row, count).string
|
||||
}
|
||||
|
||||
self.batchVideoContext = QueueLocalObject(queue: .mainQueue(), generate: {
|
||||
return BatchVideoRenderingContext(context: context)
|
||||
})
|
||||
|
||||
super.init(context: context, theme: theme, strings: strings, fontSize: fontSize, chatPresentationContext: chatPresentationContext)
|
||||
|
||||
self.isOpaque = false
|
||||
@ -136,7 +143,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
|
||||
|
||||
self.listView.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
self.listView.view.disablesInteractiveKeyboardGestureRecognizer = true
|
||||
self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
|
||||
self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? in
|
||||
if let strongSelf = self {
|
||||
let convertedPoint = strongSelf.listView.view.convert(point, from: strongSelf.view)
|
||||
|
||||
@ -183,7 +190,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
|
||||
let controller = PremiumIntroScreen(context: strongSelf.context, source: .stickers)
|
||||
strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(controller)
|
||||
}))
|
||||
} else {
|
||||
} else if let batchVideoContext = strongSelf.batchVideoContext.unsafeGet() {
|
||||
var menuItems: [ContextMenuItem] = []
|
||||
if case let .internalReference(internalReference) = item.result, let file = internalReference.file, file.isAnimated {
|
||||
menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Preview_SaveGif, icon: { theme in
|
||||
@ -229,7 +236,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
|
||||
f(.default)
|
||||
let _ = item.resultSelected(item.result, itemNode, itemNode.bounds)
|
||||
})))
|
||||
selectedItemNodeAndContent = (itemNode.view, itemNode.bounds, ChatContextResultPeekContent(account: item.context.account, contextResult: item.result, menu: menuItems))
|
||||
selectedItemNodeAndContent = (itemNode.view, itemNode.bounds, ChatContextResultPeekContent(context: item.context, contextResult: item.result, menu: menuItems, batchVideoContext: batchVideoContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -312,7 +319,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
|
||||
}
|
||||
|
||||
let firstTime = self.currentEntries == nil
|
||||
let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, hasMore: results.nextOffset != nil, context: self.context, resultSelected: { [weak self] result, node, rect in
|
||||
let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, hasMore: results.nextOffset != nil, context: self.context, batchVideoContext: self.batchVideoContext, resultSelected: { [weak self] result, node, rect in
|
||||
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
||||
return interfaceInteraction.sendContextResult(results, result, node, rect)
|
||||
} else {
|
||||
|
@ -15,20 +15,23 @@ import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ShimmerEffect
|
||||
import SoftwareVideo
|
||||
import MultiplexedVideoNode
|
||||
import BatchVideoRendering
|
||||
import GifVideoLayer
|
||||
|
||||
final class HorizontalListContextResultsChatInputPanelItem: ListViewItem {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let result: ChatContextResult
|
||||
let batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>
|
||||
let resultSelected: (ChatContextResult, ASDisplayNode, CGRect) -> Bool
|
||||
|
||||
let selectable: Bool = true
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, result: ChatContextResult, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) {
|
||||
public init(context: AccountContext, theme: PresentationTheme, result: ChatContextResult, batchVideoContext: QueueLocalObject<BatchVideoRenderingContext>, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.result = result
|
||||
self.batchVideoContext = batchVideoContext
|
||||
self.resultSelected = resultSelected
|
||||
}
|
||||
|
||||
@ -90,7 +93,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
private let imageNode: TransformImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
private var placeholderNode: StickerShimmerEffectNode?
|
||||
private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
|
||||
private var videoLayer: GifVideoLayer?
|
||||
private var currentImageResource: TelegramMediaResource?
|
||||
private var currentVideoFile: TelegramMediaFile?
|
||||
private var currentAnimatedStickerFile: TelegramMediaFile?
|
||||
@ -103,58 +106,17 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
|
||||
override var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
switch visibility {
|
||||
case .visible:
|
||||
self.ticking = true
|
||||
default:
|
||||
self.ticking = false
|
||||
switch self.visibility {
|
||||
case .visible:
|
||||
self.videoLayer?.shouldBeAnimating = true
|
||||
case .none:
|
||||
self.videoLayer?.shouldBeAnimating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let timebase: CMTimebase
|
||||
|
||||
private var displayLink: CADisplayLink?
|
||||
private var ticking: Bool = false {
|
||||
didSet {
|
||||
if self.ticking != oldValue {
|
||||
if self.ticking {
|
||||
class DisplayLinkProxy: NSObject {
|
||||
weak var target: HorizontalListContextResultsChatInputPanelItemNode?
|
||||
init(target: HorizontalListContextResultsChatInputPanelItemNode) {
|
||||
self.target = target
|
||||
}
|
||||
|
||||
@objc func displayLinkEvent() {
|
||||
self.target?.displayLinkEvent()
|
||||
}
|
||||
}
|
||||
|
||||
let displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
|
||||
self.displayLink = displayLink
|
||||
displayLink.add(to: RunLoop.main, forMode: .common)
|
||||
if #available(iOS 10.0, *) {
|
||||
displayLink.preferredFramesPerSecond = 25
|
||||
} else {
|
||||
displayLink.frameInterval = 2
|
||||
}
|
||||
displayLink.isPaused = false
|
||||
CMTimebaseSetRate(self.timebase, rate: 1.0)
|
||||
} else if let displayLink = self.displayLink {
|
||||
self.displayLink = nil
|
||||
displayLink.isPaused = true
|
||||
displayLink.invalidate()
|
||||
CMTimebaseSetRate(self.timebase, rate: 0.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func displayLinkEvent() {
|
||||
let timestamp = CMTimebaseGetTime(self.timebase).seconds
|
||||
self.videoLayer?.1.tick(timestamp: timestamp)
|
||||
}
|
||||
|
||||
init() {
|
||||
self.imageNodeBackground = ASDisplayNode()
|
||||
self.imageNodeBackground.isLayerBacked = true
|
||||
@ -197,10 +159,6 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let displayLink = self.displayLink {
|
||||
displayLink.isPaused = true
|
||||
displayLink.invalidate()
|
||||
}
|
||||
self.statusDisposable.dispose()
|
||||
self.fetchDisposable.dispose()
|
||||
}
|
||||
@ -384,30 +342,25 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
}
|
||||
|
||||
if updatedVideoFile {
|
||||
if let (thumbnailLayer, _, layer) = strongSelf.videoLayer {
|
||||
if let videoLayer = strongSelf.videoLayer {
|
||||
strongSelf.videoLayer = nil
|
||||
thumbnailLayer.removeFromSupernode()
|
||||
layer.layer.removeFromSuperlayer()
|
||||
videoLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
if let videoFile = videoFile {
|
||||
let thumbnailLayer = SoftwareVideoThumbnailNode(account: item.context.account, fileReference: .standalone(media: videoFile), synchronousLoad: synchronousLoads)
|
||||
thumbnailLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
strongSelf.addSubnode(thumbnailLayer)
|
||||
let layerHolder = takeSampleBufferLayer()
|
||||
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
||||
layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
strongSelf.layer.addSublayer(layerHolder.layer)
|
||||
if let videoFile, let batchVideoContext = item.batchVideoContext.unsafeGet() {
|
||||
let videoLayer = GifVideoLayer(
|
||||
context: item.context,
|
||||
batchVideoContext: batchVideoContext,
|
||||
userLocation: .other,
|
||||
file: .standalone(media: videoFile),
|
||||
synchronousLoad: synchronousLoads
|
||||
)
|
||||
videoLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
||||
videoLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
strongSelf.layer.addSublayer(videoLayer)
|
||||
|
||||
let manager = SoftwareVideoLayerFrameManager(account: item.context.account, userLocation: .other, userContentType: .other, fileReference: .standalone(media: videoFile), layerHolder: layerHolder)
|
||||
strongSelf.videoLayer = (thumbnailLayer, manager, layerHolder)
|
||||
thumbnailLayer.ready = { [weak thumbnailLayer, weak manager] in
|
||||
if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager {
|
||||
if strongSelf.videoLayer?.0 === thumbnailLayer && strongSelf.videoLayer?.1 === manager {
|
||||
manager.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
strongSelf.videoLayer = videoLayer
|
||||
videoLayer.shouldBeAnimating = strongSelf.visibility != .none
|
||||
}
|
||||
}
|
||||
|
||||
@ -477,11 +430,9 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
strongSelf.statusNode.transitionToState(.none, completion: { })
|
||||
}
|
||||
|
||||
if let (thumbnailLayer, _, layer) = strongSelf.videoLayer {
|
||||
thumbnailLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
|
||||
thumbnailLayer.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset)
|
||||
layer.layer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
|
||||
layer.layer.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset)
|
||||
if let videoLayer = strongSelf.videoLayer {
|
||||
videoLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
|
||||
videoLayer.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset)
|
||||
}
|
||||
|
||||
if let animationNode = strongSelf.animationNode {
|
||||
|
@ -85,6 +85,7 @@ import GiftStoreScreen
|
||||
import SendInviteLinkScreen
|
||||
import PostSuggestionsSettingsScreen
|
||||
import ForumSettingsScreen
|
||||
import ForumCreateTopicScreen
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
@ -2609,6 +2610,25 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}
|
||||
}
|
||||
|
||||
public func makeEditForumTopicScreen(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, threadInfo: EngineMessageHistoryThread.Info, isHidden: Bool) -> ViewController {
|
||||
let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .edit(threadId: threadId, threadInfo: threadInfo, isHidden: isHidden))
|
||||
controller.navigationPresentation = .modal
|
||||
controller.completion = { [weak controller] title, fileId, _, isHidden in
|
||||
let _ = (context.engine.peers.editForumChannelTopic(id: peerId, threadId: threadId, title: title, iconFileId: fileId)
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
controller?.dismiss()
|
||||
})
|
||||
|
||||
if let isHidden {
|
||||
let _ = (context.engine.peers.setForumChannelTopicHidden(id: peerId, threadId: threadId, isHidden: isHidden)
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
controller?.dismiss()
|
||||
})
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
||||
private func mapIntroSource(source: PremiumIntroSource) -> PremiumSource {
|
||||
let mappedSource: PremiumSource
|
||||
switch source {
|
||||
|
Loading…
x
Reference in New Issue
Block a user