Merge commit '2b85da99851cf417600c23dd7a8c92f3c55619e0'

This commit is contained in:
Isaac 2023-11-27 00:54:16 +04:00
commit d46368ccfe
27 changed files with 647 additions and 723 deletions

View File

@ -10489,6 +10489,7 @@ Sorry for the inconvenience.";
"Chat.SimilarChannels" = "Similar Channels"; "Chat.SimilarChannels" = "Similar Channels";
"Chat.SimilarChannels.Join" = "Join"; "Chat.SimilarChannels.Join" = "Join";
"Chat.SimilarChannels.JoinedChannel" = "You joined channel **%@**."; "Chat.SimilarChannels.JoinedChannel" = "You joined channel **%@**.";
"Chat.SimilarChannels.MoreChannels" = "More Channels";
"Wallpaper.ApplyForMe" = "Apply for Me"; "Wallpaper.ApplyForMe" = "Apply for Me";
"Wallpaper.ApplyForBoth" = "Apply for Me and %@"; "Wallpaper.ApplyForBoth" = "Apply for Me and %@";
@ -10548,3 +10549,7 @@ Sorry for the inconvenience.";
"MediaEditor.VideoRemovalConfirmation" = "Are you sure you want to delete video message?"; "MediaEditor.VideoRemovalConfirmation" = "Are you sure you want to delete video message?";
"MediaEditor.HoldToRecordVideo" = "Hold to record video"; "MediaEditor.HoldToRecordVideo" = "Hold to record video";
"Chat.ChannelRecommendation.PremiumTooltip" = "Subcribe to [Telegram Premium]() to unlock up to **100** channels.";
"Story.ForwardAuthorHiddenTooltip" = "The account was hidden by the user";

View File

@ -117,6 +117,7 @@ private final class CameraContext {
private var invalidated = false private var invalidated = false
private let detectedCodesPipe = ValuePipe<[CameraCode]>() private let detectedCodesPipe = ValuePipe<[CameraCode]>()
private let audioLevelPipe = ValuePipe<Float>()
fileprivate let modeChangePromise = ValuePromise<Camera.ModeChange>(.none) fileprivate let modeChangePromise = ValuePromise<Camera.ModeChange>(.none)
var previewView: CameraPreviewView? var previewView: CameraPreviewView?
@ -281,6 +282,10 @@ private final class CameraContext {
} }
} }
private var micLevelPeak: Int16 = 0
private var micLevelPeakCount = 0
private var isDualCameraEnabled: Bool? private var isDualCameraEnabled: Bool?
public func setDualCameraEnabled(_ enabled: Bool, change: Bool = true) { public func setDualCameraEnabled(_ enabled: Bool, change: Bool = true) {
guard enabled != self.isDualCameraEnabled else { guard enabled != self.isDualCameraEnabled else {
@ -352,6 +357,48 @@ private final class CameraContext {
self.lastSnapshotTimestamp = timestamp self.lastSnapshotTimestamp = timestamp
} }
} }
if self.initialConfiguration.reportAudioLevel {
self.mainDeviceContext?.output.processAudioBuffer = { [weak self] sampleBuffer in
guard let self else {
return
}
var blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer)
let numSamplesInBuffer = CMSampleBufferGetNumSamples(sampleBuffer)
var audioBufferList = AudioBufferList()
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, bufferListSizeNeededOut: nil, bufferListOut: &audioBufferList, bufferListSize: MemoryLayout<AudioBufferList>.size, blockBufferAllocator: nil, blockBufferMemoryAllocator: nil, flags: kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, blockBufferOut: &blockBuffer)
// for bufferCount in 0..<Int(audioBufferList.mNumberBuffers) {
let buffer = audioBufferList.mBuffers.mData
let size = audioBufferList.mBuffers.mDataByteSize
if let data = buffer?.bindMemory(to: Int16.self, capacity: Int(size)) {
processWaveformPreview(samples: data, count: numSamplesInBuffer)
}
// }
func processWaveformPreview(samples: UnsafePointer<Int16>, count: Int) {
for i in 0..<count {
var sample = samples[i]
if sample < 0 {
sample = -sample
}
if self.micLevelPeak < sample {
self.micLevelPeak = sample
}
self.micLevelPeakCount += 1
if self.micLevelPeakCount >= 1200 {
let level = Float(self.micLevelPeak) / 4000.0
self.audioLevelPipe.putNext(level)
self.micLevelPeak = 0
self.micLevelPeakCount = 0
}
}
}
}
}
self.mainDeviceContext?.output.processCodes = { [weak self] codes in self.mainDeviceContext?.output.processCodes = { [weak self] codes in
self?.detectedCodesPipe.putNext(codes) self?.detectedCodesPipe.putNext(codes)
} }
@ -526,6 +573,10 @@ private final class CameraContext {
return self.detectedCodesPipe.signal() return self.detectedCodesPipe.signal()
} }
var audioLevel: Signal<Float, NoError> {
return self.audioLevelPipe.signal()
}
@objc private func sessionInterruptionEnded(notification: NSNotification) { @objc private func sessionInterruptionEnded(notification: NSNotification) {
} }
@ -564,8 +615,9 @@ public final class Camera {
let metadata: Bool let metadata: Bool
let preferredFps: Double let preferredFps: Double
let preferWide: Bool let preferWide: Bool
let reportAudioLevel: Bool
public init(preset: Preset, position: Position, isDualEnabled: Bool = false, audio: Bool, photo: Bool, metadata: Bool, preferredFps: Double, preferWide: Bool = false) { public init(preset: Preset, position: Position, isDualEnabled: Bool = false, audio: Bool, photo: Bool, metadata: Bool, preferredFps: Double, preferWide: Bool = false, reportAudioLevel: Bool = false) {
self.preset = preset self.preset = preset
self.position = position self.position = position
self.isDualEnabled = isDualEnabled self.isDualEnabled = isDualEnabled
@ -574,6 +626,7 @@ public final class Camera {
self.metadata = metadata self.metadata = metadata
self.preferredFps = preferredFps self.preferredFps = preferredFps
self.preferWide = preferWide self.preferWide = preferWide
self.reportAudioLevel = reportAudioLevel
} }
} }
@ -865,6 +918,20 @@ public final class Camera {
} }
} }
public var audioLevel: Signal<Float, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.queue.async {
if let context = self.contextRef?.takeUnretainedValue() {
disposable.set(context.audioLevel.start(next: { codes in
subscriber.putNext(codes)
}))
}
}
return disposable
}
}
public enum ModeChange: Equatable { public enum ModeChange: Equatable {
case none case none
case position case position

View File

@ -96,6 +96,7 @@ final class CameraOutput: NSObject {
private var videoRecorder: VideoRecorder? private var videoRecorder: VideoRecorder?
var processSampleBuffer: ((CMSampleBuffer, CVImageBuffer, AVCaptureConnection) -> Void)? var processSampleBuffer: ((CMSampleBuffer, CVImageBuffer, AVCaptureConnection) -> Void)?
var processAudioBuffer: ((CMSampleBuffer) -> Void)?
var processCodes: (([CameraCode]) -> Void)? var processCodes: (([CameraCode]) -> Void)?
init(exclusive: Bool) { init(exclusive: Bool) {
@ -379,6 +380,8 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA
if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection) self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection)
} else {
self.processAudioBuffer?(sampleBuffer)
} }
if let videoRecorder = self.videoRecorder, videoRecorder.isRecording { if let videoRecorder = self.videoRecorder, videoRecorder.isRecording {

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,6 +2984,7 @@ 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?
@ -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,8 +3094,10 @@ 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 {
if self.shouldDeleteEntity(entityView.entity) {
self.entitiesView.remove(uuid: entityView.entity.uuid, animated: true) self.entitiesView.remove(uuid: entityView.entity.uuid, animated: true)
} }
}
})) }))
if let entityView = entityView as? DrawingLocationEntityView { if let entityView = entityView as? DrawingLocationEntityView {
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Edit, accessibilityLabel: presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Edit, accessibilityLabel: presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in

View File

@ -47,7 +47,8 @@ public final class EntityVideoRecorder {
photo: false, photo: false,
metadata: false, metadata: false,
preferredFps: 60.0, preferredFps: 60.0,
preferWide: true preferWide: true,
reportAudioLevel: true
), ),
previewView: self.previewView, previewView: self.previewView,
secondaryPreviewView: nil secondaryPreviewView: nil
@ -73,7 +74,7 @@ public final class EntityVideoRecorder {
} }
} }
self.micLevelPromise.set(.single(0.0)) self.micLevelPromise.set(camera.audioLevel)
let start = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0 let start = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0
mediaEditor.stop() mediaEditor.stop()

