Various fixes

This commit is contained in:
Ilya Laktyushin 2023-11-26 18:52:39 +04:00
parent de1ebed57c
commit 28e3525091
12 changed files with 352 additions and 570 deletions

View File

@ -2460,6 +2460,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
}, },
onTextEditingEnded: { _ in }, onTextEditingEnded: { _ in },
editEntity: { _ in }, editEntity: { _ in },
shouldDeleteEntity: { _ in
return true
},
getCurrentImage: { [weak controller] in getCurrentImage: { [weak controller] in
return controller?.getCurrentImage() return controller?.getCurrentImage()
}, },
@ -2981,7 +2984,8 @@ public final class DrawingToolsInteraction {
private let onInteractionUpdated: (Bool) -> Void private let onInteractionUpdated: (Bool) -> Void
private let onTextEditingEnded: (Bool) -> Void private let onTextEditingEnded: (Bool) -> Void
private let editEntity: (DrawingEntity) -> Void private let editEntity: (DrawingEntity) -> Void
private let shouldDeleteEntity: (DrawingEntity) -> Bool
public let getCurrentImage: () -> UIImage? public let getCurrentImage: () -> UIImage?
private let getControllerNode: () -> ASDisplayNode? private let getControllerNode: () -> ASDisplayNode?
private let present: (ViewController, PresentationContextType, Any?) -> Void private let present: (ViewController, PresentationContextType, Any?) -> Void
@ -3012,6 +3016,7 @@ public final class DrawingToolsInteraction {
onInteractionUpdated: @escaping (Bool) -> Void, onInteractionUpdated: @escaping (Bool) -> Void,
onTextEditingEnded: @escaping (Bool) -> Void, onTextEditingEnded: @escaping (Bool) -> Void,
editEntity: @escaping (DrawingEntity) -> Void, editEntity: @escaping (DrawingEntity) -> Void,
shouldDeleteEntity: @escaping (DrawingEntity) -> Bool,
getCurrentImage: @escaping () -> UIImage?, getCurrentImage: @escaping () -> UIImage?,
getControllerNode: @escaping () -> ASDisplayNode?, getControllerNode: @escaping () -> ASDisplayNode?,
present: @escaping (ViewController, PresentationContextType, Any?) -> Void, present: @escaping (ViewController, PresentationContextType, Any?) -> Void,
@ -3030,6 +3035,7 @@ public final class DrawingToolsInteraction {
self.onInteractionUpdated = onInteractionUpdated self.onInteractionUpdated = onInteractionUpdated
self.onTextEditingEnded = onTextEditingEnded self.onTextEditingEnded = onTextEditingEnded
self.editEntity = editEntity self.editEntity = editEntity
self.shouldDeleteEntity = shouldDeleteEntity
self.getCurrentImage = getCurrentImage self.getCurrentImage = getCurrentImage
self.getControllerNode = getControllerNode self.getControllerNode = getControllerNode
self.present = present self.present = present
@ -3088,7 +3094,9 @@ public final class DrawingToolsInteraction {
var actions: [ContextMenuAction] = [] var actions: [ContextMenuAction] = []
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Delete, accessibilityLabel: presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in 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 { 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 { if let entityView = entityView as? DrawingLocationEntityView {

View File

@ -36,7 +36,7 @@ private enum StatsSection: Int32 {
private enum StatsEntry: ItemListNodeEntry { private enum StatsEntry: ItemListNodeEntry {
case overviewTitle(PresentationTheme, String) case overviewTitle(PresentationTheme, String)
case overview(PresentationTheme, PostStats, Int32?) case overview(PresentationTheme, PostStats, EngineStoryItem.Views?, Int32?)
case interactionsTitle(PresentationTheme, String) case interactionsTitle(PresentationTheme, String)
case interactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Bool) case interactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Bool)
@ -89,8 +89,8 @@ private enum StatsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .overview(lhsTheme, lhsStats, lhsPublicShares): case let .overview(lhsTheme, lhsStats, lhsViews, lhsPublicShares):
if case let .overview(rhsTheme, rhsStats, rhsPublicShares) = rhs, lhsTheme === rhsTheme, lhsPublicShares == rhsPublicShares { 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 { if let lhsMessageStats = lhsStats as? MessageStats, let rhsMessageStats = rhsStats as? MessageStats {
return lhsMessageStats == rhsMessageStats return lhsMessageStats == rhsMessageStats
} else if let lhsStoryStats = lhsStats as? StoryStats, let rhsStoryStats = rhsStats as? StoryStats { } else if let lhsStoryStats = lhsStats as? StoryStats, let rhsStoryStats = rhsStats as? StoryStats {
@ -152,8 +152,8 @@ private enum StatsEntry: ItemListNodeEntry {
let .reactionsTitle(_, text), let .reactionsTitle(_, text),
let .publicForwardsTitle(_, text): let .publicForwardsTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .overview(_, stats, publicShares): case let .overview(_, stats, storyViews, publicShares):
return StatsOverviewItem(presentationData: presentationData, stats: stats as! Stats, publicShares: publicShares, sectionId: self.section, style: .blocks) 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): 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 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 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] = [] var entries: [StatsEntry] = []
if let data = data { if let data = data {
entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_MessageOverview.uppercased())) 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 var isStories = false
if let _ = data as? StoryStats { if let _ = data as? StoryStats {
@ -244,8 +251,6 @@ public enum StatsSubject {
} }
protocol PostStats { protocol PostStats {
var views: Int { get }
var forwards: Int { get }
var interactionsGraph: StatsGraph { get } var interactionsGraph: StatsGraph { get }
var interactionsGraphDelta: Int64 { get } var interactionsGraphDelta: Int64 { get }
var reactionsGraph: StatsGraph { get } var reactionsGraph: StatsGraph { get }
@ -378,15 +383,17 @@ public func messageStatsController(context: AccountContext, updatedPresentationD
} }
let title: String let title: String
var storyViews: EngineStoryItem.Views?
switch subject { switch subject {
case .message: case .message:
title = presentationData.strings.Stats_MessageTitle title = presentationData.strings.Stats_MessageTitle
case .story: case let .story(_, _, storyItem):
title = presentationData.strings.Stats_StoryTitle 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 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)) return (controllerState, (listState, arguments))
} }

View File

@ -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<Void, NoError>?, (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)
}
}

View File

@ -35,13 +35,15 @@ extension StoryStats: Stats {
class StatsOverviewItem: ListViewItem, ItemListItem { class StatsOverviewItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let stats: Stats let stats: Stats
let storyViews: EngineStoryItem.Views?
let publicShares: Int32? let publicShares: Int32?
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let style: ItemListStyle 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.presentationData = presentationData
self.stats = stats self.stats = stats
self.storyViews = storyViews
self.publicShares = publicShares self.publicShares = publicShares
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
@ -349,11 +351,11 @@ class StatsOverviewItemNode: ListViewItemNode {
) )
height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing 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( topLeftItemLayoutAndApply = makeTopLeftItemLayout(
params.width, params.width,
item.presentationData, item.presentationData,
compactNumericCountString(stats.views), compactNumericCountString(views.seenCount),
item.presentationData.strings.Stats_Message_Views, item.presentationData.strings.Stats_Message_Views,
nil nil
) )
@ -369,7 +371,7 @@ class StatsOverviewItemNode: ListViewItemNode {
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
params.width, params.width,
item.presentationData, item.presentationData,
compactNumericCountString(stats.reactions), compactNumericCountString(views.reactedCount),
item.presentationData.strings.Stats_Message_Reactions, item.presentationData.strings.Stats_Message_Reactions,
nil nil
) )
@ -377,7 +379,7 @@ class StatsOverviewItemNode: ListViewItemNode {
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout( middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
params.width, params.width,
item.presentationData, item.presentationData,
compactNumericCountString(stats.forwards), compactNumericCountString(views.forwardCount),
item.presentationData.strings.Stats_Message_PrivateShares, item.presentationData.strings.Stats_Message_PrivateShares,
nil nil
) )

View File

@ -5,32 +5,17 @@ import TelegramApi
import MtProtoKit import MtProtoKit
public struct StoryStats: Equatable { public struct StoryStats: Equatable {
public let views: Int
public let forwards: Int
public let reactions: Int
public let interactionsGraph: StatsGraph public let interactionsGraph: StatsGraph
public let interactionsGraphDelta: Int64 public let interactionsGraphDelta: Int64
public let reactionsGraph: StatsGraph public let reactionsGraph: StatsGraph
init(views: Int, forwards: Int, reactions: Int, interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) { init(interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) {
self.views = views
self.forwards = forwards
self.reactions = reactions
self.interactionsGraph = interactionsGraph self.interactionsGraph = interactionsGraph
self.interactionsGraphDelta = interactionsGraphDelta self.interactionsGraphDelta = interactionsGraphDelta
self.reactionsGraph = reactionsGraph self.reactionsGraph = reactionsGraph
} }
public static func == (lhs: StoryStats, rhs: StoryStats) -> Bool { 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 { if lhs.interactionsGraph != rhs.interactionsGraph {
return false return false
} }
@ -44,7 +29,7 @@ public struct StoryStats: Equatable {
} }
public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> StoryStats { 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<StoryStats?, NoError> in |> mapToSignal { data -> Signal<StoryStats?, NoError> in
guard let (statsDatacenterId, peer) = data, let peerReference = PeerReference(peer) else { guard let (statsDatacenterId, peer) = data, let inputPeer = apiInputPeer(peer) else {
return .never() return .never()
} }
return _internal_getStoriesById(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peerReference, ids: [storyId]) var flags: Int32 = 0
|> mapToSignal { stories -> Signal<StoryStats?, NoError> in if dark {
guard let storyItem = stories.first, case let .item(story) = storyItem, let inputPeer = apiInputPeer(peer) else { flags |= (1 << 1)
return .never() }
let request = Api.functions.stats.getStoryStats(flags: flags, peer: inputPeer, id: storyId)
let signal: Signal<Api.stats.StoryStats, MTRpcError>
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 {
var flags: Int32 = 0 signal = network.request(request)
if dark { }
flags |= (1 << 1)
} return signal
|> mapToSignal { result -> Signal<StoryStats?, MTRpcError> in
let request = Api.functions.stats.getStoryStats(flags: flags, peer: inputPeer, id: storyId) if case let .storyStats(apiInteractionsGraph, apiReactionsGraph) = result {
let signal: Signal<Api.stats.StoryStats, MTRpcError> let interactionsGraph = StatsGraph(apiStatsGraph: apiInteractionsGraph)
if network.datacenterId != statsDatacenterId { var interactionsGraphDelta: Int64 = 86400
signal = network.download(datacenterId: Int(statsDatacenterId), isMedia: false, tag: nil) if case let .Loaded(_, data) = interactionsGraph {
|> castError(MTRpcError.self) if let start = data.range(of: "[\"x\",") {
|> mapToSignal { worker in let substring = data.suffix(from: start.upperBound)
return worker.request(request) if let end = substring.range(of: "],") {
} let valuesString = substring.prefix(through: substring.index(before: end.lowerBound))
} else { let values = valuesString.components(separatedBy: ",").compactMap { Int64($0) }
signal = network.request(request) if values.count > 1 {
} let first = values[0]
let second = values[1]
var views: Int = 0 let delta = abs(second - first) / 1000
var forwards: Int = 0 interactionsGraphDelta = delta
var reactions: Int = 0
if let storyViews = story.views {
views = storyViews.seenCount
forwards = storyViews.forwardCount
reactions = storyViews.reactedCount
}
return signal
|> mapToSignal { result -> Signal<StoryStats?, MTRpcError> 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.count = 0
self.isLoadingMore = true
self.loadMore() self.loadMore()
} }

View File

@ -9,6 +9,12 @@ public enum EngineAudioTranscriptionResult {
case error 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<EngineAudioTranscriptionResult, NoError> { func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: MessageId) -> Signal<EngineAudioTranscriptionResult, NoError> {
return postbox.transaction { transaction -> Api.InputPeer? in return postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
@ -18,17 +24,24 @@ func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: Me
return .single(.error) return .single(.error)
} }
return network.request(Api.functions.messages.transcribeAudio(peer: inputPeer, msgId: messageId.id)) return network.request(Api.functions.messages.transcribeAudio(peer: inputPeer, msgId: messageId.id))
|> map { result -> Result<Api.messages.TranscribedAudio, AudioTranscriptionMessageAttribute.TranscriptionError> in |> map { result -> InternalAudioTranscriptionResult in
return .success(result) return .success(result)
} }
|> `catch` { error -> Signal<Result<Api.messages.TranscribedAudio, AudioTranscriptionMessageAttribute.TranscriptionError>, NoError> in |> `catch` { error -> Signal<InternalAudioTranscriptionResult, NoError> in
let mappedError: AudioTranscriptionMessageAttribute.TranscriptionError 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 mappedError = .tooLong
} else { } else {
mappedError = .generic mappedError = .generic
} }
return .single(.failure(mappedError)) return .single(.error(mappedError))
} }
|> mapToSignal { result -> Signal<EngineAudioTranscriptionResult, NoError> in |> mapToSignal { result -> Signal<EngineAudioTranscriptionResult, NoError> in
return postbox.transaction { transaction -> EngineAudioTranscriptionResult in return postbox.transaction { transaction -> EngineAudioTranscriptionResult in
@ -38,6 +51,7 @@ func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: Me
switch transcribedAudio { switch transcribedAudio {
case let .transcribedAudio(flags, transcriptionId, text, trialRemainingCount, trialUntilDate): case let .transcribedAudio(flags, transcriptionId, text, trialRemainingCount, trialUntilDate):
let isPending = (flags & (1 << 0)) != 0 let isPending = (flags & (1 << 0)) != 0
updatedAttribute = AudioTranscriptionMessageAttribute(id: transcriptionId, text: text, isPending: isPending, didRate: false, error: nil)
_internal_updateAudioTranscriptionTrialState(transaction: transaction) { current in _internal_updateAudioTranscriptionTrialState(transaction: transaction) { current in
var updated = current var updated = current
@ -50,10 +64,17 @@ func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: Me
} }
return updated 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) 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 transaction.updateMessage(messageId, update: { currentMessage in

View File

@ -23,36 +23,17 @@ extension MediaEditorScreen {
guard let controller, let mediaEditor = controller.node.mediaEditor else { guard let controller, let mediaEditor = controller.node.mediaEditor else {
return return
} }
let entitiesView = controller.node.entitiesView
if mediaEditor.values.additionalVideoPath != nil { if mediaEditor.values.additionalVideoPath != nil {
let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } controller.node.presentVideoRemoveConfirmation()
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))
return return
} }
if isActive { if isActive {
if controller.cameraAuthorizationStatus != .allowed || controller.microphoneAuthorizationStatus != .allowed {
controller.requestDeviceAccess()
return
}
guard self.recorder == nil else { guard self.recorder == nil else {
return return
} }

View File

@ -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 getCurrentImage: { [weak self] in
guard let mediaEditor = self?.mediaEditor else { guard let mediaEditor = self?.mediaEditor else {
return nil return nil
@ -3396,10 +3405,41 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}), in: .window(.root)) }), 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) { func presentTrackOptions(trackId: Int32, sourceView: UIView) {
let value = self.mediaEditor?.values.audioTrackVolume ?? 1.0 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] = [ let items: [ContextMenuItem] = [
.custom(VolumeSliderContextItem(minValue: 0.0, value: value, valueChanged: { [weak self] value, _ in .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 self {
if let mediaEditor = self.mediaEditor { if let mediaEditor = self.mediaEditor {
if trackId == 1 { if trackId == 1 {
mediaEditor.setAdditionalVideo(nil, positionChanges: []) self.presentVideoRemoveConfirmation()
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)
}
} else { } else {
mediaEditor.setAudioTrack(nil) mediaEditor.setAudioTrack(nil)
if !mediaEditor.sourceIsVideo && !mediaEditor.isPlaying { if !mediaEditor.sourceIsVideo && !mediaEditor.isPlaying {
mediaEditor.play() mediaEditor.play()
} }
@ -4013,7 +4043,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
private var audioSessionDisposable: Disposable? private var audioSessionDisposable: Disposable?
private let postingAvailabilityPromise = Promise<StoriesUploadAvailability>() private let postingAvailabilityPromise = Promise<StoriesUploadAvailability>()
private var postingAvailabilityDisposable: Disposable? private var postingAvailabilityDisposable: Disposable?
private var authorizationStatusDisposables = DisposableSet()
private(set) var cameraAuthorizationStatus: AccessType = .notDetermined
private(set) var microphoneAuthorizationStatus: AccessType = .notDetermined
public init( public init(
context: AccountContext, context: AccountContext,
subject: Signal<Subject?, NoError>, subject: Signal<Subject?, NoError>,
@ -4088,6 +4122,20 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let _ = forwardSource { if let _ = forwardSource {
self.postingAvailabilityPromise.set(self.context.engine.messages.checkStoriesUploadAvailability(target: .myStories)) 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) { required public init(coder aDecoder: NSCoder) {
@ -4098,6 +4146,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.exportDisposable.dispose() self.exportDisposable.dispose()
self.audioSessionDisposable?.dispose() self.audioSessionDisposable?.dispose()
self.postingAvailabilityDisposable?.dispose() self.postingAvailabilityDisposable?.dispose()
self.authorizationStatusDisposables.dispose()
} }
override public func loadDisplayNode() { override public func loadDisplayNode() {
@ -4176,6 +4225,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
fileprivate var isEmbeddedEditor: Bool { fileprivate var isEmbeddedEditor: Bool {
return self.isEditingStory || self.forwardSource != nil 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 = {}) { func openPrivacySettings(_ privacy: MediaEditorResultPrivacy? = nil, completion: @escaping () -> Void = {}) {
self.node.mediaEditor?.maybePauseVideo() self.node.mediaEditor?.maybePauseVideo()

View File

@ -1647,10 +1647,14 @@ public final class StoryItemSetContainerComponent: Component {
} }
var isChannel = false var isChannel = false
var canShare = true
var displayFooter = false var displayFooter = false
if case .channel = component.slice.peer { if case let .channel(channel) = component.slice.peer {
displayFooter = true displayFooter = true
isChannel = true isChannel = true
if channel.addressName == nil {
canShare = false
}
} else if component.slice.peer.id == component.context.account.peerId { } else if component.slice.peer.id == component.context.account.peerId {
displayFooter = true displayFooter = true
} else if component.slice.item.storyItem.isPending { } 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) return StoryFooterPanelComponent.MyReaction(reaction: value, file: centerAnimation, animationFileId: animationFileId)
}, },
isChannel: isChannel, isChannel: isChannel,
canShare: canShare,
externalViews: nil, externalViews: nil,
expandFraction: footerExpandFraction, expandFraction: footerExpandFraction,
expandViewStats: { [weak self] in expandViewStats: { [weak self] in
@ -1802,6 +1807,13 @@ public final class StoryItemSetContainerComponent: Component {
return return
} }
self.openStoryEditing(repost: true) 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: {}, environment: {},

View File

@ -41,6 +41,7 @@ public final class StoryFooterPanelComponent: Component {
public let storyItem: EngineStoryItem public let storyItem: EngineStoryItem
public let myReaction: MyReaction? public let myReaction: MyReaction?
public let isChannel: Bool public let isChannel: Bool
public let canShare: Bool
public let externalViews: EngineStoryItem.Views? public let externalViews: EngineStoryItem.Views?
public let expandFraction: CGFloat public let expandFraction: CGFloat
public let expandViewStats: () -> Void public let expandViewStats: () -> Void
@ -49,6 +50,7 @@ public final class StoryFooterPanelComponent: Component {
public let likeAction: () -> Void public let likeAction: () -> Void
public let forwardAction: () -> Void public let forwardAction: () -> Void
public let repostAction: () -> Void public let repostAction: () -> Void
public let cancelUploadAction: () -> Void
public init( public init(
context: AccountContext, context: AccountContext,
@ -57,6 +59,7 @@ public final class StoryFooterPanelComponent: Component {
storyItem: EngineStoryItem, storyItem: EngineStoryItem,
myReaction: MyReaction?, myReaction: MyReaction?,
isChannel: Bool, isChannel: Bool,
canShare: Bool,
externalViews: EngineStoryItem.Views?, externalViews: EngineStoryItem.Views?,
expandFraction: CGFloat, expandFraction: CGFloat,
expandViewStats: @escaping () -> Void, expandViewStats: @escaping () -> Void,
@ -64,7 +67,8 @@ public final class StoryFooterPanelComponent: Component {
moreAction: @escaping (UIView, ContextGesture?) -> Void, moreAction: @escaping (UIView, ContextGesture?) -> Void,
likeAction: @escaping () -> Void, likeAction: @escaping () -> Void,
forwardAction: @escaping () -> Void, forwardAction: @escaping () -> Void,
repostAction: @escaping () -> Void repostAction: @escaping () -> Void,
cancelUploadAction: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
@ -72,6 +76,7 @@ public final class StoryFooterPanelComponent: Component {
self.storyItem = storyItem self.storyItem = storyItem
self.myReaction = myReaction self.myReaction = myReaction
self.isChannel = isChannel self.isChannel = isChannel
self.canShare = canShare
self.externalViews = externalViews self.externalViews = externalViews
self.expandViewStats = expandViewStats self.expandViewStats = expandViewStats
self.expandFraction = expandFraction self.expandFraction = expandFraction
@ -80,6 +85,7 @@ public final class StoryFooterPanelComponent: Component {
self.likeAction = likeAction self.likeAction = likeAction
self.forwardAction = forwardAction self.forwardAction = forwardAction
self.repostAction = repostAction self.repostAction = repostAction
self.cancelUploadAction = cancelUploadAction
} }
public static func ==(lhs: StoryFooterPanelComponent, rhs: StoryFooterPanelComponent) -> Bool { public static func ==(lhs: StoryFooterPanelComponent, rhs: StoryFooterPanelComponent) -> Bool {
@ -227,7 +233,7 @@ public final class StoryFooterPanelComponent: Component {
guard let component = self.component else { guard let component = self.component else {
return return
} }
component.context.engine.messages.cancelStoryUpload(stableId: component.storyItem.id) component.cancelUploadAction()
} }
func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
@ -428,23 +434,7 @@ public final class StoryFooterPanelComponent: Component {
likeButton = ComponentView() likeButton = ComponentView()
self.likeButton = likeButton self.likeButton = likeButton
} }
let repostButton: ComponentView<Empty>
if let current = self.repostButton {
repostButton = current
} else {
repostButton = ComponentView()
self.repostButton = repostButton
}
let forwardButton: ComponentView<Empty>
if let current = self.forwardButton {
forwardButton = current
} else {
forwardButton = ComponentView()
self.forwardButton = forwardButton
}
let likeButtonSize = likeButton.update( let likeButtonSize = likeButton.update(
transition: likeStatsTransition, transition: likeStatsTransition,
component: AnyComponent(MessageInputActionButtonComponent( component: AnyComponent(MessageInputActionButtonComponent(
@ -500,100 +490,127 @@ public final class StoryFooterPanelComponent: Component {
rightContentOffset -= likeButtonSize.width + 14.0 rightContentOffset -= likeButtonSize.width + 14.0
} }
let repostButtonSize = repostButton.update( if component.canShare {
transition: likeStatsTransition, let repostButton: ComponentView<Empty>
component: AnyComponent(MessageInputActionButtonComponent( if let current = self.repostButton {
mode: .repost, repostButton = current
storyId: component.storyItem.id, } else {
action: { [weak self] _, action, _ in repostButton = ComponentView()
guard let self, let component = self.component else { self.repostButton = repostButton
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) let forwardButton: ComponentView<Empty>
likeStatsTransition.setBounds(view: repostButtonView, bounds: CGRect(origin: CGPoint(), size: repostButtonFrame.size)) if let current = self.forwardButton {
likeStatsTransition.setAlpha(view: repostButtonView, alpha: 1.0 - component.expandFraction) forwardButton = current
} else {
rightContentOffset -= repostButtonSize.width + 14.0 forwardButton = ComponentView()
} self.forwardButton = forwardButton
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) let repostButtonSize = repostButton.update(
likeStatsTransition.setBounds(view: forwardButtonView, bounds: CGRect(origin: CGPoint(), size: forwardButtonFrame.size)) transition: likeStatsTransition,
likeStatsTransition.setAlpha(view: forwardButtonView, alpha: 1.0 - component.expandFraction) 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 { } else {
if let likeButton = self.likeButton { if let likeButton = self.likeButton {
@ -917,7 +934,11 @@ public final class StoryFooterPanelComponent: Component {
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
} }
component.deleteAction() if component.storyItem.isPending {
component.cancelUploadAction()
} else {
component.deleteAction()
}
} }
).minSize(CGSize(width: 44.0, height: baseHeight))), ).minSize(CGSize(width: 44.0, height: baseHeight))),
environment: {}, environment: {},

View File

@ -263,7 +263,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
private var isLoadingValue: Bool = false private var isLoadingValue: Bool = false
private var isLoadingEarlier: Bool = false private var isLoadingEarlier: Bool = false
private func updateIsLoading(isLoading: Bool, earlier: Bool, animated: Bool) { 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) let updated = isLoading != self.isLoadingValue || (isLoading && earlier && !self.isLoadingEarlier)

View File

@ -483,6 +483,32 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) { if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) {
rootTabController.selectedIndex = index rootTabController.selectedIndex = index
} }
if forwardInfo != nil {
var viewControllers = self.viewControllers
var dismissNext = false
var range: Range<Int>?
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 let completionImpl: () -> Void = { [weak self] in