mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various fixes
This commit is contained in:
parent
de1ebed57c
commit
28e3525091
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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<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 _internal_getStoriesById(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peerReference, ids: [storyId])
|
||||
|> mapToSignal { stories -> Signal<StoryStats?, NoError> 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<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)
|
||||
}
|
||||
|
||||
var flags: Int32 = 0
|
||||
if dark {
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
|
||||
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 {
|
||||
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<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
|
||||
}
|
||||
} else {
|
||||
signal = network.request(request)
|
||||
}
|
||||
|
||||
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.isLoadingMore = true
|
||||
|
||||
self.loadMore()
|
||||
}
|
||||
|
||||
|
@ -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<EngineAudioTranscriptionResult, NoError> {
|
||||
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<Api.messages.TranscribedAudio, AudioTranscriptionMessageAttribute.TranscriptionError> in
|
||||
|> map { result -> InternalAudioTranscriptionResult in
|
||||
return .success(result)
|
||||
}
|
||||
|> `catch` { error -> Signal<Result<Api.messages.TranscribedAudio, AudioTranscriptionMessageAttribute.TranscriptionError>, NoError> in
|
||||
|> `catch` { error -> Signal<InternalAudioTranscriptionResult, NoError> 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<EngineAudioTranscriptionResult, NoError> 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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<StoriesUploadAvailability>()
|
||||
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<Subject?, NoError>,
|
||||
@ -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()
|
||||
|
@ -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: {},
|
||||
|
@ -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<Empty>, transition: Transition) -> CGSize {
|
||||
@ -428,23 +434,7 @@ public final class StoryFooterPanelComponent: Component {
|
||||
likeButton = ComponentView()
|
||||
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(
|
||||
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<Empty>
|
||||
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<Empty>
|
||||
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: {},
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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<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
|
||||
|
Loading…
x
Reference in New Issue
Block a user