View File

@ -468,6 +468,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
let hasTopGroupInset: Bool let hasTopGroupInset: Bool
let noInsets: Bool let noInsets: Bool
let noCorners: Bool let noCorners: Bool
let style: ItemListStyle
public let tag: ItemListItemTag? public let tag: ItemListItemTag?
let header: ListViewItemHeader? let header: ListViewItemHeader?
let shimmering: ItemListPeerItemShimmering? let shimmering: ItemListPeerItemShimmering?
@ -508,6 +509,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
hasTopGroupInset: Bool = true, hasTopGroupInset: Bool = true,
noInsets: Bool = false, noInsets: Bool = false,
noCorners: Bool = false, noCorners: Bool = false,
style: ItemListStyle = .blocks,
tag: ItemListItemTag? = nil, tag: ItemListItemTag? = nil,
header: ListViewItemHeader? = nil, header: ListViewItemHeader? = nil,
shimmering: ItemListPeerItemShimmering? = nil, shimmering: ItemListPeerItemShimmering? = nil,
@ -547,6 +549,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
self.hasTopGroupInset = hasTopGroupInset self.hasTopGroupInset = hasTopGroupInset
self.noInsets = noInsets self.noInsets = noInsets
self.noCorners = noCorners self.noCorners = noCorners
self.style = style
self.tag = tag self.tag = tag
self.header = header self.header = header
self.shimmering = shimmering self.shimmering = shimmering
@ -588,6 +591,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
hasTopGroupInset: Bool = true, hasTopGroupInset: Bool = true,
noInsets: Bool = false, noInsets: Bool = false,
noCorners: Bool = false, noCorners: Bool = false,
style: ItemListStyle = .blocks,
tag: ItemListItemTag? = nil, tag: ItemListItemTag? = nil,
header: ListViewItemHeader? = nil, header: ListViewItemHeader? = nil,
shimmering: ItemListPeerItemShimmering? = nil, shimmering: ItemListPeerItemShimmering? = nil,
@ -627,6 +631,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
self.hasTopGroupInset = hasTopGroupInset self.hasTopGroupInset = hasTopGroupInset
self.noInsets = noInsets self.noInsets = noInsets
self.noCorners = noCorners self.noCorners = noCorners
self.style = style
self.tag = tag self.tag = tag
self.header = header self.header = header
self.shimmering = shimmering self.shimmering = shimmering
@ -889,7 +894,6 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
return { item, params, neighbors, headerAtTop in return { item, params, neighbors, headerAtTop in
var updateArrowImage: UIImage? var updateArrowImage: UIImage?
var updatedTheme: PresentationTheme?
let statusFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0) let statusFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)
let labelFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0) let labelFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0)
@ -938,7 +942,6 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
let badgeDiameter: CGFloat = 20.0 let badgeDiameter: CGFloat = 20.0
if currentItem?.presentationData.theme !== item.presentationData.theme { if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
if let badgeColor = badgeColor { if let badgeColor = badgeColor {
updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor)
@ -1247,13 +1250,22 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
strongSelf.labelArrowNode?.image = updateArrowImage strongSelf.labelArrowNode?.image = updateArrowImage
} }
if let _ = updatedTheme { let itemBackgroundColor: UIColor
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor let itemSeparatorColor: UIColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor switch item.style {
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor case .plain:
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
} }
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
let revealOffset = strongSelf.revealOffset let revealOffset = strongSelf.revealOffset
let transition: ContainedViewLayoutTransition let transition: ContainedViewLayoutTransition

View File

