mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
Merge commit '2b85da99851cf417600c23dd7a8c92f3c55619e0'
This commit is contained in:
commit
d46368ccfe
@ -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";
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -1,317 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import Display
|
|
||||||
import AsyncDisplayKit
|
|
||||||
import SwiftSignalKit
|
|
||||||
import TelegramCore
|
|
||||||
import TelegramPresentationData
|
|
||||||
import ItemListUI
|
|
||||||
import PresentationDataUtils
|
|
||||||
|
|
||||||
final class MessageStatsOverviewItem: ListViewItem, ItemListItem {
|
|
||||||
let presentationData: ItemListPresentationData
|
|
||||||
let stats: PostStats
|
|
||||||
let publicShares: Int32?
|
|
||||||
let reactions: Int32
|
|
||||||
let sectionId: ItemListSectionId
|
|
||||||
let style: ItemListStyle
|
|
||||||
|
|
||||||
init(presentationData: ItemListPresentationData, stats: PostStats, publicShares: Int32?, reactions: Int32, sectionId: ItemListSectionId, style: ItemListStyle) {
|
|
||||||
self.presentationData = presentationData
|
|
||||||
self.stats = stats
|
|
||||||
self.publicShares = publicShares
|
|
||||||
self.reactions = reactions
|
|
||||||
self.sectionId = sectionId
|
|
||||||
self.style = style
|
|
||||||
}
|
|
||||||
|
|
||||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
|
||||||
async {
|
|
||||||
let node = MessageStatsOverviewItemNode()
|
|
||||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
|
||||||
|
|
||||||
node.contentSize = layout.contentSize
|
|
||||||
node.insets = layout.insets
|
|
||||||
|
|
||||||
Queue.mainQueue().async {
|
|
||||||
completion(node, {
|
|
||||||
return (nil, { _ in apply() })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
|
||||||
Queue.mainQueue().async {
|
|
||||||
if let nodeValue = node() as? MessageStatsOverviewItemNode {
|
|
||||||
let makeLayout = nodeValue.asyncLayout()
|
|
||||||
|
|
||||||
async {
|
|
||||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
|
||||||
Queue.mainQueue().async {
|
|
||||||
completion(layout, { _ in
|
|
||||||
apply()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectable: Bool = false
|
|
||||||
}
|
|
||||||
|
|
||||||
class MessageStatsOverviewItemNode: ListViewItemNode {
|
|
||||||
private let backgroundNode: ASDisplayNode
|
|
||||||
private let topStripeNode: ASDisplayNode
|
|
||||||
private let bottomStripeNode: ASDisplayNode
|
|
||||||
private let maskNode: ASImageNode
|
|
||||||
|
|
||||||
private let leftValueLabel: ImmediateTextNode
|
|
||||||
private let centerValueLabel: ImmediateTextNode
|
|
||||||
private let rightValueLabel: ImmediateTextNode
|
|
||||||
|
|
||||||
private let leftTitleLabel: ImmediateTextNode
|
|
||||||
private let centerTitleLabel: ImmediateTextNode
|
|
||||||
private let rightTitleLabel: ImmediateTextNode
|
|
||||||
|
|
||||||
private var item: MessageStatsOverviewItem?
|
|
||||||
|
|
||||||
init() {
|
|
||||||
self.backgroundNode = ASDisplayNode()
|
|
||||||
self.backgroundNode.isLayerBacked = true
|
|
||||||
self.backgroundNode.backgroundColor = .white
|
|
||||||
|
|
||||||
self.topStripeNode = ASDisplayNode()
|
|
||||||
self.topStripeNode.isLayerBacked = true
|
|
||||||
|
|
||||||
self.bottomStripeNode = ASDisplayNode()
|
|
||||||
self.bottomStripeNode.isLayerBacked = true
|
|
||||||
|
|
||||||
self.maskNode = ASImageNode()
|
|
||||||
|
|
||||||
self.leftValueLabel = ImmediateTextNode()
|
|
||||||
self.centerValueLabel = ImmediateTextNode()
|
|
||||||
self.rightValueLabel = ImmediateTextNode()
|
|
||||||
|
|
||||||
self.leftTitleLabel = ImmediateTextNode()
|
|
||||||
self.centerTitleLabel = ImmediateTextNode()
|
|
||||||
self.rightTitleLabel = ImmediateTextNode()
|
|
||||||
|
|
||||||
super.init(layerBacked: false, dynamicBounce: false)
|
|
||||||
|
|
||||||
self.clipsToBounds = true
|
|
||||||
|
|
||||||
self.addSubnode(self.leftValueLabel)
|
|
||||||
self.addSubnode(self.centerValueLabel)
|
|
||||||
self.addSubnode(self.rightValueLabel)
|
|
||||||
|
|
||||||
self.addSubnode(self.leftTitleLabel)
|
|
||||||
self.addSubnode(self.centerTitleLabel)
|
|
||||||
self.addSubnode(self.rightTitleLabel)
|
|
||||||
}
|
|
||||||
|
|
||||||
func asyncLayout() -> (_ item: MessageStatsOverviewItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
|
||||||
let makeLeftValueLabelLayout = TextNode.asyncLayout(self.leftValueLabel)
|
|
||||||
let makeRightValueLabelLayout = TextNode.asyncLayout(self.rightValueLabel)
|
|
||||||
let makeCenterValueLabelLayout = TextNode.asyncLayout(self.centerValueLabel)
|
|
||||||
|
|
||||||
let makeLeftTitleLabelLayout = TextNode.asyncLayout(self.leftTitleLabel)
|
|
||||||
let makeRightTitleLabelLayout = TextNode.asyncLayout(self.rightTitleLabel)
|
|
||||||
let makeCenterTitleLabelLayout = TextNode.asyncLayout(self.centerTitleLabel)
|
|
||||||
|
|
||||||
let currentItem = self.item
|
|
||||||
|
|
||||||
return { item, params, neighbors in
|
|
||||||
let insets: UIEdgeInsets
|
|
||||||
let separatorHeight = UIScreenPixel
|
|
||||||
let itemBackgroundColor: UIColor
|
|
||||||
let itemSeparatorColor: UIColor
|
|
||||||
|
|
||||||
let topInset: CGFloat = 14.0
|
|
||||||
let sideInset: CGFloat = 16.0
|
|
||||||
|
|
||||||
var height: CGFloat = topInset * 2.0
|
|
||||||
|
|
||||||
let leftInset = params.leftInset
|
|
||||||
let rightInset = params.rightInset
|
|
||||||
var updatedTheme: PresentationTheme?
|
|
||||||
|
|
||||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
|
||||||
updatedTheme = item.presentationData.theme
|
|
||||||
}
|
|
||||||
|
|
||||||
switch item.style {
|
|
||||||
case .plain:
|
|
||||||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
|
||||||
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
|
||||||
insets = itemListNeighborsPlainInsets(neighbors)
|
|
||||||
case .blocks:
|
|
||||||
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
|
||||||
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
|
||||||
insets = itemListNeighborsGroupedInsets(neighbors, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
let valueFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize)
|
|
||||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
|
||||||
|
|
||||||
let leftValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
|
||||||
let rightValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
|
||||||
let centerValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
|
||||||
|
|
||||||
let leftTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
|
||||||
let rightTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
|
||||||
let centerTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
|
||||||
|
|
||||||
let centerTitle: String
|
|
||||||
let centerValue: String
|
|
||||||
if let _ = item.stats as? StoryStats {
|
|
||||||
centerTitle = "Reactions"
|
|
||||||
centerValue = compactNumericCountString(Int(item.reactions))
|
|
||||||
} else {
|
|
||||||
centerTitle = item.presentationData.strings.Stats_Message_PublicShares
|
|
||||||
centerValue = item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–"
|
|
||||||
}
|
|
||||||
|
|
||||||
leftValueLabelLayoutAndApply = makeLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(item.stats.views), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
||||||
|
|
||||||
centerValueLabelLayoutAndApply = makeCenterValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: centerValue, font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
||||||
|
|
||||||
rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, item.stats.forwards - Int($0))))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
||||||
|
|
||||||
var remainingWidth: CGFloat = params.width - leftInset - rightInset - sideInset * 2.0
|
|
||||||
let maxItemWidth: CGFloat = floor(remainingWidth / 2.8)
|
|
||||||
|
|
||||||
leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_Views, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
||||||
remainingWidth -= leftTitleLabelLayoutAndApply!.0.size.width - 4.0
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
centerTitleLabelLayoutAndApply = makeCenterTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: centerTitle, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
||||||
remainingWidth -= centerTitleLabelLayoutAndApply!.0.size.width - 4.0
|
|
||||||
|
|
||||||
rightTitleLabelLayoutAndApply = makeRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_PrivateShares, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
||||||
|
|
||||||
var maxLabelHeight = rightTitleLabelLayoutAndApply!.0.size.height
|
|
||||||
maxLabelHeight = max(maxLabelHeight, centerTitleLabelLayoutAndApply!.0.size.height)
|
|
||||||
maxLabelHeight = max(maxLabelHeight, leftTitleLabelLayoutAndApply!.0.size.height)
|
|
||||||
|
|
||||||
height += rightValueLabelLayoutAndApply!.0.size.height + maxLabelHeight
|
|
||||||
|
|
||||||
let contentSize = CGSize(width: params.width, height: height)
|
|
||||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.item = item
|
|
||||||
|
|
||||||
let _ = leftValueLabelLayoutAndApply?.1()
|
|
||||||
let _ = centerValueLabelLayoutAndApply?.1()
|
|
||||||
let _ = rightValueLabelLayoutAndApply?.1()
|
|
||||||
let _ = leftTitleLabelLayoutAndApply?.1()
|
|
||||||
let _ = centerTitleLabelLayoutAndApply?.1()
|
|
||||||
let _ = rightTitleLabelLayoutAndApply?.1()
|
|
||||||
|
|
||||||
if let _ = updatedTheme {
|
|
||||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
|
||||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
|
||||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
|
||||||
}
|
|
||||||
|
|
||||||
switch item.style {
|
|
||||||
case .plain:
|
|
||||||
if strongSelf.backgroundNode.supernode != nil {
|
|
||||||
strongSelf.backgroundNode.removeFromSupernode()
|
|
||||||
}
|
|
||||||
if strongSelf.topStripeNode.supernode != nil {
|
|
||||||
strongSelf.topStripeNode.removeFromSupernode()
|
|
||||||
}
|
|
||||||
if strongSelf.bottomStripeNode.supernode == nil {
|
|
||||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
|
||||||
}
|
|
||||||
if strongSelf.maskNode.supernode != nil {
|
|
||||||
strongSelf.maskNode.removeFromSupernode()
|
|
||||||
}
|
|
||||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
|
||||||
case .blocks:
|
|
||||||
if strongSelf.backgroundNode.supernode == nil {
|
|
||||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
|
||||||
}
|
|
||||||
if strongSelf.topStripeNode.supernode == nil {
|
|
||||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
|
||||||
}
|
|
||||||
if strongSelf.bottomStripeNode.supernode == nil {
|
|
||||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
|
||||||
}
|
|
||||||
if strongSelf.maskNode.supernode == nil {
|
|
||||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
|
||||||
var hasTopCorners = false
|
|
||||||
var hasBottomCorners = false
|
|
||||||
switch neighbors.top {
|
|
||||||
case .sameSection(false):
|
|
||||||
strongSelf.topStripeNode.isHidden = true
|
|
||||||
default:
|
|
||||||
hasTopCorners = true
|
|
||||||
strongSelf.topStripeNode.isHidden = hasCorners
|
|
||||||
}
|
|
||||||
let bottomStripeInset: CGFloat
|
|
||||||
switch neighbors.bottom {
|
|
||||||
case .sameSection(false):
|
|
||||||
bottomStripeInset = leftInset
|
|
||||||
strongSelf.bottomStripeNode.isHidden = false
|
|
||||||
default:
|
|
||||||
bottomStripeInset = 0.0
|
|
||||||
hasBottomCorners = true
|
|
||||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
|
||||||
}
|
|
||||||
|
|
||||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
|
||||||
|
|
||||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
|
||||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
|
||||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
|
||||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
|
||||||
}
|
|
||||||
|
|
||||||
let maxLeftWidth = max(leftValueLabelLayoutAndApply?.0.size.width ?? 0.0, leftTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
|
||||||
let maxCenterWidth = max(centerValueLabelLayoutAndApply?.0.size.width ?? 0.0, centerTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
|
||||||
let maxRightWidth = max(rightValueLabelLayoutAndApply?.0.size.width ?? 0.0, rightTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
|
||||||
|
|
||||||
let horizontalSpacing = max(1.0, min(60, (params.width - leftInset - rightInset - sideInset * 2.0 - maxLeftWidth - maxCenterWidth - maxRightWidth) / 2.0))
|
|
||||||
|
|
||||||
var x: CGFloat = leftInset + (params.width - leftInset - rightInset - maxLeftWidth - maxCenterWidth - maxRightWidth - horizontalSpacing * 2.0) / 2.0
|
|
||||||
if let leftValueLabelLayout = leftValueLabelLayoutAndApply?.0, let leftTitleLabelLayout = leftTitleLabelLayoutAndApply?.0 {
|
|
||||||
strongSelf.leftValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: leftValueLabelLayout.size)
|
|
||||||
strongSelf.leftTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.leftValueLabel.frame.maxY), size: leftTitleLabelLayout.size)
|
|
||||||
x += max(leftValueLabelLayout.size.width, leftTitleLabelLayout.size.width) + horizontalSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
if let centerValueLabelLayout = centerValueLabelLayoutAndApply?.0, let centerTitleLabelLayout = centerTitleLabelLayoutAndApply?.0 {
|
|
||||||
strongSelf.centerValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: centerValueLabelLayout.size)
|
|
||||||
strongSelf.centerTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.centerValueLabel.frame.maxY), size: centerTitleLabelLayout.size)
|
|
||||||
x += max(centerValueLabelLayout.size.width, centerTitleLabelLayout.size.width) + horizontalSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
if let rightValueLabelLayout = rightValueLabelLayoutAndApply?.0, let rightTitleLabelLayout = rightTitleLabelLayoutAndApply?.0 {
|
|
||||||
strongSelf.rightValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: rightValueLabelLayout.size)
|
|
||||||
strongSelf.rightTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.rightValueLabel.frame.maxY), size: rightTitleLabelLayout.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
|
||||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
|
||||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
|
||||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -35,13 +35,15 @@ extension StoryStats: Stats {
|
|||||||
class StatsOverviewItem: ListViewItem, ItemListItem {
|
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
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
@ -131,7 +98,6 @@ private func requestStoryStats(accountPeerId: PeerId, postbox: Postbox, network:
|
|||||||
}
|
}
|
||||||
|> retryRequest
|
|> retryRequest
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class StoryStatsContextImpl {
|
private final class StoryStatsContextImpl {
|
||||||
@ -255,8 +221,6 @@ private final class StoryStatsPublicForwardsContextImpl {
|
|||||||
|
|
||||||
self.count = 0
|
self.count = 0
|
||||||
|
|
||||||
self.isLoadingMore = true
|
|
||||||
|
|
||||||
self.loadMore()
|
self.loadMore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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: {},
|
||||||
|
@ -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: {},
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user