From 28e35250910d0818fbaac769974ca1d886a6b3b9 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 26 Nov 2023 18:52:39 +0400 Subject: [PATCH 1/5] Various fixes --- .../DrawingUI/Sources/DrawingScreen.swift | 12 +- .../Sources/MessageStatsController.swift | 29 +- .../Sources/MessageStatsOverviewItem.swift | 317 ------------------ .../Sources/StatsOverviewItem.swift | 12 +- .../Sources/Statistics/StoryStatistics.swift | 124 +++---- .../Messages/Transcription.swift | 33 +- .../Sources/MediaEditorRecording.swift | 31 +- .../Sources/MediaEditorScreen.swift | 83 ++++- .../StoryItemSetContainerComponent.swift | 14 +- .../Sources/StoryFooterPanelComponent.swift | 239 +++++++------ .../Sources/ChatControllerNode.swift | 2 +- .../Sources/TelegramRootController.swift | 26 ++ 12 files changed, 352 insertions(+), 570 deletions(-) delete mode 100644 submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 32d750bfcc..385984dbaa 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -2460,6 +2460,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U }, onTextEditingEnded: { _ in }, editEntity: { _ in }, + shouldDeleteEntity: { _ in + return true + }, getCurrentImage: { [weak controller] in return controller?.getCurrentImage() }, @@ -2981,7 +2984,8 @@ public final class DrawingToolsInteraction { private let onInteractionUpdated: (Bool) -> Void private let onTextEditingEnded: (Bool) -> Void private let editEntity: (DrawingEntity) -> Void - + private let shouldDeleteEntity: (DrawingEntity) -> Bool + public let getCurrentImage: () -> UIImage? private let getControllerNode: () -> ASDisplayNode? private let present: (ViewController, PresentationContextType, Any?) -> Void @@ -3012,6 +3016,7 @@ public final class DrawingToolsInteraction { onInteractionUpdated: @escaping (Bool) -> Void, onTextEditingEnded: @escaping (Bool) -> Void, editEntity: @escaping (DrawingEntity) -> Void, + shouldDeleteEntity: @escaping (DrawingEntity) -> Bool, getCurrentImage: @escaping () -> UIImage?, getControllerNode: @escaping () -> ASDisplayNode?, present: @escaping (ViewController, PresentationContextType, Any?) -> Void, @@ -3030,6 +3035,7 @@ public final class DrawingToolsInteraction { self.onInteractionUpdated = onInteractionUpdated self.onTextEditingEnded = onTextEditingEnded self.editEntity = editEntity + self.shouldDeleteEntity = shouldDeleteEntity self.getCurrentImage = getCurrentImage self.getControllerNode = getControllerNode self.present = present @@ -3088,7 +3094,9 @@ public final class DrawingToolsInteraction { var actions: [ContextMenuAction] = [] actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Delete, accessibilityLabel: presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in if let self, let entityView { - self.entitiesView.remove(uuid: entityView.entity.uuid, animated: true) + if self.shouldDeleteEntity(entityView.entity) { + self.entitiesView.remove(uuid: entityView.entity.uuid, animated: true) + } } })) if let entityView = entityView as? DrawingLocationEntityView { diff --git a/submodules/StatisticsUI/Sources/MessageStatsController.swift b/submodules/StatisticsUI/Sources/MessageStatsController.swift index d12350fffc..3be5c9a213 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsController.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsController.swift @@ -36,7 +36,7 @@ private enum StatsSection: Int32 { private enum StatsEntry: ItemListNodeEntry { case overviewTitle(PresentationTheme, String) - case overview(PresentationTheme, PostStats, Int32?) + case overview(PresentationTheme, PostStats, EngineStoryItem.Views?, Int32?) case interactionsTitle(PresentationTheme, String) case interactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Bool) @@ -89,8 +89,8 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } - case let .overview(lhsTheme, lhsStats, lhsPublicShares): - if case let .overview(rhsTheme, rhsStats, rhsPublicShares) = rhs, lhsTheme === rhsTheme, lhsPublicShares == rhsPublicShares { + case let .overview(lhsTheme, lhsStats, lhsViews, lhsPublicShares): + if case let .overview(rhsTheme, rhsStats, rhsViews, rhsPublicShares) = rhs, lhsTheme === rhsTheme, lhsViews == rhsViews, lhsPublicShares == rhsPublicShares { if let lhsMessageStats = lhsStats as? MessageStats, let rhsMessageStats = rhsStats as? MessageStats { return lhsMessageStats == rhsMessageStats } else if let lhsStoryStats = lhsStats as? StoryStats, let rhsStoryStats = rhsStats as? StoryStats { @@ -152,8 +152,8 @@ private enum StatsEntry: ItemListNodeEntry { let .reactionsTitle(_, text), let .publicForwardsTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .overview(_, stats, publicShares): - return StatsOverviewItem(presentationData: presentationData, stats: stats as! Stats, publicShares: publicShares, sectionId: self.section, style: .blocks) + case let .overview(_, stats, storyViews, publicShares): + return StatsOverviewItem(presentationData: presentationData, stats: stats as! Stats, storyViews: storyViews, publicShares: publicShares, sectionId: self.section, style: .blocks) case let .interactionsGraph(_, _, _, graph, type, noInitialZoom), let .reactionsGraph(_, _, _, graph, type, noInitialZoom): return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, noInitialZoom: noInitialZoom, getDetailsData: { date, completion in let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in @@ -179,12 +179,19 @@ private enum StatsEntry: ItemListNodeEntry { } } -private func messageStatsControllerEntries(data: PostStats?, messages: SearchMessagesResult?, forwards: StoryStatsPublicForwardsContext.State?, presentationData: PresentationData) -> [StatsEntry] { +private func messageStatsControllerEntries(data: PostStats?, storyViews: EngineStoryItem.Views?, messages: SearchMessagesResult?, forwards: StoryStatsPublicForwardsContext.State?, presentationData: PresentationData) -> [StatsEntry] { var entries: [StatsEntry] = [] if let data = data { entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_MessageOverview.uppercased())) - entries.append(.overview(presentationData.theme, data, messages?.totalCount)) + + var publicShares: Int32? + if let messages { + publicShares = messages.totalCount + } else if let forwards { + publicShares = forwards.count + } + entries.append(.overview(presentationData.theme, data, storyViews, publicShares)) var isStories = false if let _ = data as? StoryStats { @@ -244,8 +251,6 @@ public enum StatsSubject { } protocol PostStats { - var views: Int { get } - var forwards: Int { get } var interactionsGraph: StatsGraph { get } var interactionsGraphDelta: Int64 { get } var reactionsGraph: StatsGraph { get } @@ -378,15 +383,17 @@ public func messageStatsController(context: AccountContext, updatedPresentationD } let title: String + var storyViews: EngineStoryItem.Views? switch subject { case .message: title = presentationData.strings.Stats_MessageTitle - case .story: + case let .story(_, _, storyItem): title = presentationData.strings.Stats_StoryTitle + storyViews = storyItem?.views } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: iconNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: { }) }, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: messageStatsControllerEntries(data: data, messages: search?.0, forwards: forwards, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: messageStatsControllerEntries(data: data, storyViews: storyViews, messages: search?.0, forwards: forwards, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift b/submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift deleted file mode 100644 index 16460e3501..0000000000 --- a/submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift +++ /dev/null @@ -1,317 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import ItemListUI -import PresentationDataUtils - -final class MessageStatsOverviewItem: ListViewItem, ItemListItem { - let presentationData: ItemListPresentationData - let stats: PostStats - let publicShares: Int32? - let reactions: Int32 - let sectionId: ItemListSectionId - let style: ItemListStyle - - init(presentationData: ItemListPresentationData, stats: PostStats, publicShares: Int32?, reactions: Int32, sectionId: ItemListSectionId, style: ItemListStyle) { - self.presentationData = presentationData - self.stats = stats - self.publicShares = publicShares - self.reactions = reactions - self.sectionId = sectionId - self.style = style - } - - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - async { - let node = MessageStatsOverviewItemNode() - let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - Queue.mainQueue().async { - completion(node, { - return (nil, { _ in apply() }) - }) - } - } - } - - 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 { - if let nodeValue = node() as? MessageStatsOverviewItemNode { - let makeLayout = nodeValue.asyncLayout() - - async { - let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - Queue.mainQueue().async { - completion(layout, { _ in - apply() - }) - } - } - } - } - } - - var selectable: Bool = false -} - -class MessageStatsOverviewItemNode: ListViewItemNode { - private let backgroundNode: ASDisplayNode - private let topStripeNode: ASDisplayNode - private let bottomStripeNode: ASDisplayNode - private let maskNode: ASImageNode - - private let leftValueLabel: ImmediateTextNode - private let centerValueLabel: ImmediateTextNode - private let rightValueLabel: ImmediateTextNode - - private let leftTitleLabel: ImmediateTextNode - private let centerTitleLabel: ImmediateTextNode - private let rightTitleLabel: ImmediateTextNode - - private var item: MessageStatsOverviewItem? - - init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white - - self.topStripeNode = ASDisplayNode() - self.topStripeNode.isLayerBacked = true - - self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.isLayerBacked = true - - self.maskNode = ASImageNode() - - self.leftValueLabel = ImmediateTextNode() - self.centerValueLabel = ImmediateTextNode() - self.rightValueLabel = ImmediateTextNode() - - self.leftTitleLabel = ImmediateTextNode() - self.centerTitleLabel = ImmediateTextNode() - self.rightTitleLabel = ImmediateTextNode() - - super.init(layerBacked: false, dynamicBounce: false) - - self.clipsToBounds = true - - self.addSubnode(self.leftValueLabel) - self.addSubnode(self.centerValueLabel) - self.addSubnode(self.rightValueLabel) - - self.addSubnode(self.leftTitleLabel) - self.addSubnode(self.centerTitleLabel) - self.addSubnode(self.rightTitleLabel) - } - - func asyncLayout() -> (_ item: MessageStatsOverviewItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeLeftValueLabelLayout = TextNode.asyncLayout(self.leftValueLabel) - let makeRightValueLabelLayout = TextNode.asyncLayout(self.rightValueLabel) - let makeCenterValueLabelLayout = TextNode.asyncLayout(self.centerValueLabel) - - let makeLeftTitleLabelLayout = TextNode.asyncLayout(self.leftTitleLabel) - let makeRightTitleLabelLayout = TextNode.asyncLayout(self.rightTitleLabel) - let makeCenterTitleLabelLayout = TextNode.asyncLayout(self.centerTitleLabel) - - let currentItem = self.item - - return { item, params, neighbors in - let insets: UIEdgeInsets - let separatorHeight = UIScreenPixel - let itemBackgroundColor: UIColor - let itemSeparatorColor: UIColor - - let topInset: CGFloat = 14.0 - let sideInset: CGFloat = 16.0 - - var height: CGFloat = topInset * 2.0 - - let leftInset = params.leftInset - let rightInset = params.rightInset - var updatedTheme: PresentationTheme? - - if currentItem?.presentationData.theme !== item.presentationData.theme { - updatedTheme = item.presentationData.theme - } - - switch item.style { - case .plain: - itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor - itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor - insets = itemListNeighborsPlainInsets(neighbors) - case .blocks: - itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor - insets = itemListNeighborsGroupedInsets(neighbors, params) - } - - let valueFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize) - let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) - - let leftValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))? - let rightValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))? - let centerValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))? - - let leftTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))? - let rightTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))? - let centerTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))? - - let centerTitle: String - let centerValue: String - if let _ = item.stats as? StoryStats { - centerTitle = "Reactions" - centerValue = compactNumericCountString(Int(item.reactions)) - } else { - centerTitle = item.presentationData.strings.Stats_Message_PublicShares - centerValue = item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–" - } - - leftValueLabelLayoutAndApply = makeLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(item.stats.views), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - - centerValueLabelLayoutAndApply = makeCenterValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: centerValue, font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - - rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, item.stats.forwards - Int($0))))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - - var remainingWidth: CGFloat = params.width - leftInset - rightInset - sideInset * 2.0 - let maxItemWidth: CGFloat = floor(remainingWidth / 2.8) - - leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_Views, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - remainingWidth -= leftTitleLabelLayoutAndApply!.0.size.width - 4.0 - - - - centerTitleLabelLayoutAndApply = makeCenterTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: centerTitle, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - remainingWidth -= centerTitleLabelLayoutAndApply!.0.size.width - 4.0 - - rightTitleLabelLayoutAndApply = makeRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_PrivateShares, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - - var maxLabelHeight = rightTitleLabelLayoutAndApply!.0.size.height - maxLabelHeight = max(maxLabelHeight, centerTitleLabelLayoutAndApply!.0.size.height) - maxLabelHeight = max(maxLabelHeight, leftTitleLabelLayoutAndApply!.0.size.height) - - height += rightValueLabelLayoutAndApply!.0.size.height + maxLabelHeight - - let contentSize = CGSize(width: params.width, height: height) - return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in - if let strongSelf = self { - strongSelf.item = item - - let _ = leftValueLabelLayoutAndApply?.1() - let _ = centerValueLabelLayoutAndApply?.1() - let _ = rightValueLabelLayoutAndApply?.1() - let _ = leftTitleLabelLayoutAndApply?.1() - let _ = centerTitleLabelLayoutAndApply?.1() - let _ = rightTitleLabelLayoutAndApply?.1() - - if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - } - - switch item.style { - case .plain: - if strongSelf.backgroundNode.supernode != nil { - strongSelf.backgroundNode.removeFromSupernode() - } - if strongSelf.topStripeNode.supernode != nil { - strongSelf.topStripeNode.removeFromSupernode() - } - if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) - } - if strongSelf.maskNode.supernode != nil { - strongSelf.maskNode.removeFromSupernode() - } - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) - case .blocks: - if strongSelf.backgroundNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) - } - if strongSelf.topStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) - } - if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) - } - if strongSelf.maskNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.maskNode, at: 3) - } - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners - } - let bottomStripeInset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = leftInset - strongSelf.bottomStripeNode.isHidden = false - default: - bottomStripeInset = 0.0 - hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners - } - - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) - } - - let maxLeftWidth = max(leftValueLabelLayoutAndApply?.0.size.width ?? 0.0, leftTitleLabelLayoutAndApply?.0.size.width ?? 0.0) - let maxCenterWidth = max(centerValueLabelLayoutAndApply?.0.size.width ?? 0.0, centerTitleLabelLayoutAndApply?.0.size.width ?? 0.0) - let maxRightWidth = max(rightValueLabelLayoutAndApply?.0.size.width ?? 0.0, rightTitleLabelLayoutAndApply?.0.size.width ?? 0.0) - - let horizontalSpacing = max(1.0, min(60, (params.width - leftInset - rightInset - sideInset * 2.0 - maxLeftWidth - maxCenterWidth - maxRightWidth) / 2.0)) - - var x: CGFloat = leftInset + (params.width - leftInset - rightInset - maxLeftWidth - maxCenterWidth - maxRightWidth - horizontalSpacing * 2.0) / 2.0 - if let leftValueLabelLayout = leftValueLabelLayoutAndApply?.0, let leftTitleLabelLayout = leftTitleLabelLayoutAndApply?.0 { - strongSelf.leftValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: leftValueLabelLayout.size) - strongSelf.leftTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.leftValueLabel.frame.maxY), size: leftTitleLabelLayout.size) - x += max(leftValueLabelLayout.size.width, leftTitleLabelLayout.size.width) + horizontalSpacing - } - - if let centerValueLabelLayout = centerValueLabelLayoutAndApply?.0, let centerTitleLabelLayout = centerTitleLabelLayoutAndApply?.0 { - strongSelf.centerValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: centerValueLabelLayout.size) - strongSelf.centerTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.centerValueLabel.frame.maxY), size: centerTitleLabelLayout.size) - x += max(centerValueLabelLayout.size.width, centerTitleLabelLayout.size.width) + horizontalSpacing - } - - if let rightValueLabelLayout = rightValueLabelLayoutAndApply?.0, let rightTitleLabelLayout = rightTitleLabelLayoutAndApply?.0 { - strongSelf.rightValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: rightValueLabelLayout.size) - strongSelf.rightTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.rightValueLabel.frame.maxY), size: rightTitleLabelLayout.size) - } - } - }) - } - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - } - - override func animateAdded(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - } -} - diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index e0ccfa73cf..eaee3235e4 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -35,13 +35,15 @@ extension StoryStats: Stats { class StatsOverviewItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData let stats: Stats + let storyViews: EngineStoryItem.Views? let publicShares: Int32? let sectionId: ItemListSectionId let style: ItemListStyle - init(presentationData: ItemListPresentationData, stats: Stats, publicShares: Int32? = nil, sectionId: ItemListSectionId, style: ItemListStyle) { + init(presentationData: ItemListPresentationData, stats: Stats, storyViews: EngineStoryItem.Views? = nil, publicShares: Int32? = nil, sectionId: ItemListSectionId, style: ItemListStyle) { self.presentationData = presentationData self.stats = stats + self.storyViews = storyViews self.publicShares = publicShares self.sectionId = sectionId self.style = style @@ -349,11 +351,11 @@ class StatsOverviewItemNode: ListViewItemNode { ) height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing - } else if let stats = item.stats as? StoryStats { + } else if let _ = item.stats as? StoryStats, let views = item.storyViews { topLeftItemLayoutAndApply = makeTopLeftItemLayout( params.width, item.presentationData, - compactNumericCountString(stats.views), + compactNumericCountString(views.seenCount), item.presentationData.strings.Stats_Message_Views, nil ) @@ -369,7 +371,7 @@ class StatsOverviewItemNode: ListViewItemNode { middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( params.width, item.presentationData, - compactNumericCountString(stats.reactions), + compactNumericCountString(views.reactedCount), item.presentationData.strings.Stats_Message_Reactions, nil ) @@ -377,7 +379,7 @@ class StatsOverviewItemNode: ListViewItemNode { middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout( params.width, item.presentationData, - compactNumericCountString(stats.forwards), + compactNumericCountString(views.forwardCount), item.presentationData.strings.Stats_Message_PrivateShares, nil ) diff --git a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift index 3b424d551b..dd66dfe2af 100644 --- a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift @@ -5,32 +5,17 @@ import TelegramApi import MtProtoKit public struct StoryStats: Equatable { - public let views: Int - public let forwards: Int - public let reactions: Int public let interactionsGraph: StatsGraph public let interactionsGraphDelta: Int64 public let reactionsGraph: StatsGraph - init(views: Int, forwards: Int, reactions: Int, interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) { - self.views = views - self.forwards = forwards - self.reactions = reactions + init(interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) { self.interactionsGraph = interactionsGraph self.interactionsGraphDelta = interactionsGraphDelta self.reactionsGraph = reactionsGraph } public static func == (lhs: StoryStats, rhs: StoryStats) -> Bool { - if lhs.views != rhs.views { - return false - } - if lhs.forwards != rhs.forwards { - return false - } - if lhs.reactions != rhs.reactions { - return false - } if lhs.interactionsGraph != rhs.interactionsGraph { return false } @@ -44,7 +29,7 @@ public struct StoryStats: Equatable { } public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> StoryStats { - return StoryStats(views: self.views, forwards: self.forwards, reactions: self.reactions, interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta, reactionsGraph: self.reactionsGraph) + return StoryStats(interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta, reactionsGraph: self.reactionsGraph) } } @@ -61,76 +46,57 @@ private func requestStoryStats(accountPeerId: PeerId, postbox: Postbox, network: } } |> mapToSignal { data -> Signal in - guard let (statsDatacenterId, peer) = data, let peerReference = PeerReference(peer) else { + guard let (statsDatacenterId, peer) = data, let inputPeer = apiInputPeer(peer) else { return .never() } - return _internal_getStoriesById(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peerReference, ids: [storyId]) - |> mapToSignal { stories -> Signal in - guard let storyItem = stories.first, case let .item(story) = storyItem, let inputPeer = apiInputPeer(peer) else { - return .never() + var flags: Int32 = 0 + if dark { + flags |= (1 << 1) + } + + let request = Api.functions.stats.getStoryStats(flags: flags, peer: inputPeer, id: storyId) + let signal: Signal + if network.datacenterId != statsDatacenterId { + signal = network.download(datacenterId: Int(statsDatacenterId), isMedia: false, tag: nil) + |> castError(MTRpcError.self) + |> mapToSignal { worker in + return worker.request(request) } - - var flags: Int32 = 0 - if dark { - flags |= (1 << 1) - } - - let request = Api.functions.stats.getStoryStats(flags: flags, peer: inputPeer, id: storyId) - let signal: Signal - if network.datacenterId != statsDatacenterId { - signal = network.download(datacenterId: Int(statsDatacenterId), isMedia: false, tag: nil) - |> castError(MTRpcError.self) - |> mapToSignal { worker in - return worker.request(request) - } - } else { - signal = network.request(request) - } - - var views: Int = 0 - var forwards: Int = 0 - var reactions: Int = 0 - if let storyViews = story.views { - views = storyViews.seenCount - forwards = storyViews.forwardCount - reactions = storyViews.reactedCount - } - - return signal - |> mapToSignal { result -> Signal in - if case let .storyStats(apiInteractionsGraph, apiReactionsGraph) = result { - let interactionsGraph = StatsGraph(apiStatsGraph: apiInteractionsGraph) - var interactionsGraphDelta: Int64 = 86400 - if case let .Loaded(_, data) = interactionsGraph { - if let start = data.range(of: "[\"x\",") { - let substring = data.suffix(from: start.upperBound) - if let end = substring.range(of: "],") { - let valuesString = substring.prefix(through: substring.index(before: end.lowerBound)) - let values = valuesString.components(separatedBy: ",").compactMap { Int64($0) } - if values.count > 1 { - let first = values[0] - let second = values[1] - let delta = abs(second - first) / 1000 - interactionsGraphDelta = delta - } + } else { + signal = network.request(request) + } + + return signal + |> mapToSignal { result -> Signal in + if case let .storyStats(apiInteractionsGraph, apiReactionsGraph) = result { + let interactionsGraph = StatsGraph(apiStatsGraph: apiInteractionsGraph) + var interactionsGraphDelta: Int64 = 86400 + if case let .Loaded(_, data) = interactionsGraph { + if let start = data.range(of: "[\"x\",") { + let substring = data.suffix(from: start.upperBound) + if let end = substring.range(of: "],") { + let valuesString = substring.prefix(through: substring.index(before: end.lowerBound)) + let values = valuesString.components(separatedBy: ",").compactMap { Int64($0) } + if values.count > 1 { + let first = values[0] + let second = values[1] + let delta = abs(second - first) / 1000 + interactionsGraphDelta = delta } } } - let reactionsGraph = StatsGraph(apiStatsGraph: apiReactionsGraph) - return .single(StoryStats( - views: views, - forwards: forwards, - reactions: reactions, - interactionsGraph: interactionsGraph, - interactionsGraphDelta: interactionsGraphDelta, - reactionsGraph: reactionsGraph - )) - } else { - return .single(nil) } + let reactionsGraph = StatsGraph(apiStatsGraph: apiReactionsGraph) + return .single(StoryStats( + interactionsGraph: interactionsGraph, + interactionsGraphDelta: interactionsGraphDelta, + reactionsGraph: reactionsGraph + )) + } else { + return .single(nil) } - |> retryRequest } + |> retryRequest } } @@ -255,8 +221,6 @@ private final class StoryStatsPublicForwardsContextImpl { self.count = 0 - self.isLoadingMore = true - self.loadMore() } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Transcription.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Transcription.swift index 50356fb4c5..ec7911a7b4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Transcription.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Transcription.swift @@ -9,6 +9,12 @@ public enum EngineAudioTranscriptionResult { case error } +private enum InternalAudioTranscriptionResult { + case success(Api.messages.TranscribedAudio) + case error(AudioTranscriptionMessageAttribute.TranscriptionError) + case limitExceeded(Int32) +} + func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: MessageId) -> Signal { return postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) @@ -18,17 +24,24 @@ func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: Me return .single(.error) } return network.request(Api.functions.messages.transcribeAudio(peer: inputPeer, msgId: messageId.id)) - |> map { result -> Result in + |> map { result -> InternalAudioTranscriptionResult in return .success(result) } - |> `catch` { error -> Signal, NoError> in + |> `catch` { error -> Signal in let mappedError: AudioTranscriptionMessageAttribute.TranscriptionError - if error.errorDescription == "MSG_VOICE_TOO_LONG" { + if error.errorDescription.hasPrefix("FLOOD_WAIT_") { + if let range = error.errorDescription.range(of: "_", options: .backwards) { + if let value = Int32(error.errorDescription[range.upperBound...]) { + return .single(.limitExceeded(value)) + } + } + mappedError = .generic + } else if error.errorDescription == "MSG_VOICE_TOO_LONG" { mappedError = .tooLong } else { mappedError = .generic } - return .single(.failure(mappedError)) + return .single(.error(mappedError)) } |> mapToSignal { result -> Signal in return postbox.transaction { transaction -> EngineAudioTranscriptionResult in @@ -38,6 +51,7 @@ func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: Me switch transcribedAudio { case let .transcribedAudio(flags, transcriptionId, text, trialRemainingCount, trialUntilDate): let isPending = (flags & (1 << 0)) != 0 + updatedAttribute = AudioTranscriptionMessageAttribute(id: transcriptionId, text: text, isPending: isPending, didRate: false, error: nil) _internal_updateAudioTranscriptionTrialState(transaction: transaction) { current in var updated = current @@ -50,10 +64,17 @@ func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: Me } return updated } - updatedAttribute = AudioTranscriptionMessageAttribute(id: transcriptionId, text: text, isPending: isPending, didRate: false, error: nil) } - case let .failure(error): + case let .error(error): updatedAttribute = AudioTranscriptionMessageAttribute(id: 0, text: "", isPending: false, didRate: false, error: error) + case let .limitExceeded(timeout): + let cooldownTime = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + timeout + _internal_updateAudioTranscriptionTrialState(transaction: transaction) { current in + var updated = current + updated = updated.withUpdatedCooldownUntilTime(cooldownTime) + return updated + } + return .error } transaction.updateMessage(messageId, update: { currentMessage in diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift index 94e2596889..595de405f2 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift @@ -23,36 +23,17 @@ extension MediaEditorScreen { guard let controller, let mediaEditor = controller.node.mediaEditor else { return } - let entitiesView = controller.node.entitiesView if mediaEditor.values.additionalVideoPath != nil { - let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } - let alertController = textAlertController( - context: controller.context, - forceTheme: defaultDarkColorPresentationTheme, - title: nil, - text: presentationData.strings.MediaEditor_VideoRemovalConfirmation, - actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - }), - TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { [weak mediaEditor, weak entitiesView] in - mediaEditor?.setAdditionalVideo(nil, positionChanges: []) - if let entityView = entitiesView?.getView(where: { entityView in - if let entity = entityView.entity as? DrawingStickerEntity, case .dualVideoReference = entity.content { - return true - } else { - return false - } - }) { - entitiesView?.remove(uuid: entityView.entity.uuid, animated: false) - } - }) - ] - ) - controller.present(alertController, in: .window(.root)) + controller.node.presentVideoRemoveConfirmation() return } if isActive { + if controller.cameraAuthorizationStatus != .allowed || controller.microphoneAuthorizationStatus != .allowed { + controller.requestDeviceAccess() + return + } + guard self.recorder == nil else { return } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 312c8a663b..c4f089e5b2 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2454,6 +2454,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } }, + shouldDeleteEntity: { [weak self] entity in + if let self { + if let stickerEntity = entity as? DrawingStickerEntity, case .dualVideoReference(true) = stickerEntity.content { + self.presentVideoRemoveConfirmation() + return false + } + } + return true + }, getCurrentImage: { [weak self] in guard let mediaEditor = self?.mediaEditor else { return nil @@ -3396,10 +3405,41 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }), in: .window(.root)) } + func presentVideoRemoveConfirmation() { + guard let controller = self.controller else { + return + } + let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } + let alertController = textAlertController( + context: controller.context, + forceTheme: defaultDarkColorPresentationTheme, + title: nil, + text: presentationData.strings.MediaEditor_VideoRemovalConfirmation, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { [weak mediaEditor, weak entitiesView] in + mediaEditor?.setAdditionalVideo(nil, positionChanges: []) + if let entityView = entitiesView?.getView(where: { entityView in + if let entity = entityView.entity as? DrawingStickerEntity, case .dualVideoReference = entity.content { + return true + } else { + return false + } + }) { + entitiesView?.remove(uuid: entityView.entity.uuid, animated: false) + } + }) + ] + ) + controller.present(alertController, in: .window(.root)) + } + func presentTrackOptions(trackId: Int32, sourceView: UIView) { let value = self.mediaEditor?.values.audioTrackVolume ?? 1.0 - let actionTitle: String = trackId == 2 ? self.presentationData.strings.MediaEditor_RemoveAudio : self.presentationData.strings.MediaEditor_RemoveVideo + let isVideo = trackId != 2 + let actionTitle: String = isVideo ? self.presentationData.strings.MediaEditor_RemoveVideo : self.presentationData.strings.MediaEditor_RemoveAudio let items: [ContextMenuItem] = [ .custom(VolumeSliderContextItem(minValue: 0.0, value: value, valueChanged: { [weak self] value, _ in @@ -3416,19 +3456,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let self { if let mediaEditor = self.mediaEditor { if trackId == 1 { - mediaEditor.setAdditionalVideo(nil, positionChanges: []) - if let entityView = self.entitiesView.getView(where: { entityView in - if let entity = entityView.entity as? DrawingStickerEntity, case .dualVideoReference = entity.content { - return true - } else { - return false - } - }) { - self.entitiesView.remove(uuid: entityView.entity.uuid, animated: false) - } + self.presentVideoRemoveConfirmation() } else { mediaEditor.setAudioTrack(nil) - if !mediaEditor.sourceIsVideo && !mediaEditor.isPlaying { mediaEditor.play() } @@ -4013,7 +4043,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var audioSessionDisposable: Disposable? private let postingAvailabilityPromise = Promise() private var postingAvailabilityDisposable: Disposable? - + + private var authorizationStatusDisposables = DisposableSet() + private(set) var cameraAuthorizationStatus: AccessType = .notDetermined + private(set) var microphoneAuthorizationStatus: AccessType = .notDetermined + public init( context: AccountContext, subject: Signal, @@ -4088,6 +4122,20 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let _ = forwardSource { self.postingAvailabilityPromise.set(self.context.engine.messages.checkStoriesUploadAvailability(target: .myStories)) } + + self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .camera(.video)) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let self { + self.cameraAuthorizationStatus = status + } + })) + + self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .microphone(.video)) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let self { + self.microphoneAuthorizationStatus = status + } + })) } required public init(coder aDecoder: NSCoder) { @@ -4098,6 +4146,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.exportDisposable.dispose() self.audioSessionDisposable?.dispose() self.postingAvailabilityDisposable?.dispose() + self.authorizationStatusDisposables.dispose() } override public func loadDisplayNode() { @@ -4176,6 +4225,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate var isEmbeddedEditor: Bool { return self.isEditingStory || self.forwardSource != nil } + + func requestDeviceAccess() { + DeviceAccess.authorizeAccess(to: .camera(.video), { granted in + if granted { + DeviceAccess.authorizeAccess(to: .microphone(.video)) + } + }) + } func openPrivacySettings(_ privacy: MediaEditorResultPrivacy? = nil, completion: @escaping () -> Void = {}) { self.node.mediaEditor?.maybePauseVideo() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 6593a7bc64..f2f8a98c13 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1647,10 +1647,14 @@ public final class StoryItemSetContainerComponent: Component { } var isChannel = false + var canShare = true var displayFooter = false - if case .channel = component.slice.peer { + if case let .channel(channel) = component.slice.peer { displayFooter = true isChannel = true + if channel.addressName == nil { + canShare = false + } } else if component.slice.peer.id == component.context.account.peerId { displayFooter = true } else if component.slice.item.storyItem.isPending { @@ -1719,6 +1723,7 @@ public final class StoryItemSetContainerComponent: Component { return StoryFooterPanelComponent.MyReaction(reaction: value, file: centerAnimation, animationFileId: animationFileId) }, isChannel: isChannel, + canShare: canShare, externalViews: nil, expandFraction: footerExpandFraction, expandViewStats: { [weak self] in @@ -1802,6 +1807,13 @@ public final class StoryItemSetContainerComponent: Component { return } self.openStoryEditing(repost: true) + }, + cancelUploadAction: { [weak self] in + guard let self, let component = self.component, let controller = self.component?.controller() as? StoryContainerScreen else { + return + } + component.context.engine.messages.cancelStoryUpload(stableId: component.slice.item.storyItem.id) + controller.dismissWithoutTransitionOut() } )), environment: {}, diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index d39b5f6277..004615d863 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -41,6 +41,7 @@ public final class StoryFooterPanelComponent: Component { public let storyItem: EngineStoryItem public let myReaction: MyReaction? public let isChannel: Bool + public let canShare: Bool public let externalViews: EngineStoryItem.Views? public let expandFraction: CGFloat public let expandViewStats: () -> Void @@ -49,6 +50,7 @@ public final class StoryFooterPanelComponent: Component { public let likeAction: () -> Void public let forwardAction: () -> Void public let repostAction: () -> Void + public let cancelUploadAction: () -> Void public init( context: AccountContext, @@ -57,6 +59,7 @@ public final class StoryFooterPanelComponent: Component { storyItem: EngineStoryItem, myReaction: MyReaction?, isChannel: Bool, + canShare: Bool, externalViews: EngineStoryItem.Views?, expandFraction: CGFloat, expandViewStats: @escaping () -> Void, @@ -64,7 +67,8 @@ public final class StoryFooterPanelComponent: Component { moreAction: @escaping (UIView, ContextGesture?) -> Void, likeAction: @escaping () -> Void, forwardAction: @escaping () -> Void, - repostAction: @escaping () -> Void + repostAction: @escaping () -> Void, + cancelUploadAction: @escaping () -> Void ) { self.context = context self.theme = theme @@ -72,6 +76,7 @@ public final class StoryFooterPanelComponent: Component { self.storyItem = storyItem self.myReaction = myReaction self.isChannel = isChannel + self.canShare = canShare self.externalViews = externalViews self.expandViewStats = expandViewStats self.expandFraction = expandFraction @@ -80,6 +85,7 @@ public final class StoryFooterPanelComponent: Component { self.likeAction = likeAction self.forwardAction = forwardAction self.repostAction = repostAction + self.cancelUploadAction = cancelUploadAction } public static func ==(lhs: StoryFooterPanelComponent, rhs: StoryFooterPanelComponent) -> Bool { @@ -227,7 +233,7 @@ public final class StoryFooterPanelComponent: Component { guard let component = self.component else { return } - component.context.engine.messages.cancelStoryUpload(stableId: component.storyItem.id) + component.cancelUploadAction() } func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -428,23 +434,7 @@ public final class StoryFooterPanelComponent: Component { likeButton = ComponentView() self.likeButton = likeButton } - - let repostButton: ComponentView - if let current = self.repostButton { - repostButton = current - } else { - repostButton = ComponentView() - self.repostButton = repostButton - } - - let forwardButton: ComponentView - if let current = self.forwardButton { - forwardButton = current - } else { - forwardButton = ComponentView() - self.forwardButton = forwardButton - } - + let likeButtonSize = likeButton.update( transition: likeStatsTransition, component: AnyComponent(MessageInputActionButtonComponent( @@ -500,100 +490,127 @@ public final class StoryFooterPanelComponent: Component { rightContentOffset -= likeButtonSize.width + 14.0 } - let repostButtonSize = repostButton.update( - transition: likeStatsTransition, - component: AnyComponent(MessageInputActionButtonComponent( - mode: .repost, - storyId: component.storyItem.id, - action: { [weak self] _, action, _ in - guard let self, let component = self.component else { - return - } - guard case .up = action else { - return - } - component.repostAction() - }, - longPressAction: nil, - switchMediaInputMode: { - }, - updateMediaCancelFraction: { _ in - }, - lockMediaRecording: { - }, - stopAndPreviewMediaRecording: { - }, - moreAction: { _, _ in }, - context: component.context, - theme: component.theme, - strings: component.strings, - presentController: { _ in }, - audioRecorder: nil, - videoRecordingStatus: nil - )), - environment: {}, - containerSize: CGSize(width: 33.0, height: 33.0) - ) - if let repostButtonView = repostButton.view { - if repostButtonView.superview == nil { - self.addSubview(repostButtonView) + if component.canShare { + let repostButton: ComponentView + if let current = self.repostButton { + repostButton = current + } else { + repostButton = ComponentView() + self.repostButton = repostButton } - var repostButtonFrame = CGRect(origin: CGPoint(x: rightContentOffset - repostButtonSize.width, y: floor((size.height - repostButtonSize.height) * 0.5)), size: repostButtonSize) - repostButtonFrame.origin.y += component.expandFraction * 45.0 - likeStatsTransition.setPosition(view: repostButtonView, position: repostButtonFrame.center) - likeStatsTransition.setBounds(view: repostButtonView, bounds: CGRect(origin: CGPoint(), size: repostButtonFrame.size)) - likeStatsTransition.setAlpha(view: repostButtonView, alpha: 1.0 - component.expandFraction) - - rightContentOffset -= repostButtonSize.width + 14.0 - } - - let forwardButtonSize = forwardButton.update( - transition: likeStatsTransition, - component: AnyComponent(MessageInputActionButtonComponent( - mode: .forward, - storyId: component.storyItem.id, - action: { [weak self] _, action, _ in - guard let self, let component = self.component else { - return - } - guard case .up = action else { - return - } - component.forwardAction() - }, - longPressAction: nil, - switchMediaInputMode: { - }, - updateMediaCancelFraction: { _ in - }, - lockMediaRecording: { - }, - stopAndPreviewMediaRecording: { - }, - moreAction: { _, _ in }, - context: component.context, - theme: component.theme, - strings: component.strings, - presentController: { _ in }, - audioRecorder: nil, - videoRecordingStatus: nil - )), - environment: {}, - containerSize: CGSize(width: 33.0, height: 33.0) - ) - if let forwardButtonView = forwardButton.view { - if forwardButtonView.superview == nil { - self.addSubview(forwardButtonView) + let forwardButton: ComponentView + if let current = self.forwardButton { + forwardButton = current + } else { + forwardButton = ComponentView() + self.forwardButton = forwardButton } - var forwardButtonFrame = CGRect(origin: CGPoint(x: rightContentOffset - likeButtonSize.width, y: floor((size.height - forwardButtonSize.height) * 0.5)), size: forwardButtonSize) - forwardButtonFrame.origin.y += component.expandFraction * 45.0 - likeStatsTransition.setPosition(view: forwardButtonView, position: forwardButtonFrame.center) - likeStatsTransition.setBounds(view: forwardButtonView, bounds: CGRect(origin: CGPoint(), size: forwardButtonFrame.size)) - likeStatsTransition.setAlpha(view: forwardButtonView, alpha: 1.0 - component.expandFraction) + let repostButtonSize = repostButton.update( + transition: likeStatsTransition, + component: AnyComponent(MessageInputActionButtonComponent( + mode: .repost, + storyId: component.storyItem.id, + action: { [weak self] _, action, _ in + guard let self, let component = self.component else { + return + } + guard case .up = action else { + return + } + component.repostAction() + }, + longPressAction: nil, + switchMediaInputMode: { + }, + updateMediaCancelFraction: { _ in + }, + lockMediaRecording: { + }, + stopAndPreviewMediaRecording: { + }, + moreAction: { _, _ in }, + context: component.context, + theme: component.theme, + strings: component.strings, + presentController: { _ in }, + audioRecorder: nil, + videoRecordingStatus: nil + )), + environment: {}, + containerSize: CGSize(width: 33.0, height: 33.0) + ) + if let repostButtonView = repostButton.view { + if repostButtonView.superview == nil { + self.addSubview(repostButtonView) + } + var repostButtonFrame = CGRect(origin: CGPoint(x: rightContentOffset - repostButtonSize.width, y: floor((size.height - repostButtonSize.height) * 0.5)), size: repostButtonSize) + repostButtonFrame.origin.y += component.expandFraction * 45.0 + + likeStatsTransition.setPosition(view: repostButtonView, position: repostButtonFrame.center) + likeStatsTransition.setBounds(view: repostButtonView, bounds: CGRect(origin: CGPoint(), size: repostButtonFrame.size)) + likeStatsTransition.setAlpha(view: repostButtonView, alpha: 1.0 - component.expandFraction) + + rightContentOffset -= repostButtonSize.width + 14.0 + } - rightContentOffset -= forwardButtonSize.width + 8.0 + let forwardButtonSize = forwardButton.update( + transition: likeStatsTransition, + component: AnyComponent(MessageInputActionButtonComponent( + mode: .forward, + storyId: component.storyItem.id, + action: { [weak self] _, action, _ in + guard let self, let component = self.component else { + return + } + guard case .up = action else { + return + } + component.forwardAction() + }, + longPressAction: nil, + switchMediaInputMode: { + }, + updateMediaCancelFraction: { _ in + }, + lockMediaRecording: { + }, + stopAndPreviewMediaRecording: { + }, + moreAction: { _, _ in }, + context: component.context, + theme: component.theme, + strings: component.strings, + presentController: { _ in }, + audioRecorder: nil, + videoRecordingStatus: nil + )), + environment: {}, + containerSize: CGSize(width: 33.0, height: 33.0) + ) + if let forwardButtonView = forwardButton.view { + if forwardButtonView.superview == nil { + self.addSubview(forwardButtonView) + } + var forwardButtonFrame = CGRect(origin: CGPoint(x: rightContentOffset - likeButtonSize.width, y: floor((size.height - forwardButtonSize.height) * 0.5)), size: forwardButtonSize) + forwardButtonFrame.origin.y += component.expandFraction * 45.0 + + likeStatsTransition.setPosition(view: forwardButtonView, position: forwardButtonFrame.center) + likeStatsTransition.setBounds(view: forwardButtonView, bounds: CGRect(origin: CGPoint(), size: forwardButtonFrame.size)) + likeStatsTransition.setAlpha(view: forwardButtonView, alpha: 1.0 - component.expandFraction) + + rightContentOffset -= forwardButtonSize.width + 8.0 + } + } else { + if let repostButton = self.repostButton { + self.repostButton = nil + repostButton.view?.removeFromSuperview() + } + if let forwardButton = self.forwardButton { + self.forwardButton = nil + forwardButton.view?.removeFromSuperview() + } } } else { if let likeButton = self.likeButton { @@ -917,7 +934,11 @@ public final class StoryFooterPanelComponent: Component { guard let self, let component = self.component else { return } - component.deleteAction() + if component.storyItem.isPending { + component.cancelUploadAction() + } else { + component.deleteAction() + } } ).minSize(CGSize(width: 44.0, height: baseHeight))), environment: {}, diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 7c9f016a75..4c6e53ed31 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -263,7 +263,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var isLoadingValue: Bool = false private var isLoadingEarlier: Bool = false private func updateIsLoading(isLoading: Bool, earlier: Bool, animated: Bool) { - let useLoadingPlaceholder = "".isEmpty + let useLoadingPlaceholder = self.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser let updated = isLoading != self.isLoadingValue || (isLoading && earlier && !self.isLoadingEarlier) diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 90d1bec42b..1632fed615 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -483,6 +483,32 @@ public final class TelegramRootController: NavigationController, TelegramRootCon if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) { rootTabController.selectedIndex = index } + if forwardInfo != nil { + var viewControllers = self.viewControllers + var dismissNext = false + var range: Range? + for i in (0 ..< viewControllers.count).reversed() { + let controller = viewControllers[i] + if controller is MediaEditorScreen { + dismissNext = true + } + if dismissNext { + if controller !== self.rootTabController { + if let current = range { + range = current.lowerBound - 1 ..< current.upperBound + } else { + range = i ..< i + } + } else { + break + } + } + } + if let range { + viewControllers.removeSubrange(range) + self.setViewControllers(viewControllers, animated: false) + } + } } let completionImpl: () -> Void = { [weak self] in From 6ec58eb9b46e24b40d40114636d4fab9ecc8d711 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 26 Nov 2023 20:11:40 +0400 Subject: [PATCH 2/5] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 ++++ .../TGModernConversationInputMicButton.m | 24 ++++++++++++++++++- ...essageJoinedChannelBubbleContentNode.swift | 4 ++-- .../StoryContentCaptionComponent.swift | 13 ++++++---- .../TelegramUI/Sources/ChatController.swift | 3 +++ 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 83ddf8f55f..b2a55dc231 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10548,3 +10548,7 @@ Sorry for the inconvenience."; "MediaEditor.VideoRemovalConfirmation" = "Are you sure you want to delete video message?"; "MediaEditor.HoldToRecordVideo" = "Hold to record video"; + +"Chat.ChannelRecommendation.PremiumTooltip" = "Subcribe to [Telegram Premium]() to unlock up to **100** channels."; + +"Story.ForwardAuthorHiddenTooltip" = "The account was hidden by the user"; diff --git a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m index 88ff9f0696..bf73ed470e 100644 --- a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m +++ b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m @@ -563,6 +563,27 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius _hidesPanelOnLock = true; } ++ (UIImage *)stopIconImage +{ + static dispatch_once_t onceToken; + static UIImage *iconImage; + dispatch_once(&onceToken, ^ + { + CGRect rect = CGRectMake(0, 0, 22.0f, 22.0f); + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextAddPath(context, [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 22, 22) cornerRadius:7].CGPath); + CGContextSetFillColorWithColor(context, UIColorRGBA(0x0ffffff, 1.3f).CGColor); + CGContextFillPath(context); + + iconImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + return iconImage; +} + + - (void)animateLock { if (!_animatedIn) { return; @@ -575,8 +596,9 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius snapshotView.frame = _innerIconView.frame; [_innerIconWrapperView insertSubview:snapshotView atIndex:0]; + UIImage *icon = _hidesPanelOnLock ? [TGModernConversationInputMicButton stopIconImage] : TGComponentsImageNamed(@"RecordSendIcon"); _previousIcon = _innerIconView.image; - [self setIcon:TGTintedImage(TGComponentsImageNamed(@"RecordSendIcon"), _pallete != nil ? _pallete.iconColor : [UIColor whiteColor])]; + [self setIcon:TGTintedImage(icon, _pallete != nil && !_hidesPanelOnLock ? _pallete.iconColor : [UIColor whiteColor])]; _currentScale = 1; _cancelTargetTranslation = 0; diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift index 7b354c8096..064658edfa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift @@ -342,7 +342,7 @@ public class ChatMessageJoinedChannelBubbleContentNode: ChatMessageBubbleContent let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } let controller = UndoOverlayController( presentationData: presentationData, - content: .premiumPaywall(title: nil, text: "Subcribe to [Telegram Premium]() to unlock up to **100** channels.", customUndoText: nil, timeout: nil, linkAction: nil), + content: .premiumPaywall(title: nil, text: item.presentationData.strings.Chat_ChannelRecommendation_PremiumTooltip, customUndoText: nil, timeout: nil, linkAction: nil), elevatedLayout: false, action: { [weak self] action in if case .info = action { @@ -680,7 +680,7 @@ private final class ChannelItemComponent: Component { self.component = component self.state = state - self.contextContainer.isGestureEnabled = component.contextAction != nil + self.contextContainer.isGestureEnabled = true let titleSize = self.title.update( transition: .immediate, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 12ddeeea5d..8e108c77d6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -674,7 +674,6 @@ final class StoryContentCaptionComponent: Component { let authorName: String let isChannel: Bool let text: String? - var isEnabled = true switch forwardInfo { case let .known(peer, _, _): @@ -701,7 +700,6 @@ final class StoryContentCaptionComponent: Component { authorName = name isChannel = false text = "" - isEnabled = false } if let text { @@ -731,9 +729,16 @@ final class StoryContentCaptionComponent: Component { action: { [weak self] in if let self, case let .known(peer, _, _) = forwardInfo, let story = self.forwardInfoStory { self.component?.openStory(peer, story) + } else if let controller = self?.component?.controller() as? StoryContainerScreen { + let tooltipController = TooltipController(content: .text(component.strings.Story_ForwardAuthorHiddenTooltip), baseFontSize: 17.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) + controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak controller] in + if let self, let controller, let forwardInfoPanel = self.forwardInfoPanel?.view { + return (controller.node, forwardInfoPanel.convert(forwardInfoPanel.bounds, to: controller.view)) + } + return nil + })) } - }, - isEnabled: isEnabled + } ) ), environment: {}, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index e7e4c88d46..de51e8af43 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -10002,6 +10002,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, presentController: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) }, presentControllerInCurrent: { [weak self] controller, arguments in + if controller is UndoOverlayController { + self?.dismissAllTooltips() + } self?.present(controller, in: .current, with: arguments) }, getNavigationController: { [weak self] in return self?.navigationController as? NavigationController From fb50102b0636ed98b5e70a5f823ece91ca0ec974 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 26 Nov 2023 22:22:35 +0400 Subject: [PATCH 3/5] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 1 + submodules/Camera/Sources/Camera.swift | 69 ++++++++++- submodules/Camera/Sources/CameraOutput.swift | 3 + .../DrawingUI/Sources/VideoRecorder.swift | 5 +- ...essageJoinedChannelBubbleContentNode.swift | 114 +++++++----------- .../Sources/MediaEditorRecording.swift | 57 ++++++++- .../Sources/MediaEditorScreen.swift | 29 +---- .../PeerInfoRecommendedChannelsPane.swift | 15 ++- .../Sources/PeerInfoHeaderNode.swift | 49 ++------ 9 files changed, 198 insertions(+), 144 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b2a55dc231..28b2fb36a7 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10489,6 +10489,7 @@ Sorry for the inconvenience."; "Chat.SimilarChannels" = "Similar Channels"; "Chat.SimilarChannels.Join" = "Join"; "Chat.SimilarChannels.JoinedChannel" = "You joined channel **%@**."; +"Chat.SimilarChannels.MoreChannels" = "More Channels"; "Wallpaper.ApplyForMe" = "Apply for Me"; "Wallpaper.ApplyForBoth" = "Apply for Me and %@"; diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 0f56597b9a..6e5b470df1 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -117,6 +117,7 @@ private final class CameraContext { private var invalidated = false private let detectedCodesPipe = ValuePipe<[CameraCode]>() + private let audioLevelPipe = ValuePipe() fileprivate let modeChangePromise = ValuePromise(.none) var previewView: CameraPreviewView? @@ -281,6 +282,10 @@ private final class CameraContext { } } + private var micLevelPeak: Int16 = 0 + private var micLevelPeakCount = 0 + + private var isDualCameraEnabled: Bool? public func setDualCameraEnabled(_ enabled: Bool, change: Bool = true) { guard enabled != self.isDualCameraEnabled else { @@ -352,6 +357,48 @@ private final class CameraContext { self.lastSnapshotTimestamp = timestamp } } + if self.initialConfiguration.reportAudioLevel { + self.mainDeviceContext?.output.processAudioBuffer = { [weak self] sampleBuffer in + guard let self else { + return + } + var blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) + let numSamplesInBuffer = CMSampleBufferGetNumSamples(sampleBuffer) + var audioBufferList = AudioBufferList() + + CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, bufferListSizeNeededOut: nil, bufferListOut: &audioBufferList, bufferListSize: MemoryLayout.size, blockBufferAllocator: nil, blockBufferMemoryAllocator: nil, flags: kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, blockBufferOut: &blockBuffer) + +// for bufferCount in 0.., count: Int) { + for i in 0..= 1200 { + let level = Float(self.micLevelPeak) / 4000.0 + self.audioLevelPipe.putNext(level) + + self.micLevelPeak = 0 + self.micLevelPeakCount = 0 + } + } + } + } + } self.mainDeviceContext?.output.processCodes = { [weak self] codes in self?.detectedCodesPipe.putNext(codes) } @@ -526,6 +573,10 @@ private final class CameraContext { return self.detectedCodesPipe.signal() } + var audioLevel: Signal { + return self.audioLevelPipe.signal() + } + @objc private func sessionInterruptionEnded(notification: NSNotification) { } @@ -564,8 +615,9 @@ public final class Camera { let metadata: Bool let preferredFps: Double let preferWide: Bool + let reportAudioLevel: Bool - public init(preset: Preset, position: Position, isDualEnabled: Bool = false, audio: Bool, photo: Bool, metadata: Bool, preferredFps: Double, preferWide: Bool = false) { + public init(preset: Preset, position: Position, isDualEnabled: Bool = false, audio: Bool, photo: Bool, metadata: Bool, preferredFps: Double, preferWide: Bool = false, reportAudioLevel: Bool = false) { self.preset = preset self.position = position self.isDualEnabled = isDualEnabled @@ -574,6 +626,7 @@ public final class Camera { self.metadata = metadata self.preferredFps = preferredFps self.preferWide = preferWide + self.reportAudioLevel = reportAudioLevel } } @@ -865,6 +918,20 @@ public final class Camera { } } + public var audioLevel: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + disposable.set(context.audioLevel.start(next: { codes in + subscriber.putNext(codes) + })) + } + } + return disposable + } + } + public enum ModeChange: Equatable { case none case position diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index 80daa60410..b48219f156 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -96,6 +96,7 @@ final class CameraOutput: NSObject { private var videoRecorder: VideoRecorder? var processSampleBuffer: ((CMSampleBuffer, CVImageBuffer, AVCaptureConnection) -> Void)? + var processAudioBuffer: ((CMSampleBuffer) -> Void)? var processCodes: (([CameraCode]) -> Void)? init(exclusive: Bool) { @@ -379,6 +380,8 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection) + } else { + self.processAudioBuffer?(sampleBuffer) } if let videoRecorder = self.videoRecorder, videoRecorder.isRecording { diff --git a/submodules/DrawingUI/Sources/VideoRecorder.swift b/submodules/DrawingUI/Sources/VideoRecorder.swift index 03c30a9f15..b835582b5f 100644 --- a/submodules/DrawingUI/Sources/VideoRecorder.swift +++ b/submodules/DrawingUI/Sources/VideoRecorder.swift @@ -47,7 +47,8 @@ public final class EntityVideoRecorder { photo: false, metadata: false, preferredFps: 60.0, - preferWide: true + preferWide: true, + reportAudioLevel: true ), previewView: self.previewView, secondaryPreviewView: nil @@ -73,7 +74,7 @@ public final class EntityVideoRecorder { } } - self.micLevelPromise.set(.single(0.0)) + self.micLevelPromise.set(camera.audioLevel) let start = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0 mediaEditor.stop() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift index 064658edfa..3130bc9361 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift @@ -544,11 +544,6 @@ private class MessageBackgroundNode: ASDisplayNode { private let itemSize = CGSize(width: 84.0, height: 90.0) private final class ChannelItemComponent: Component { - class ExternalState { - var cachedPlaceholderImage: UIImage? - } - - let externalState: ExternalState let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings @@ -561,7 +556,6 @@ private final class ChannelItemComponent: Component { let contextAction: ((EnginePeer, UIView, ContextGesture?) -> Void)? init( - externalState: ExternalState, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, @@ -573,7 +567,6 @@ private final class ChannelItemComponent: Component { openMore: @escaping () -> Void, contextAction: ((EnginePeer, UIView, ContextGesture?) -> Void)? ) { - self.externalState = externalState self.context = context self.theme = theme self.strings = strings @@ -677,8 +670,11 @@ private final class ChannelItemComponent: Component { } func update(component: ChannelItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component self.component = component self.state = state + + let themeUpdated = previousComponent?.theme !== component.theme self.contextContainer.isGestureEnabled = true @@ -776,69 +772,43 @@ private final class ChannelItemComponent: Component { } self.circlesView.isHidden = false - if self.circlesView.image == nil { - if let current = component.externalState.cachedPlaceholderImage { - self.circlesView.image = current - } else { - let image = generateImage(CGSize(width: 50.0, height: avatarSize.height), rotatedContext: { size, context in - context.clear(CGRect(origin: .zero, size: size)) - - let randomColors: [(UInt32, UInt32)] = [ - (0x4493de, 0x52d5d9), - (0xfcc418, 0xf6774a), - (0xffc9a2, 0xfbedb2), - (0x133e88, 0x131925), - (0x63c7f0, 0xf6c506), - (0x88a5cb, 0x162639), - (0xd669ed, 0xe0a2f3), - (0x54cb68, 0xa0de7e) - ] - - context.saveGState() - - let rect1 = CGRect(origin: CGPoint(x: size.width - avatarSize.width, y: 0.0), size: avatarSize) - context.addEllipse(in: rect1) - context.clip() - - var firstColors: NSArray = [] - if let random = randomColors.randomElement() { - firstColors = [UIColor(rgb: random.0).cgColor, UIColor(rgb: random.1).cgColor] - } - var locations: [CGFloat] = [1.0, 0.0] - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let firstGradient = CGGradient(colorsSpace: colorSpace, colors: firstColors as CFArray, locations: &locations)! - context.drawLinearGradient(firstGradient, start: CGPoint(x: rect1.minX, y: rect1.minY), end: CGPoint(x: rect1.maxX, y: rect1.maxY), options: CGGradientDrawingOptions()) - - context.restoreGState() - - context.setBlendMode(.clear) - context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - avatarSize.width - 12.0, y: -2.0), size: CGSize(width: avatarSize.width + 4.0, height: avatarSize.height + 4.0))) - - context.setBlendMode(.normal) - - context.saveGState() - - let rect2 = CGRect(origin: CGPoint(x: size.width - avatarSize.width - 10.0, y: 0.0), size: avatarSize) - context.addEllipse(in: rect2) - context.clip() - - var secondColors: NSArray = [] - if let random = randomColors.randomElement() { - secondColors = [UIColor(rgb: random.0).cgColor, UIColor(rgb: random.1).cgColor] - } - - let secondGradient = CGGradient(colorsSpace: colorSpace, colors: secondColors as CFArray, locations: &locations)! - context.drawLinearGradient(secondGradient, start: CGPoint(x: rect2.minX, y: rect2.minY), end: CGPoint(x: rect2.minX, y: rect2.maxY), options: CGGradientDrawingOptions()) - - context.restoreGState() - - context.setBlendMode(.clear) - context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - avatarSize.width - 22.0, y: -2.0), size: CGSize(width: avatarSize.width + 4.0, height: avatarSize.height + 4.0))) - }) - component.externalState.cachedPlaceholderImage = image - self.circlesView.image = image - } + if self.circlesView.image == nil || themeUpdated { + let image = generateImage(CGSize(width: 50.0, height: avatarSize.height), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + let color = component.theme.chat.message.incoming.secondaryTextColor.withMultipliedAlpha(0.35) + + context.saveGState() + + let rect1 = CGRect(origin: CGPoint(x: size.width - avatarSize.width, y: 0.0), size: avatarSize) + context.addEllipse(in: rect1) + context.clip() + + context.setFillColor(color.cgColor) + context.fill(rect1) + + context.restoreGState() + + context.setBlendMode(.clear) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - avatarSize.width - 12.0, y: -2.0), size: CGSize(width: avatarSize.width + 4.0, height: avatarSize.height + 4.0))) + + context.setBlendMode(.normal) + + context.saveGState() + + let rect2 = CGRect(origin: CGPoint(x: size.width - avatarSize.width - 10.0, y: 0.0), size: avatarSize) + context.addEllipse(in: rect2) + context.clip() + + context.setFillColor(color.cgColor) + context.fill(rect2) + + context.restoreGState() + + context.setBlendMode(.clear) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - avatarSize.width - 22.0, y: -2.0), size: CGSize(width: avatarSize.width + 4.0, height: avatarSize.height + 4.0))) + }) + self.circlesView.image = image } self.circlesView.frame = CGRect(origin: CGPoint(x: avatarFrame.midX, y: 0.0), size: CGSize(width: 50.0, height: 60.0)) } else { @@ -991,7 +961,6 @@ final class ChannelListPanelComponent: Component { private let measureItem = ComponentView() private var visibleItems: [EnginePeer.Id: ComponentView] = [:] - private var externalState = ChannelItemComponent.ExternalState() private var ignoreScrolling: Bool = false @@ -1072,7 +1041,7 @@ final class ChannelListPanelComponent: Component { if !component.context.isPremium { isLocked = true } - title = isLocked ? "Unlock More Channels" : "View More Channels" + title = component.strings.Chat_SimilarChannels_MoreChannels subtitle = "+\(component.peers.count - channelsLimit)" isLast = true } else { @@ -1092,7 +1061,6 @@ final class ChannelListPanelComponent: Component { let _ = itemView.update( transition: itemTransition, component: AnyComponent(ChannelItemComponent( - externalState: self.externalState, context: component.context, theme: component.theme, strings: component.strings, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift index 595de405f2..d2d30465c1 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift @@ -1,11 +1,14 @@ import Foundation import UIKit import Display +import SwiftSignalKit import MediaEditor import DrawingUI import ChatPresentationInterfaceState import PresentationDataUtils import TelegramPresentationData +import DeviceAccess +import AccountContext extension MediaEditorScreen { final class Recording { @@ -13,10 +16,56 @@ extension MediaEditorScreen { private var recorder: EntityVideoRecorder? + private let idleTimerExtensionDisposable = MetaDisposable() + + private var authorizationStatusDisposables = DisposableSet() + private var cameraAuthorizationStatus: AccessType = .notDetermined + private var microphoneAuthorizationStatus: AccessType = .notDetermined + + fileprivate var cameraIsActive = true { + didSet { + guard let context = self.controller?.context else { + return + } + if self.cameraIsActive { + self.idleTimerExtensionDisposable.set(context.sharedContext.applicationBindings.pushIdleTimerExtension()) + } else { + self.idleTimerExtensionDisposable.set(nil) + } + } + } + var isLocked = false init(controller: MediaEditorScreen) { self.controller = controller + + self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .camera(.video)) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let self { + self.cameraAuthorizationStatus = status + } + })) + + self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .microphone(.video)) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let self { + self.microphoneAuthorizationStatus = status + } + })) + } + + deinit { + self.idleTimerExtensionDisposable.dispose() + self.authorizationStatusDisposables.dispose() + } + + func requestDeviceAccess() { + DeviceAccess.authorizeAccess(to: .camera(.video), { granted in + if granted { + DeviceAccess.authorizeAccess(to: .microphone(.video)) + } + }) } func setMediaRecordingActive(_ isActive: Bool, finished: Bool, sourceView: UIView?) { @@ -29,8 +78,8 @@ extension MediaEditorScreen { } if isActive { - if controller.cameraAuthorizationStatus != .allowed || controller.microphoneAuthorizationStatus != .allowed { - controller.requestDeviceAccess() + if self.cameraAuthorizationStatus != .allowed || self.microphoneAuthorizationStatus != .allowed { + self.requestDeviceAccess() return } @@ -53,6 +102,8 @@ extension MediaEditorScreen { } self.recorder = recorder controller.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2)) + + self.cameraIsActive = true } else { if let recorder = self.recorder { recorder.stopRecording(save: finished, completion: { [weak self] in @@ -65,6 +116,8 @@ extension MediaEditorScreen { }) controller.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2)) + + self.cameraIsActive = false } else { guard self.tooltipController == nil, let sourceView else { return diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index c4f089e5b2..32eff31e10 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -4043,11 +4043,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var audioSessionDisposable: Disposable? private let postingAvailabilityPromise = Promise() private var postingAvailabilityDisposable: Disposable? - - private var authorizationStatusDisposables = DisposableSet() - private(set) var cameraAuthorizationStatus: AccessType = .notDetermined - private(set) var microphoneAuthorizationStatus: AccessType = .notDetermined - + public init( context: AccountContext, subject: Signal, @@ -4122,20 +4118,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let _ = forwardSource { self.postingAvailabilityPromise.set(self.context.engine.messages.checkStoriesUploadAvailability(target: .myStories)) } - - self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .camera(.video)) - |> deliverOnMainQueue).start(next: { [weak self] status in - if let self { - self.cameraAuthorizationStatus = status - } - })) - - self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .microphone(.video)) - |> deliverOnMainQueue).start(next: { [weak self] status in - if let self { - self.microphoneAuthorizationStatus = status - } - })) } required public init(coder aDecoder: NSCoder) { @@ -4146,7 +4128,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.exportDisposable.dispose() self.audioSessionDisposable?.dispose() self.postingAvailabilityDisposable?.dispose() - self.authorizationStatusDisposables.dispose() } override public func loadDisplayNode() { @@ -4225,14 +4206,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate var isEmbeddedEditor: Bool { return self.isEditingStory || self.forwardSource != nil } - - func requestDeviceAccess() { - DeviceAccess.authorizeAccess(to: .camera(.video), { granted in - if granted { - DeviceAccess.authorizeAccess(to: .microphone(.video)) - } - }) - } func openPrivacySettings(_ privacy: MediaEditorResultPrivacy? = nil, completion: @escaping () -> Void = {}) { self.node.mediaEditor?.maybePauseVideo() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift index b38ab21e95..d36445ea24 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift @@ -117,10 +117,11 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode return self.ready.get() } + private let statusPromise = Promise(nil) var status: Signal { - return .single(nil) + self.statusPromise.get() } - + var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? var tabBarOffset: CGFloat { return 0.0 @@ -159,6 +160,16 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode strongSelf.currentState = (recommendedChannels, isPremium) strongSelf.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData) }) + + self.statusPromise.set(context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId) + ) + |> map { count -> PeerInfoStatusData? in + if let count { + return PeerInfoStatusData(text: presentationData.strings.Conversation_StatusSubscribers(Int32(count)), isActivity: true, key: .recommended) + } + return nil + }) } deinit { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 834ccd76c5..6fc70332b1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -116,7 +116,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { var subtitleBackgroundButton: HighlightTrackingButtonNode? var subtitleArrowNode: ASImageNode? let panelSubtitleNode: MultiScaleTextNode - let nextPanelSubtitleNode: MultiScaleTextNode let usernameNodeContainer: ASDisplayNode let usernameNodeRawContainer: ASDisplayNode let usernameNode: MultiScaleTextNode @@ -195,9 +194,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.panelSubtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.panelSubtitleNode.displaysAsynchronously = false - self.nextPanelSubtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) - self.nextPanelSubtitleNode.displaysAsynchronously = false - self.usernameNodeContainer = ASDisplayNode() self.usernameNodeRawContainer = ASDisplayNode() self.usernameNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) @@ -258,7 +254,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.titleNodeContainer.addSubnode(self.titleNode) self.subtitleNodeContainer.addSubnode(self.subtitleNode) self.subtitleNodeContainer.addSubnode(self.panelSubtitleNode) -// self.subtitleNodeContainer.addSubnode(self.nextPanelSubtitleNode) self.usernameNodeContainer.addSubnode(self.usernameNode) self.regularContentNode.addSubnode(self.avatarClippingNode) @@ -778,7 +773,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.titleNode.updateTintColor(color: navigationContentsPrimaryColor, transition: navigationTransition) self.subtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition) self.panelSubtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition) - self.nextPanelSubtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition) if let navigationBar = self.controller?.navigationBar { if let mainContentNode = navigationBar.backButtonNode.mainContentNode { navigationTransition.updateTintColor(layer: mainContentNode.layer, color: navigationContentsAccentColor) @@ -839,7 +833,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { let subtitleAttributes: MultiScaleTextState.Attributes var subtitleIsButton: Bool = false var panelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)? - var nextPanelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)? let usernameString: (text: String, attributes: MultiScaleTextState.Attributes) if let peer = peer { isPremium = peer.isPremium @@ -899,7 +892,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { subtitleIsButton = true - let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData + let (maybePanelStatusData, _, _) = panelStatusData if let panelStatusData = maybePanelStatusData { let subtitleColor: UIColor if panelStatusData.isActivity { @@ -909,9 +902,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { } panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) } - if let nextPanelStatusData = maybeNextPanelStatusData { - nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: .white)) - } } else if let statusData = statusData { let subtitleColor: UIColor if statusData.isActivity { @@ -926,7 +916,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(16.0), color: .white)) - let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData + let (maybePanelStatusData, _, _) = panelStatusData if let panelStatusData = maybePanelStatusData { let subtitleColor: UIColor if panelStatusData.isActivity { @@ -936,9 +926,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { } panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) } - if let nextPanelStatusData = maybeNextPanelStatusData { - nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: .white)) - } } else { subtitleStringText = " " subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: .white) @@ -1071,14 +1058,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { ], mainState: TitleNodeStateRegular) self.panelSubtitleNode.accessibilityLabel = panelSubtitleString?.text ?? subtitleStringText - let nextPanelSubtitleNodeLayout = self.nextPanelSubtitleNode.updateLayout(text: nextPanelSubtitleString?.text ?? subtitleStringText, states: [ - TitleNodeStateRegular: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize), - TitleNodeStateExpanded: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize) - ], mainState: TitleNodeStateRegular) - if let _ = nextPanelSubtitleString { - self.nextPanelSubtitleNode.isHidden = false - } - let usernameNodeLayout = self.usernameNode.updateLayout(text: usernameString.text, states: [ TitleNodeStateRegular: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)), TitleNodeStateExpanded: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height)) @@ -1096,7 +1075,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { let titleExpandedSize = titleNodeLayout[TitleNodeStateExpanded]!.size let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size let _ = panelSubtitleNodeLayout[TitleNodeStateRegular]!.size - let _ = nextPanelSubtitleNodeLayout[TitleNodeStateRegular]!.size let usernameSize = usernameNodeLayout[TitleNodeStateRegular]!.size var titleHorizontalOffset: CGFloat = 0.0 @@ -1203,11 +1181,17 @@ final class PeerInfoHeaderNode: ASDisplayNode { if (panelSubtitleString?.text ?? subtitleStringText) != subtitleStringText { subtitleAlpha = 1.0 - effectiveAreaExpansionFraction panelSubtitleAlpha = effectiveAreaExpansionFraction + subtitleOffset = -effectiveAreaExpansionFraction * 5.0 panelSubtitleOffset = (1.0 - effectiveAreaExpansionFraction) * 5.0 } else { - subtitleAlpha = 1.0 - panelSubtitleAlpha = 0.0 + if effectiveAreaExpansionFraction == 1.0 { + subtitleAlpha = 0.0 + panelSubtitleAlpha = 1.0 + } else { + subtitleAlpha = 1.0 + panelSubtitleAlpha = 0.0 + } } } self.subtitleNode.update(stateFractions: [ @@ -1220,11 +1204,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 ], alpha: panelSubtitleAlpha, transition: transition) - self.nextPanelSubtitleNode.update(stateFractions: [ - TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, - TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 - ], alpha: panelSubtitleAlpha, transition: transition) - self.usernameNode.update(stateFractions: [ TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 @@ -1501,8 +1480,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.subtitleNodeRawContainer.frame = rawSubtitleFrame transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize())) transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize())) - transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) - transition.updateFrame(node: self.nextPanelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) + transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset - 1.0), size: CGSize())) transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize())) transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale) @@ -1517,7 +1495,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } else { titleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * titleMinScale subtitleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * subtitleMinScale - subtitleOffset = titleCollapseFraction * -2.0 + subtitleOffset = titleCollapseFraction * -1.0 } let rawTitleFrame = titleFrame.offsetBy(dx: self.isAvatarExpanded ? 0.0 : titleHorizontalOffset * titleScale, dy: 0.0) @@ -1544,8 +1522,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateFrameAdditiveToCenter(node: self.usernameNodeContainer, frame: CGRect(origin: usernameCenter, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset)) } transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize())) - transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) - transition.updateFrame(node: self.nextPanelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) + transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset - 1.0), size: CGSize())) transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize())) transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale) From bd8f2dae27bee81e9ab473c042cda4bfab2d2988 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 27 Nov 2023 00:18:25 +0400 Subject: [PATCH 4/5] Various fixes --- .../Sources/ItemListPeerItem.swift | 26 +++++++++---- .../Sources/ChatHistoryEntry.swift | 11 ++++++ .../PeerInfoGroupsInCommonPaneNode.swift | 2 +- .../PeerInfoRecommendedChannelsPane.swift | 37 +++++++++++++------ .../Sources/ChatHistoryEntriesForView.swift | 30 +++++++++------ 5 files changed, 75 insertions(+), 31 deletions(-) diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 4d8cebf90d..093c11e33e 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -468,6 +468,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { let hasTopGroupInset: Bool let noInsets: Bool let noCorners: Bool + let style: ItemListStyle public let tag: ItemListItemTag? let header: ListViewItemHeader? let shimmering: ItemListPeerItemShimmering? @@ -508,6 +509,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, + style: ItemListStyle = .blocks, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, @@ -547,6 +549,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { self.hasTopGroupInset = hasTopGroupInset self.noInsets = noInsets self.noCorners = noCorners + self.style = style self.tag = tag self.header = header self.shimmering = shimmering @@ -588,6 +591,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, + style: ItemListStyle = .blocks, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, @@ -627,6 +631,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { self.hasTopGroupInset = hasTopGroupInset self.noInsets = noInsets self.noCorners = noCorners + self.style = style self.tag = tag self.header = header self.shimmering = shimmering @@ -889,7 +894,6 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo return { item, params, neighbors, headerAtTop in var updateArrowImage: UIImage? - var updatedTheme: PresentationTheme? let statusFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0) let labelFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0) @@ -938,7 +942,6 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let badgeDiameter: CGFloat = 20.0 if currentItem?.presentationData.theme !== item.presentationData.theme { - updatedTheme = item.presentationData.theme updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) if let badgeColor = badgeColor { updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) @@ -1247,13 +1250,22 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo strongSelf.labelArrowNode?.image = updateArrowImage } - if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor } + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + let revealOffset = strongSelf.revealOffset let transition: ContainedViewLayoutTransition diff --git a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift index ee01ff53fd..809deb1ba6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift +++ b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift @@ -108,6 +108,17 @@ public enum ChatHistoryEntry: Identifiable, Comparable { return MessageIndex.absoluteLowerBound() } } + + public var timestamp: Int32? { + switch self { + case let .MessageEntry(message, _, _, _, _, _): + return message.timestamp + case let .MessageGroupEntry(_, messages, _): + return messages[0].0.timestamp + default: + return nil + } + } public static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { switch lhs { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift index 673fd6f5c0..01c912103e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -44,7 +44,7 @@ private struct GroupsInCommonListEntry: Comparable, Identifiable { }, removePeer: { _ in }, contextAction: { node, gesture in openPeerContextAction(peer, node, gesture) - }, hasTopStripe: false, noInsets: true, noCorners: true) + }, hasTopStripe: false, noInsets: true, noCorners: true, style: .plain) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift index d36445ea24..ce226d65bf 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift @@ -72,7 +72,7 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable { }, removePeer: { _ in }, contextAction: { node, gesture in openPeerContextAction(peer._asPeer(), node, gesture) - }, hasTopStripe: false, noInsets: true, noCorners: true, disableInteractiveTransitionIfNecessary: true) + }, hasTopStripe: false, noInsets: true, noCorners: true, style: .plain, disableInteractiveTransitionIfNecessary: true) } } } @@ -106,7 +106,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode private var unlockText: ComponentView? private var unlockButton: SolidRoundedButtonNode? - private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isScrollingLockedAtTop: Bool)? + private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? private var theme: PresentationTheme? private let presentationDataPromise = Promise() @@ -190,7 +190,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { let isFirstLayout = self.currentParams == nil - self.currentParams = (size, sideInset, bottomInset, isScrollingLockedAtTop) + self.currentParams = (size, sideInset, bottomInset, isScrollingLockedAtTop, presentationData) self.presentationDataPromise.set(.single(presentationData)) transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) @@ -237,10 +237,19 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode self.enqueuedTransactions.append(transaction) self.dequeueTransaction() + self.layoutUnlockPanel() + } + + private func layoutUnlockPanel() { + guard let (_, isPremium) = self.currentState, let currentParams = self.currentParams else { + return + } if !isPremium { - guard let size = self.currentParams?.size, let sideInset = self.currentParams?.sideInset, let bottomInset = self.currentParams?.bottomInset else { - return - } + let size = currentParams.size + let sideInset = currentParams.sideInset + let bottomInset = currentParams.bottomInset + let presentationData = currentParams.presentationData + let themeUpdated = self.theme !== presentationData.theme self.theme = presentationData.theme @@ -281,13 +290,12 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode } if themeUpdated { - let topColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.0) - let bottomColor = presentationData.theme.list.itemBlocksBackgroundColor + let topColor = presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.0) + let bottomColor = presentationData.theme.list.plainBackgroundColor unlockBackground.image = generateGradientImage(size: CGSize(width: 1.0, height: 170.0), colors: [topColor, bottomColor, bottomColor], locations: [0.0, 0.3, 1.0]) unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme)) } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) let textColor = presentationData.theme.list.itemSecondaryTextColor @@ -296,6 +304,11 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode return nil }) + var scrollOffset: CGFloat = 0.0 + if case let .known(offset) = self.listNode.visibleBottomContentOffset() { + scrollOffset = min(0.0, offset + bottomInset + 80.0) + } + let unlockSize = unlockText.update( transition: .immediate, component: AnyComponent( @@ -314,14 +327,14 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.unlockPressed))) self.view.addSubview(view) } - view.frame = CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: size.height - bottomInset - unlockSize.height - 13.0), size: unlockSize) + view.frame = CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: size.height - bottomInset - unlockSize.height - 13.0 + scrollOffset), size: unlockSize) } - unlockBackground.frame = CGRect(x: 0.0, y: size.height - bottomInset - 170.0, width: size.width, height: bottomInset + 170.0) + unlockBackground.frame = CGRect(x: 0.0, y: size.height - bottomInset - 170.0 + scrollOffset, width: size.width, height: bottomInset + 170.0) let buttonSideInset = sideInset + 16.0 let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0) - unlockButton.frame = CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - unlockSize.height - buttonSize.height - 26.0), size: buttonSize) + unlockButton.frame = CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - unlockSize.height - buttonSize.height - 26.0 + scrollOffset), size: buttonSize) let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate) } else { self.unlockBackground?.removeFromSuperview() diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 695fbeb214..d9eadc9668 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -131,12 +131,6 @@ func chatHistoryEntriesForView( } } - if let maybeJoinMessage = joinMessage { - if message.timestamp > maybeJoinMessage.timestamp, (!view.holeEarlier || count > 0) { - entries.append(.MessageEntry(maybeJoinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil))) - joinMessage = nil - } - } count += 1 if let customThreadOutgoingReadState = customThreadOutgoingReadState { @@ -234,7 +228,7 @@ func chatHistoryEntriesForView( entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }))) } } - + if !groupBucket.isEmpty { assert(groupMessages || reverseGroupedMessages) if reverseGroupedMessages { @@ -253,11 +247,25 @@ func chatHistoryEntriesForView( } } - if let maybeJoinMessage = joinMessage, !view.holeLater { - entries.append(.MessageEntry(maybeJoinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil))) - joinMessage = nil + if let lowerTimestamp = view.entries.last?.message.timestamp, let upperTimestamp = view.entries.first?.message.timestamp { + if let joinMessage { + var insertAtPosition: Int? + if joinMessage.timestamp >= lowerTimestamp && view.laterId == nil { + insertAtPosition = entries.count + } else if joinMessage.timestamp < lowerTimestamp && joinMessage.timestamp > upperTimestamp { + for i in 0 ..< entries.count { + if let timestamp = entries[i].timestamp, timestamp > joinMessage.timestamp { + insertAtPosition = i + break + } + } + } + if let insertAtPosition { + entries.insert(.MessageEntry(joinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: insertAtPosition) + } + } } - + if let maxReadIndex = view.maxReadIndex, includeUnreadEntry { var i = 0 let unreadEntry: ChatHistoryEntry = .UnreadEntry(maxReadIndex, presentationData) From 2b85da99851cf417600c23dd7a8c92f3c55619e0 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 27 Nov 2023 00:35:57 +0400 Subject: [PATCH 5/5] Various fixes --- .../Sources/MediaScrubberComponent.swift | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift index a1de76158d..700185e9f5 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift @@ -458,6 +458,9 @@ final class MediaScrubberComponent: Component { var endPosition = self.endPosition var trimViewOffset: CGFloat = 0.0 var trimViewVisualInsets: UIEdgeInsets = .zero + var trackViewWidth: CGFloat = availableSize.width + var mainTrimDuration = self.trimDuration + if let track = component.tracks.first(where: { $0.id == self.selectedTrackId }), track.id != 0 { if let trimRange = track.trimRange { startPosition = trimRange.lowerBound @@ -472,15 +475,22 @@ final class MediaScrubberComponent: Component { trimViewOffset = -delta trimViewVisualInsets.left = delta } + + if lowestVideoId == 0 && track.id == 1 { + trimViewVisualInsets = .zero + trackViewWidth = trackView.containerView.frame.width + mainTrimDuration = track.duration + } } } let scrubberSize = CGSize(width: availableSize.width, height: trackHeight) + self.trimView.isHollow = self.selectedTrackId != lowestVideoId || self.isAudioOnly let (leftHandleFrame, rightHandleFrame) = self.trimView.update( visualInsets: trimViewVisualInsets, - scrubberSize: scrubberSize, - duration: trimDuration, + scrubberSize: CGSize(width: trackViewWidth, height: trackHeight), + duration: mainTrimDuration, startPosition: startPosition, endPosition: endPosition, position: component.position,