@ -563,6 +563,27 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
_hidesPanelOnLock = true; _hidesPanelOnLock = true;
} }
+ (UIImage *)stopIconImage
{
static dispatch_once_t onceToken;
static UIImage *iconImage;
dispatch_once(&onceToken, ^
{
CGRect rect = CGRectMake(0, 0, 22.0f, 22.0f);
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 22, 22) cornerRadius:7].CGPath);
CGContextSetFillColorWithColor(context, UIColorRGBA(0x0ffffff, 1.3f).CGColor);
CGContextFillPath(context);
iconImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
});
return iconImage;
}
- (void)animateLock { - (void)animateLock {
if (!_animatedIn) { if (!_animatedIn) {
return; return;
@ -575,8 +596,9 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
snapshotView.frame = _innerIconView.frame; snapshotView.frame = _innerIconView.frame;
[_innerIconWrapperView insertSubview:snapshotView atIndex:0]; [_innerIconWrapperView insertSubview:snapshotView atIndex:0];
UIImage *icon = _hidesPanelOnLock ? [TGModernConversationInputMicButton stopIconImage] : TGComponentsImageNamed(@"RecordSendIcon");
_previousIcon = _innerIconView.image; _previousIcon = _innerIconView.image;
[self setIcon:TGTintedImage(TGComponentsImageNamed(@"RecordSendIcon"), _pallete != nil ? _pallete.iconColor : [UIColor whiteColor])]; [self setIcon:TGTintedImage(icon, _pallete != nil && !_hidesPanelOnLock ? _pallete.iconColor : [UIColor whiteColor])];
_currentScale = 1; _currentScale = 1;
_cancelTargetTranslation = 0; _cancelTargetTranslation = 0;

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,15 +46,9 @@ 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])
|> 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 var flags: Int32 = 0
if dark { if dark {
flags |= (1 << 1) flags |= (1 << 1)
@ -87,15 +66,6 @@ private func requestStoryStats(accountPeerId: PeerId, postbox: Postbox, network:
signal = network.request(request) 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 return signal
|> mapToSignal { result -> Signal<StoryStats?, MTRpcError> in |> mapToSignal { result -> Signal<StoryStats?, MTRpcError> in
if case let .storyStats(apiInteractionsGraph, apiReactionsGraph) = result { if case let .storyStats(apiInteractionsGraph, apiReactionsGraph) = result {
@ -118,9 +88,6 @@ private func requestStoryStats(accountPeerId: PeerId, postbox: Postbox, network:
} }
let reactionsGraph = StatsGraph(apiStatsGraph: apiReactionsGraph) let reactionsGraph = StatsGraph(apiStatsGraph: apiReactionsGraph)
return .single(StoryStats( return .single(StoryStats(
views: views,
forwards: forwards,
reactions: reactions,
interactionsGraph: interactionsGraph, interactionsGraph: interactionsGraph,
interactionsGraphDelta: interactionsGraphDelta, interactionsGraphDelta: interactionsGraphDelta,
reactionsGraph: reactionsGraph reactionsGraph: reactionsGraph
@ -132,7 +99,6 @@ private func requestStoryStats(accountPeerId: PeerId, postbox: Postbox, network:
|> retryRequest |> retryRequest
} }
} }
}
private final class StoryStatsContextImpl { private final class StoryStatsContextImpl {
private let accountPeerId: EnginePeer.Id private let accountPeerId: EnginePeer.Id
@ -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

@ -109,6 +109,17 @@ public enum ChatHistoryEntry: Identifiable, Comparable {
} }
} }
public var timestamp: Int32? {
switch self {
case let .MessageEntry(message, _, _, _, _, _):
return message.timestamp
case let .MessageGroupEntry(_, messages, _):
return messages[0].0.timestamp
default:
return nil
}
}
public static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { public static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool {
switch lhs { switch lhs {
case let .MessageEntry(lhsMessage, lhsPresentationData, lhsRead, _, lhsSelection, lhsAttributes): case let .MessageEntry(lhsMessage, lhsPresentationData, lhsRead, _, lhsSelection, lhsAttributes):

View File

@ -342,7 +342,7 @@ public class ChatMessageJoinedChannelBubbleContentNode: ChatMessageBubbleContent
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController( let controller = UndoOverlayController(
presentationData: presentationData, presentationData: presentationData,
content: .premiumPaywall(title: nil, text: "Subcribe to [Telegram Premium]() to unlock up to **100** channels.", customUndoText: nil, timeout: nil, linkAction: nil), content: .premiumPaywall(title: nil, text: item.presentationData.strings.Chat_ChannelRecommendation_PremiumTooltip, customUndoText: nil, timeout: nil, linkAction: nil),
elevatedLayout: false, elevatedLayout: false,
action: { [weak self] action in action: { [weak self] action in
if case .info = action { if case .info = action {
@ -544,11 +544,6 @@ private class MessageBackgroundNode: ASDisplayNode {
private let itemSize = CGSize(width: 84.0, height: 90.0) private let itemSize = CGSize(width: 84.0, height: 90.0)
private final class ChannelItemComponent: Component { private final class ChannelItemComponent: Component {
class ExternalState {
var cachedPlaceholderImage: UIImage?
}
let externalState: ExternalState
let context: AccountContext let context: AccountContext
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
@ -561,7 +556,6 @@ private final class ChannelItemComponent: Component {
let contextAction: ((EnginePeer, UIView, ContextGesture?) -> Void)? let contextAction: ((EnginePeer, UIView, ContextGesture?) -> Void)?
init( init(
externalState: ExternalState,
context: AccountContext, context: AccountContext,
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
@ -573,7 +567,6 @@ private final class ChannelItemComponent: Component {
openMore: @escaping () -> Void, openMore: @escaping () -> Void,
contextAction: ((EnginePeer, UIView, ContextGesture?) -> Void)? contextAction: ((EnginePeer, UIView, ContextGesture?) -> Void)?
) { ) {
self.externalState = externalState
self.context = context self.context = context
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
@ -677,10 +670,13 @@ private final class ChannelItemComponent: Component {
} }
func update(component: ChannelItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: ChannelItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let previousComponent = self.component
self.component = component self.component = component
self.state = state self.state = state
self.contextContainer.isGestureEnabled = component.contextAction != nil let themeUpdated = previousComponent?.theme !== component.theme
self.contextContainer.isGestureEnabled = true
let titleSize = self.title.update( let titleSize = self.title.update(
transition: .immediate, transition: .immediate,
@ -776,23 +772,11 @@ private final class ChannelItemComponent: Component {
} }
self.circlesView.isHidden = false self.circlesView.isHidden = false
if self.circlesView.image == nil { if self.circlesView.image == nil || themeUpdated {
if let current = component.externalState.cachedPlaceholderImage {
self.circlesView.image = current
} else {
let image = generateImage(CGSize(width: 50.0, height: avatarSize.height), rotatedContext: { size, context in let image = generateImage(CGSize(width: 50.0, height: avatarSize.height), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size)) context.clear(CGRect(origin: .zero, size: size))
let randomColors: [(UInt32, UInt32)] = [ let color = component.theme.chat.message.incoming.secondaryTextColor.withMultipliedAlpha(0.35)
(0x4493de, 0x52d5d9),
(0xfcc418, 0xf6774a),
(0xffc9a2, 0xfbedb2),
(0x133e88, 0x131925),
(0x63c7f0, 0xf6c506),
(0x88a5cb, 0x162639),
(0xd669ed, 0xe0a2f3),
(0x54cb68, 0xa0de7e)
]
context.saveGState() context.saveGState()
@ -800,15 +784,8 @@ private final class ChannelItemComponent: Component {
context.addEllipse(in: rect1) context.addEllipse(in: rect1)
context.clip() context.clip()
var firstColors: NSArray = [] context.setFillColor(color.cgColor)
if let random = randomColors.randomElement() { context.fill(rect1)
firstColors = [UIColor(rgb: random.0).cgColor, UIColor(rgb: random.1).cgColor]
}
var locations: [CGFloat] = [1.0, 0.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let firstGradient = CGGradient(colorsSpace: colorSpace, colors: firstColors as CFArray, locations: &locations)!
context.drawLinearGradient(firstGradient, start: CGPoint(x: rect1.minX, y: rect1.minY), end: CGPoint(x: rect1.maxX, y: rect1.maxY), options: CGGradientDrawingOptions())
context.restoreGState() context.restoreGState()
@ -823,23 +800,16 @@ private final class ChannelItemComponent: Component {
context.addEllipse(in: rect2) context.addEllipse(in: rect2)
context.clip() context.clip()
var secondColors: NSArray = [] context.setFillColor(color.cgColor)
if let random = randomColors.randomElement() { context.fill(rect2)
secondColors = [UIColor(rgb: random.0).cgColor, UIColor(rgb: random.1).cgColor]
}
let secondGradient = CGGradient(colorsSpace: colorSpace, colors: secondColors as CFArray, locations: &locations)!
context.drawLinearGradient(secondGradient, start: CGPoint(x: rect2.minX, y: rect2.minY), end: CGPoint(x: rect2.minX, y: rect2.maxY), options: CGGradientDrawingOptions())
context.restoreGState() context.restoreGState()
context.setBlendMode(.clear) context.setBlendMode(.clear)
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - avatarSize.width - 22.0, y: -2.0), size: CGSize(width: avatarSize.width + 4.0, height: avatarSize.height + 4.0))) context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - avatarSize.width - 22.0, y: -2.0), size: CGSize(width: avatarSize.width + 4.0, height: avatarSize.height + 4.0)))
}) })
component.externalState.cachedPlaceholderImage = image
self.circlesView.image = image self.circlesView.image = image
} }
}
self.circlesView.frame = CGRect(origin: CGPoint(x: avatarFrame.midX, y: 0.0), size: CGSize(width: 50.0, height: 60.0)) self.circlesView.frame = CGRect(origin: CGPoint(x: avatarFrame.midX, y: 0.0), size: CGSize(width: 50.0, height: 60.0))
} else { } else {
if self.circlesView.superview != nil { if self.circlesView.superview != nil {
@ -991,7 +961,6 @@ final class ChannelListPanelComponent: Component {
private let measureItem = ComponentView<Empty>() private let measureItem = ComponentView<Empty>()
private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:] private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
private var externalState = ChannelItemComponent.ExternalState()
private var ignoreScrolling: Bool = false private var ignoreScrolling: Bool = false
@ -1072,7 +1041,7 @@ final class ChannelListPanelComponent: Component {
if !component.context.isPremium { if !component.context.isPremium {
isLocked = true isLocked = true
} }
title = isLocked ? "Unlock More Channels" : "View More Channels" title = component.strings.Chat_SimilarChannels_MoreChannels
subtitle = "+\(component.peers.count - channelsLimit)" subtitle = "+\(component.peers.count - channelsLimit)"
isLast = true isLast = true
} else { } else {
@ -1092,7 +1061,6 @@ final class ChannelListPanelComponent: Component {
let _ = itemView.update( let _ = itemView.update(
transition: itemTransition, transition: itemTransition,
component: AnyComponent(ChannelItemComponent( component: AnyComponent(ChannelItemComponent(
externalState: self.externalState,
context: component.context, context: component.context,
theme: component.theme, theme: component.theme,
strings: component.strings, strings: component.strings,

View File

@ -1,11 +1,14 @@
import Foundation import Foundation
import UIKit import UIKit
import Display import Display
import SwiftSignalKit
import MediaEditor import MediaEditor
import DrawingUI import DrawingUI
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import PresentationDataUtils import PresentationDataUtils
import TelegramPresentationData import TelegramPresentationData
import DeviceAccess
import AccountContext
extension MediaEditorScreen { extension MediaEditorScreen {
final class Recording { final class Recording {
@ -13,46 +16,73 @@ extension MediaEditorScreen {
private var recorder: EntityVideoRecorder? private var recorder: EntityVideoRecorder?
private let idleTimerExtensionDisposable = MetaDisposable()
private var authorizationStatusDisposables = DisposableSet()
private var cameraAuthorizationStatus: AccessType = .notDetermined
private var microphoneAuthorizationStatus: AccessType = .notDetermined
fileprivate var cameraIsActive = true {
didSet {
guard let context = self.controller?.context else {
return
}
if self.cameraIsActive {
self.idleTimerExtensionDisposable.set(context.sharedContext.applicationBindings.pushIdleTimerExtension())
} else {
self.idleTimerExtensionDisposable.set(nil)
}
}
}
var isLocked = false var isLocked = false
init(controller: MediaEditorScreen) { init(controller: MediaEditorScreen) {
self.controller = controller self.controller = controller
self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .camera(.video))
|> deliverOnMainQueue).start(next: { [weak self] status in
if let self {
self.cameraAuthorizationStatus = status
}
}))
self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .microphone(.video))
|> deliverOnMainQueue).start(next: { [weak self] status in
if let self {
self.microphoneAuthorizationStatus = status
}
}))
}
deinit {
self.idleTimerExtensionDisposable.dispose()
self.authorizationStatusDisposables.dispose()
}
func requestDeviceAccess() {
DeviceAccess.authorizeAccess(to: .camera(.video), { granted in
if granted {
DeviceAccess.authorizeAccess(to: .microphone(.video))
}
})
} }
func setMediaRecordingActive(_ isActive: Bool, finished: Bool, sourceView: UIView?) { func setMediaRecordingActive(_ isActive: Bool, finished: Bool, sourceView: UIView?) {
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 self.cameraAuthorizationStatus != .allowed || self.microphoneAuthorizationStatus != .allowed {
self.requestDeviceAccess()
return
}
guard self.recorder == nil else { guard self.recorder == nil else {
return return
} }
@ -72,6 +102,8 @@ extension MediaEditorScreen {
} }
self.recorder = recorder self.recorder = recorder
controller.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2)) controller.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2))
self.cameraIsActive = true
} else { } else {
if let recorder = self.recorder { if let recorder = self.recorder {
recorder.stopRecording(save: finished, completion: { [weak self] in recorder.stopRecording(save: finished, completion: { [weak self] in
@ -84,6 +116,8 @@ extension MediaEditorScreen {
}) })
controller.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2)) controller.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2))
self.cameraIsActive = false
} else { } else {
guard self.tooltipController == nil, let sourceView else { guard self.tooltipController == nil, let sourceView 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()
} }

View File

@ -458,6 +458,9 @@ final class MediaScrubberComponent: Component {
var endPosition = self.endPosition var endPosition = self.endPosition
var trimViewOffset: CGFloat = 0.0 var trimViewOffset: CGFloat = 0.0
var trimViewVisualInsets: UIEdgeInsets = .zero var trimViewVisualInsets: UIEdgeInsets = .zero
var trackViewWidth: CGFloat = availableSize.width
var mainTrimDuration = self.trimDuration
if let track = component.tracks.first(where: { $0.id == self.selectedTrackId }), track.id != 0 { if let track = component.tracks.first(where: { $0.id == self.selectedTrackId }), track.id != 0 {
if let trimRange = track.trimRange { if let trimRange = track.trimRange {
startPosition = trimRange.lowerBound startPosition = trimRange.lowerBound
@ -472,15 +475,22 @@ final class MediaScrubberComponent: Component {
trimViewOffset = -delta trimViewOffset = -delta
trimViewVisualInsets.left = delta trimViewVisualInsets.left = delta
} }
if lowestVideoId == 0 && track.id == 1 {
trimViewVisualInsets = .zero
trackViewWidth = trackView.containerView.frame.width
mainTrimDuration = track.duration
}
} }
} }
let scrubberSize = CGSize(width: availableSize.width, height: trackHeight) let scrubberSize = CGSize(width: availableSize.width, height: trackHeight)
self.trimView.isHollow = self.selectedTrackId != lowestVideoId || self.isAudioOnly self.trimView.isHollow = self.selectedTrackId != lowestVideoId || self.isAudioOnly
let (leftHandleFrame, rightHandleFrame) = self.trimView.update( let (leftHandleFrame, rightHandleFrame) = self.trimView.update(
visualInsets: trimViewVisualInsets, visualInsets: trimViewVisualInsets,
scrubberSize: scrubberSize, scrubberSize: CGSize(width: trackViewWidth, height: trackHeight),
duration: trimDuration, duration: mainTrimDuration,
startPosition: startPosition, startPosition: startPosition,
endPosition: endPosition, endPosition: endPosition,
position: component.position, position: component.position,

View File

@ -44,7 +44,7 @@ private struct GroupsInCommonListEntry: Comparable, Identifiable {
}, removePeer: { _ in }, removePeer: { _ in
}, contextAction: { node, gesture in }, contextAction: { node, gesture in
openPeerContextAction(peer, node, gesture) openPeerContextAction(peer, node, gesture)
}, hasTopStripe: false, noInsets: true, noCorners: true) }, hasTopStripe: false, noInsets: true, noCorners: true, style: .plain)
} }
} }

View File

@ -72,7 +72,7 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable {
}, removePeer: { _ in }, removePeer: { _ in
}, contextAction: { node, gesture in }, contextAction: { node, gesture in
openPeerContextAction(peer._asPeer(), node, gesture) openPeerContextAction(peer._asPeer(), node, gesture)
}, hasTopStripe: false, noInsets: true, noCorners: true, disableInteractiveTransitionIfNecessary: true) }, hasTopStripe: false, noInsets: true, noCorners: true, style: .plain, disableInteractiveTransitionIfNecessary: true)
} }
} }
} }
@ -106,7 +106,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
private var unlockText: ComponentView<Empty>? private var unlockText: ComponentView<Empty>?
private var unlockButton: SolidRoundedButtonNode? private var unlockButton: SolidRoundedButtonNode?
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isScrollingLockedAtTop: Bool)? private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
private var theme: PresentationTheme? private var theme: PresentationTheme?
private let presentationDataPromise = Promise<PresentationData>() private let presentationDataPromise = Promise<PresentationData>()
@ -117,8 +117,9 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
return self.ready.get() return self.ready.get()
} }
private let statusPromise = Promise<PeerInfoStatusData?>(nil)
var status: Signal<PeerInfoStatusData?, NoError> { var status: Signal<PeerInfoStatusData?, NoError> {
return .single(nil) self.statusPromise.get()
} }
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
@ -159,6 +160,16 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
strongSelf.currentState = (recommendedChannels, isPremium) strongSelf.currentState = (recommendedChannels, isPremium)
strongSelf.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData) strongSelf.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData)
}) })
self.statusPromise.set(context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId)
)
|> map { count -> PeerInfoStatusData? in
if let count {
return PeerInfoStatusData(text: presentationData.strings.Conversation_StatusSubscribers(Int32(count)), isActivity: true, key: .recommended)
}
return nil
})
} }
deinit { deinit {
@ -179,7 +190,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.currentParams == nil let isFirstLayout = self.currentParams == nil
self.currentParams = (size, sideInset, bottomInset, isScrollingLockedAtTop) self.currentParams = (size, sideInset, bottomInset, isScrollingLockedAtTop, presentationData)
self.presentationDataPromise.set(.single(presentationData)) self.presentationDataPromise.set(.single(presentationData))
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
@ -226,10 +237,19 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
self.enqueuedTransactions.append(transaction) self.enqueuedTransactions.append(transaction)
self.dequeueTransaction() self.dequeueTransaction()
if !isPremium { self.layoutUnlockPanel()
guard let size = self.currentParams?.size, let sideInset = self.currentParams?.sideInset, let bottomInset = self.currentParams?.bottomInset else { }
private func layoutUnlockPanel() {
guard let (_, isPremium) = self.currentState, let currentParams = self.currentParams else {
return return
} }
if !isPremium {
let size = currentParams.size
let sideInset = currentParams.sideInset
let bottomInset = currentParams.bottomInset
let presentationData = currentParams.presentationData
let themeUpdated = self.theme !== presentationData.theme let themeUpdated = self.theme !== presentationData.theme
self.theme = presentationData.theme self.theme = presentationData.theme
@ -270,13 +290,12 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
} }
if themeUpdated { if themeUpdated {
let topColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.0) let topColor = presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.0)
let bottomColor = presentationData.theme.list.itemBlocksBackgroundColor let bottomColor = presentationData.theme.list.plainBackgroundColor
unlockBackground.image = generateGradientImage(size: CGSize(width: 1.0, height: 170.0), colors: [topColor, bottomColor, bottomColor], locations: [0.0, 0.3, 1.0]) unlockBackground.image = generateGradientImage(size: CGSize(width: 1.0, height: 170.0), colors: [topColor, bottomColor, bottomColor], locations: [0.0, 0.3, 1.0])
unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme)) unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme))
} }
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let textFont = Font.regular(15.0) let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0) let boldTextFont = Font.semibold(15.0)
let textColor = presentationData.theme.list.itemSecondaryTextColor let textColor = presentationData.theme.list.itemSecondaryTextColor
@ -285,6 +304,11 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
return nil return nil
}) })
var scrollOffset: CGFloat = 0.0
if case let .known(offset) = self.listNode.visibleBottomContentOffset() {
scrollOffset = min(0.0, offset + bottomInset + 80.0)
}
let unlockSize = unlockText.update( let unlockSize = unlockText.update(
transition: .immediate, transition: .immediate,
component: AnyComponent( component: AnyComponent(
@ -303,14 +327,14 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.unlockPressed))) view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.unlockPressed)))
self.view.addSubview(view) self.view.addSubview(view)
} }
view.frame = CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: size.height - bottomInset - unlockSize.height - 13.0), size: unlockSize) view.frame = CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: size.height - bottomInset - unlockSize.height - 13.0 + scrollOffset), size: unlockSize)
} }
unlockBackground.frame = CGRect(x: 0.0, y: size.height - bottomInset - 170.0, width: size.width, height: bottomInset + 170.0) unlockBackground.frame = CGRect(x: 0.0, y: size.height - bottomInset - 170.0 + scrollOffset, width: size.width, height: bottomInset + 170.0)
let buttonSideInset = sideInset + 16.0 let buttonSideInset = sideInset + 16.0
let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0) let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0)
unlockButton.frame = CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - unlockSize.height - buttonSize.height - 26.0), size: buttonSize) unlockButton.frame = CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - unlockSize.height - buttonSize.height - 26.0 + scrollOffset), size: buttonSize)
let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate) let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate)
} else { } else {
self.unlockBackground?.removeFromSuperview() self.unlockBackground?.removeFromSuperview()

View File

@ -116,7 +116,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
var subtitleBackgroundButton: HighlightTrackingButtonNode? var subtitleBackgroundButton: HighlightTrackingButtonNode?
var subtitleArrowNode: ASImageNode? var subtitleArrowNode: ASImageNode?
let panelSubtitleNode: MultiScaleTextNode let panelSubtitleNode: MultiScaleTextNode
let nextPanelSubtitleNode: MultiScaleTextNode
let usernameNodeContainer: ASDisplayNode let usernameNodeContainer: ASDisplayNode
let usernameNodeRawContainer: ASDisplayNode let usernameNodeRawContainer: ASDisplayNode
let usernameNode: MultiScaleTextNode let usernameNode: MultiScaleTextNode
@ -195,9 +194,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.panelSubtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.panelSubtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded])
self.panelSubtitleNode.displaysAsynchronously = false self.panelSubtitleNode.displaysAsynchronously = false
self.nextPanelSubtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded])
self.nextPanelSubtitleNode.displaysAsynchronously = false
self.usernameNodeContainer = ASDisplayNode() self.usernameNodeContainer = ASDisplayNode()
self.usernameNodeRawContainer = ASDisplayNode() self.usernameNodeRawContainer = ASDisplayNode()
self.usernameNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.usernameNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded])
@ -258,7 +254,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.titleNodeContainer.addSubnode(self.titleNode) self.titleNodeContainer.addSubnode(self.titleNode)
self.subtitleNodeContainer.addSubnode(self.subtitleNode) self.subtitleNodeContainer.addSubnode(self.subtitleNode)
self.subtitleNodeContainer.addSubnode(self.panelSubtitleNode) self.subtitleNodeContainer.addSubnode(self.panelSubtitleNode)
// self.subtitleNodeContainer.addSubnode(self.nextPanelSubtitleNode)
self.usernameNodeContainer.addSubnode(self.usernameNode) self.usernameNodeContainer.addSubnode(self.usernameNode)
self.regularContentNode.addSubnode(self.avatarClippingNode) self.regularContentNode.addSubnode(self.avatarClippingNode)
@ -778,7 +773,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.titleNode.updateTintColor(color: navigationContentsPrimaryColor, transition: navigationTransition) self.titleNode.updateTintColor(color: navigationContentsPrimaryColor, transition: navigationTransition)
self.subtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition) self.subtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition)
self.panelSubtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition) self.panelSubtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition)
self.nextPanelSubtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition)
if let navigationBar = self.controller?.navigationBar { if let navigationBar = self.controller?.navigationBar {
if let mainContentNode = navigationBar.backButtonNode.mainContentNode { if let mainContentNode = navigationBar.backButtonNode.mainContentNode {
navigationTransition.updateTintColor(layer: mainContentNode.layer, color: navigationContentsAccentColor) navigationTransition.updateTintColor(layer: mainContentNode.layer, color: navigationContentsAccentColor)
@ -846,7 +840,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let subtitleAttributes: MultiScaleTextState.Attributes let subtitleAttributes: MultiScaleTextState.Attributes
var subtitleIsButton: Bool = false var subtitleIsButton: Bool = false
var panelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)? var panelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)?
var nextPanelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)?
let usernameString: (text: String, attributes: MultiScaleTextState.Attributes) let usernameString: (text: String, attributes: MultiScaleTextState.Attributes)
if let peer = peer { if let peer = peer {
isPremium = peer.isPremium isPremium = peer.isPremium
@ -906,7 +899,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
subtitleIsButton = true subtitleIsButton = true
let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData let (maybePanelStatusData, _, _) = panelStatusData
if let panelStatusData = maybePanelStatusData { if let panelStatusData = maybePanelStatusData {
let subtitleColor: UIColor let subtitleColor: UIColor
if panelStatusData.isActivity { if panelStatusData.isActivity {
@ -916,9 +909,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} }
panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor))
} }
if let nextPanelStatusData = maybeNextPanelStatusData {
nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: .white))
}
} else if let statusData = statusData { } else if let statusData = statusData {
let subtitleColor: UIColor let subtitleColor: UIColor
if statusData.isActivity { if statusData.isActivity {
@ -933,7 +923,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(16.0), color: .white)) usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(16.0), color: .white))
let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData let (maybePanelStatusData, _, _) = panelStatusData
if let panelStatusData = maybePanelStatusData { if let panelStatusData = maybePanelStatusData {
let subtitleColor: UIColor let subtitleColor: UIColor
if panelStatusData.isActivity { if panelStatusData.isActivity {
@ -943,9 +933,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} }
panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor))
} }
if let nextPanelStatusData = maybeNextPanelStatusData {
nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: .white))
}
} else { } else {
subtitleStringText = " " subtitleStringText = " "
subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: .white) subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: .white)
@ -1078,14 +1065,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
], mainState: TitleNodeStateRegular) ], mainState: TitleNodeStateRegular)
self.panelSubtitleNode.accessibilityLabel = panelSubtitleString?.text ?? subtitleStringText self.panelSubtitleNode.accessibilityLabel = panelSubtitleString?.text ?? subtitleStringText
let nextPanelSubtitleNodeLayout = self.nextPanelSubtitleNode.updateLayout(text: nextPanelSubtitleString?.text ?? subtitleStringText, states: [
TitleNodeStateRegular: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize),
TitleNodeStateExpanded: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize)
], mainState: TitleNodeStateRegular)
if let _ = nextPanelSubtitleString {
self.nextPanelSubtitleNode.isHidden = false
}
let usernameNodeLayout = self.usernameNode.updateLayout(text: usernameString.text, states: [ let usernameNodeLayout = self.usernameNode.updateLayout(text: usernameString.text, states: [
TitleNodeStateRegular: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)), TitleNodeStateRegular: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)),
TitleNodeStateExpanded: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height)) TitleNodeStateExpanded: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height))
@ -1103,7 +1082,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let titleExpandedSize = titleNodeLayout[TitleNodeStateExpanded]!.size let titleExpandedSize = titleNodeLayout[TitleNodeStateExpanded]!.size
let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size
let _ = panelSubtitleNodeLayout[TitleNodeStateRegular]!.size let _ = panelSubtitleNodeLayout[TitleNodeStateRegular]!.size
let _ = nextPanelSubtitleNodeLayout[TitleNodeStateRegular]!.size
let usernameSize = usernameNodeLayout[TitleNodeStateRegular]!.size let usernameSize = usernameNodeLayout[TitleNodeStateRegular]!.size
var titleHorizontalOffset: CGFloat = 0.0 var titleHorizontalOffset: CGFloat = 0.0
@ -1210,13 +1188,19 @@ final class PeerInfoHeaderNode: ASDisplayNode {
if (panelSubtitleString?.text ?? subtitleStringText) != subtitleStringText { if (panelSubtitleString?.text ?? subtitleStringText) != subtitleStringText {
subtitleAlpha = 1.0 - effectiveAreaExpansionFraction subtitleAlpha = 1.0 - effectiveAreaExpansionFraction
panelSubtitleAlpha = effectiveAreaExpansionFraction panelSubtitleAlpha = effectiveAreaExpansionFraction
subtitleOffset = -effectiveAreaExpansionFraction * 5.0 subtitleOffset = -effectiveAreaExpansionFraction * 5.0
panelSubtitleOffset = (1.0 - effectiveAreaExpansionFraction) * 5.0 panelSubtitleOffset = (1.0 - effectiveAreaExpansionFraction) * 5.0
} else {
if effectiveAreaExpansionFraction == 1.0 {
subtitleAlpha = 0.0
panelSubtitleAlpha = 1.0
} else { } else {
subtitleAlpha = 1.0 subtitleAlpha = 1.0
panelSubtitleAlpha = 0.0 panelSubtitleAlpha = 0.0
} }
} }
}
self.subtitleNode.update(stateFractions: [ self.subtitleNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
@ -1227,11 +1211,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
], alpha: panelSubtitleAlpha, transition: transition) ], alpha: panelSubtitleAlpha, transition: transition)
self.nextPanelSubtitleNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
], alpha: panelSubtitleAlpha, transition: transition)
self.usernameNode.update(stateFractions: [ self.usernameNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
@ -1520,8 +1499,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.subtitleNodeRawContainer.frame = rawSubtitleFrame self.subtitleNodeRawContainer.frame = rawSubtitleFrame
transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize())) transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize()))
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize())) transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize()))
transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset - 1.0), size: CGSize()))
transition.updateFrame(node: self.nextPanelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize()))
transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize())) transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale)
transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale) transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale)
@ -1536,7 +1514,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} else { } else {
titleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * titleMinScale titleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * titleMinScale
subtitleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * subtitleMinScale subtitleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * subtitleMinScale
subtitleOffset = titleCollapseFraction * -2.0 subtitleOffset = titleCollapseFraction * -1.0
} }
let rawTitleFrame = titleFrame.offsetBy(dx: self.isAvatarExpanded ? 0.0 : titleHorizontalOffset * titleScale, dy: 0.0) let rawTitleFrame = titleFrame.offsetBy(dx: self.isAvatarExpanded ? 0.0 : titleHorizontalOffset * titleScale, dy: 0.0)
@ -1563,8 +1541,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
transition.updateFrameAdditiveToCenter(node: self.usernameNodeContainer, frame: CGRect(origin: usernameCenter, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset)) transition.updateFrameAdditiveToCenter(node: self.usernameNodeContainer, frame: CGRect(origin: usernameCenter, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset))
} }
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize())) transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize()))
transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset - 1.0), size: CGSize()))
transition.updateFrame(node: self.nextPanelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize()))
transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize())) transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale)
transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale) transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale)

