Monoforums

This commit is contained in:
Isaac 2025-05-26 22:07:58 +08:00
parent a464698638
commit 40e26fc25c
25 changed files with 3000 additions and 1052 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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