View File

@ -674,7 +674,6 @@ final class StoryContentCaptionComponent: Component {
let authorName: String let authorName: String
let isChannel: Bool let isChannel: Bool
let text: String? let text: String?
var isEnabled = true
switch forwardInfo { switch forwardInfo {
case let .known(peer, _, _): case let .known(peer, _, _):
@ -701,7 +700,6 @@ final class StoryContentCaptionComponent: Component {
authorName = name authorName = name
isChannel = false isChannel = false
text = "" text = ""
isEnabled = false
} }
if let text { if let text {
@ -731,9 +729,16 @@ final class StoryContentCaptionComponent: Component {
action: { [weak self] in action: { [weak self] in
if let self, case let .known(peer, _, _) = forwardInfo, let story = self.forwardInfoStory { if let self, case let .known(peer, _, _) = forwardInfo, let story = self.forwardInfoStory {
self.component?.openStory(peer, story) self.component?.openStory(peer, story)
} else if let controller = self?.component?.controller() as? StoryContainerScreen {
let tooltipController = TooltipController(content: .text(component.strings.Story_ForwardAuthorHiddenTooltip), baseFontSize: 17.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true)
controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak controller] in
if let self, let controller, let forwardInfoPanel = self.forwardInfoPanel?.view {
return (controller.node, forwardInfoPanel.convert(forwardInfoPanel.bounds, to: controller.view))
}
return nil
}))
}
} }
},
isEnabled: isEnabled
) )
), ),
environment: {}, environment: {},

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 {
@ -429,22 +435,6 @@ public final class StoryFooterPanelComponent: Component {
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,6 +490,23 @@ public final class StoryFooterPanelComponent: Component {
rightContentOffset -= likeButtonSize.width + 14.0 rightContentOffset -= likeButtonSize.width + 14.0
} }
if component.canShare {
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 repostButtonSize = repostButton.update( let repostButtonSize = repostButton.update(
transition: likeStatsTransition, transition: likeStatsTransition,
component: AnyComponent(MessageInputActionButtonComponent( component: AnyComponent(MessageInputActionButtonComponent(
@ -595,6 +602,16 @@ public final class StoryFooterPanelComponent: Component {
rightContentOffset -= forwardButtonSize.width + 8.0 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 {
self.likeButton = nil self.likeButton = nil
@ -917,8 +934,12 @@ public final class StoryFooterPanelComponent: Component {
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
} }
if component.storyItem.isPending {
component.cancelUploadAction()
} else {
component.deleteAction() component.deleteAction()
} }
}
).minSize(CGSize(width: 44.0, height: baseHeight))), ).minSize(CGSize(width: 44.0, height: baseHeight))),
environment: {}, environment: {},
containerSize: CGSize(width: 44.0, height: baseHeight) containerSize: CGSize(width: 44.0, height: baseHeight)

View File

@ -10002,6 +10002,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, presentController: { [weak self] controller, arguments in }, presentController: { [weak self] controller, arguments in
self?.present(controller, in: .window(.root), with: arguments) self?.present(controller, in: .window(.root), with: arguments)
}, presentControllerInCurrent: { [weak self] controller, arguments in }, presentControllerInCurrent: { [weak self] controller, arguments in
if controller is UndoOverlayController {
self?.dismissAllTooltips()
}
self?.present(controller, in: .current, with: arguments) self?.present(controller, in: .current, with: arguments)
}, getNavigationController: { [weak self] in }, getNavigationController: { [weak self] in
return self?.navigationController as? NavigationController return self?.navigationController as? NavigationController

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

@ -131,12 +131,6 @@ func chatHistoryEntriesForView(
} }
} }
if let maybeJoinMessage = joinMessage {
if message.timestamp > maybeJoinMessage.timestamp, (!view.holeEarlier || count > 0) {
entries.append(.MessageEntry(maybeJoinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)))
joinMessage = nil
}
}
count += 1 count += 1
if let customThreadOutgoingReadState = customThreadOutgoingReadState { if let customThreadOutgoingReadState = customThreadOutgoingReadState {
@ -253,9 +247,23 @@ func chatHistoryEntriesForView(
} }
} }
if let maybeJoinMessage = joinMessage, !view.holeLater { if let lowerTimestamp = view.entries.last?.message.timestamp, let upperTimestamp = view.entries.first?.message.timestamp {
entries.append(.MessageEntry(maybeJoinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil))) if let joinMessage {
joinMessage = nil var insertAtPosition: Int?
if joinMessage.timestamp >= lowerTimestamp && view.laterId == nil {
insertAtPosition = entries.count
} else if joinMessage.timestamp < lowerTimestamp && joinMessage.timestamp > upperTimestamp {
for i in 0 ..< entries.count {
if let timestamp = entries[i].timestamp, timestamp > joinMessage.timestamp {
insertAtPosition = i
break
}
}
}
if let insertAtPosition {
entries.insert(.MessageEntry(joinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: insertAtPosition)
}
}
} }
if let maxReadIndex = view.maxReadIndex, includeUnreadEntry { if let maxReadIndex = view.maxReadIndex, includeUnreadEntry {

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