mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
bdfe639f3e
commit
67ed88e951
@ -12811,9 +12811,15 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Chat.ToastStarsSent.Title_1" = "Star sent!";
|
||||
"Chat.ToastStarsSent.Title_any" = "Stars sent!";
|
||||
|
||||
"Chat.ToastStarsSent.AnonymousTitle_1" = "Star sent anonymously!";
|
||||
"Chat.ToastStarsSent.AnonymousTitle_any" = "Stars sent anonymously!";
|
||||
|
||||
"Chat.ToastStarsSent.Text" = "You have reacted with %1$@ %2$@.";
|
||||
"Chat.ToastStarsSent.TextStarAmount_1" = "star";
|
||||
"Chat.ToastStarsSent.TextStarAmount_any" = "stars";
|
||||
|
||||
"Stars.Purchase.StarsReactionsNeededInfo" = "Buy Stars to send paid reactions **%@** and other channels.";
|
||||
"Chat.ToastStarsReactionsDisabled" = "Star Reactions were disabled in %@";"
|
||||
"Chat.ToastStarsReactionsDisabled" = "Star Reactions were disabled in %@";
|
||||
|
||||
"ChatContextMenu.SingleReactionEmojiSet" = "This reaction is from #[%@]() emoji pack.";
|
||||
|
@ -94,6 +94,7 @@ public final class ReactionIconView: PortalSourceView {
|
||||
private var animateIdle: Bool?
|
||||
private var reaction: MessageReaction.Reaction?
|
||||
|
||||
private var isPaused: Bool = false
|
||||
private var isAnimationHidden: Bool = false
|
||||
|
||||
private var disposable: Disposable?
|
||||
@ -198,6 +199,29 @@ public final class ReactionIconView: PortalSourceView {
|
||||
}
|
||||
}
|
||||
|
||||
func updateIsPaused(isPaused: Bool) {
|
||||
guard let context = self.context, let animateIdle = self.animateIdle, let animationLayer = self.animationLayer else {
|
||||
return
|
||||
}
|
||||
self.isPaused = isPaused
|
||||
|
||||
let isVisibleForAnimations = !self.isPaused && animateIdle && context.sharedContext.energyUsageSettings.loopEmoji
|
||||
if isVisibleForAnimations != animationLayer.isVisibleForAnimations {
|
||||
if isPaused {
|
||||
animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak animationLayer] _ in
|
||||
animationLayer?.removeFromSuperlayer()
|
||||
})
|
||||
self.animationLayer = nil
|
||||
self.reloadFile()
|
||||
if let animationLayer = self.animationLayer {
|
||||
animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12)
|
||||
}
|
||||
} else {
|
||||
animationLayer.isVisibleForAnimations = !self.isPaused && animateIdle && context.sharedContext.energyUsageSettings.loopEmoji
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadFile() {
|
||||
guard let context = self.context, let file = self.file, let animationCache = self.animationCache, let animationRenderer = self.animationRenderer, let placeholderColor = self.placeholderColor, let size = self.size, let animateIdle = self.animateIdle, let reaction = self.reaction else {
|
||||
return
|
||||
@ -217,7 +241,7 @@ public final class ReactionIconView: PortalSourceView {
|
||||
}
|
||||
|
||||
let animationLayer = InlineStickerItemLayer(
|
||||
context: context,
|
||||
context: .account(context),
|
||||
userLocation: .other,
|
||||
attemptSynchronousLoad: false,
|
||||
emoji: ChatTextInputTextCustomEmojiAttribute(
|
||||
@ -228,6 +252,7 @@ public final class ReactionIconView: PortalSourceView {
|
||||
file: file,
|
||||
cache: animationCache,
|
||||
renderer: animationRenderer,
|
||||
unique: true,
|
||||
placeholderColor: placeholderColor,
|
||||
pointSize: CGSize(width: iconSize.width * 2.0, height: iconSize.height * 2.0)
|
||||
)
|
||||
@ -248,7 +273,7 @@ public final class ReactionIconView: PortalSourceView {
|
||||
|
||||
animationLayer.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
|
||||
animationLayer.isVisibleForAnimations = animateIdle && context.sharedContext.energyUsageSettings.loopEmoji
|
||||
animationLayer.isVisibleForAnimations = !self.isPaused && animateIdle && context.sharedContext.energyUsageSettings.loopEmoji
|
||||
self.updateTintColor()
|
||||
}
|
||||
|
||||
@ -883,10 +908,14 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
|
||||
self.beginDelay = 0.0
|
||||
|
||||
self.containerView.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, _ in
|
||||
guard let strongSelf = self else {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
strongSelf.buttonNode.updateIsExtracted(isExtracted: isExtracted, animated: true)
|
||||
self.buttonNode.updateIsExtracted(isExtracted: isExtracted, animated: true)
|
||||
|
||||
if let iconView = self.iconView {
|
||||
iconView.updateIsPaused(isPaused: isExtracted)
|
||||
}
|
||||
}
|
||||
|
||||
if self.activateAfterCompletion {
|
||||
|
@ -102,6 +102,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
case playlistPlayback(Bool)
|
||||
case enableQuickReactionSwitch(Bool)
|
||||
case disableReloginTokens(Bool)
|
||||
case callV2(Bool)
|
||||
case liveStreamV2(Bool)
|
||||
case preferredVideoCodec(Int, String, String?, Bool)
|
||||
case disableVideoAspectScaling(Bool)
|
||||
@ -127,7 +128,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return DebugControllerSection.web.rawValue
|
||||
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2:
|
||||
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .callV2, .liveStreamV2:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .logTranslationRecognition, .resetTranslationStates:
|
||||
return DebugControllerSection.translation.rawValue
|
||||
@ -240,10 +241,12 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return 49
|
||||
case .enableQuickReactionSwitch:
|
||||
return 50
|
||||
case .liveStreamV2:
|
||||
case .callV2:
|
||||
return 51
|
||||
case .liveStreamV2:
|
||||
return 52
|
||||
case let .preferredVideoCodec(index, _, _, _):
|
||||
return 52 + index
|
||||
return 53 + index
|
||||
case .disableVideoAspectScaling:
|
||||
return 100
|
||||
case .enableNetworkFramework:
|
||||
@ -1312,6 +1315,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .callV2(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "[WIP] Video Chat V2", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
|
||||
settings.callV2 = value
|
||||
return PreferencesEntry(settings)
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .liveStreamV2(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Live Stream V2", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
@ -1476,10 +1489,11 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
||||
}
|
||||
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
|
||||
entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction))
|
||||
entries.append(.callV2(experimentalSettings.callV2))
|
||||
entries.append(.liveStreamV2(experimentalSettings.liveStreamV2))
|
||||
}
|
||||
|
||||
let codecs: [(String, String?)] = [
|
||||
/*let codecs: [(String, String?)] = [
|
||||
("No Preference", nil),
|
||||
("H265", "H265"),
|
||||
("H264", "H264"),
|
||||
@ -1489,7 +1503,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
||||
|
||||
for i in 0 ..< codecs.count {
|
||||
entries.append(.preferredVideoCodec(i, codecs[i].0, codecs[i].1, experimentalSettings.preferredVideoCodec == codecs[i].1))
|
||||
}
|
||||
}*/
|
||||
|
||||
if isMainApp {
|
||||
entries.append(.disableVideoAspectScaling(experimentalSettings.disableVideoAspectScaling))
|
||||
|
@ -500,7 +500,7 @@ public func getSharedDevideGraphicsContextSettings() -> DeviceGraphicsContextSet
|
||||
} else {
|
||||
self.colorSpace = context.colorSpace!
|
||||
}
|
||||
assert(self.rowAlignment == 32)
|
||||
assert(self.rowAlignment == 32 || self.rowAlignment == 64)
|
||||
assert(self.bitsPerPixel == 32)
|
||||
assert(self.bitsPerComponent == 8)
|
||||
}
|
||||
@ -570,7 +570,8 @@ public struct DeviceGraphicsContextSettings {
|
||||
|
||||
public func bytesPerRow(forWidth width: Int) -> Int {
|
||||
let baseValue = self.bitsPerPixel * width / 8
|
||||
return (baseValue + 31) & ~0x1F
|
||||
let alignmentMask = self.rowAlignment - 1
|
||||
return (baseValue + alignmentMask) & ~alignmentMask
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,6 +111,7 @@ swift_library(
|
||||
"//submodules/ImageBlur",
|
||||
"//submodules/MetalEngine",
|
||||
"//submodules/TelegramUI/Components/Calls/VoiceChatActionButton",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -0,0 +1,465 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import PlainButtonComponent
|
||||
import SwiftSignalKit
|
||||
import MultilineTextComponent
|
||||
import MetalEngine
|
||||
import CallScreen
|
||||
|
||||
private final class ParticipantVideoComponent: Component {
|
||||
let call: PresentationGroupCall
|
||||
let participant: GroupCallParticipantsContext.Participant
|
||||
|
||||
init(
|
||||
call: PresentationGroupCall,
|
||||
participant: GroupCallParticipantsContext.Participant
|
||||
) {
|
||||
self.call = call
|
||||
self.participant = participant
|
||||
}
|
||||
|
||||
static func ==(lhs: ParticipantVideoComponent, rhs: ParticipantVideoComponent) -> Bool {
|
||||
if lhs.participant != rhs.participant {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct VideoSpec: Equatable {
|
||||
var resolution: CGSize
|
||||
var rotationAngle: Float
|
||||
|
||||
init(resolution: CGSize, rotationAngle: Float) {
|
||||
self.resolution = resolution
|
||||
self.rotationAngle = rotationAngle
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var component: ParticipantVideoComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
|
||||
private var videoSource: AdaptedCallVideoSource?
|
||||
private var videoDisposable: Disposable?
|
||||
private var videoLayer: PrivateCallVideoLayer?
|
||||
private var videoSpec: VideoSpec?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.videoDisposable?.dispose()
|
||||
}
|
||||
|
||||
func update(component: ParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let nameColor = component.participant.peer.nameColor ?? .blue
|
||||
let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true)
|
||||
self.backgroundColor = nameColors.main
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.participant.peer.debugDisplayTitle, font: Font.regular(14.0), textColor: .white))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 100.0)
|
||||
)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: 8.0, y: availableSize.height - 8.0 - titleSize.height), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.layer.anchorPoint = CGPoint()
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setPosition(view: titleView, position: titleFrame.origin)
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
}
|
||||
|
||||
if let videoDescription = component.participant.videoDescription {
|
||||
let _ = videoDescription
|
||||
|
||||
let videoLayer: PrivateCallVideoLayer
|
||||
if let current = self.videoLayer {
|
||||
videoLayer = current
|
||||
} else {
|
||||
videoLayer = PrivateCallVideoLayer()
|
||||
self.videoLayer = videoLayer
|
||||
self.layer.insertSublayer(videoLayer, at: 0)
|
||||
|
||||
if let input = (component.call as! PresentationGroupCallImpl).video(endpointId: videoDescription.endpointId) {
|
||||
let videoSource = AdaptedCallVideoSource(videoStreamSignal: input)
|
||||
self.videoSource = videoSource
|
||||
|
||||
self.videoDisposable?.dispose()
|
||||
self.videoDisposable = videoSource.addOnUpdated { [weak self] in
|
||||
guard let self, let videoSource = self.videoSource, let videoLayer = self.videoLayer else {
|
||||
return
|
||||
}
|
||||
|
||||
let videoOutput = videoSource.currentOutput
|
||||
videoLayer.video = videoOutput
|
||||
|
||||
if let videoOutput {
|
||||
let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle)
|
||||
if self.videoSpec != videoSpec {
|
||||
self.videoSpec = videoSpec
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.videoSpec != nil {
|
||||
self.videoSpec = nil
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*var notifyOrientationUpdated = false
|
||||
var notifyIsMirroredUpdated = false
|
||||
|
||||
if !self.didReportFirstFrame {
|
||||
notifyOrientationUpdated = true
|
||||
notifyIsMirroredUpdated = true
|
||||
}
|
||||
|
||||
if let currentOutput = videoOutput {
|
||||
let currentAspect: CGFloat
|
||||
if currentOutput.resolution.height > 0.0 {
|
||||
currentAspect = currentOutput.resolution.width / currentOutput.resolution.height
|
||||
} else {
|
||||
currentAspect = 1.0
|
||||
}
|
||||
if self.currentAspect != currentAspect {
|
||||
self.currentAspect = currentAspect
|
||||
notifyOrientationUpdated = true
|
||||
}
|
||||
|
||||
let currentOrientation: PresentationCallVideoView.Orientation
|
||||
if currentOutput.followsDeviceOrientation {
|
||||
currentOrientation = .rotation0
|
||||
} else {
|
||||
if abs(currentOutput.rotationAngle - 0.0) < .ulpOfOne {
|
||||
currentOrientation = .rotation0
|
||||
} else if abs(currentOutput.rotationAngle - Float.pi * 0.5) < .ulpOfOne {
|
||||
currentOrientation = .rotation90
|
||||
} else if abs(currentOutput.rotationAngle - Float.pi) < .ulpOfOne {
|
||||
currentOrientation = .rotation180
|
||||
} else if abs(currentOutput.rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne {
|
||||
currentOrientation = .rotation270
|
||||
} else {
|
||||
currentOrientation = .rotation0
|
||||
}
|
||||
}
|
||||
if self.currentOrientation != currentOrientation {
|
||||
self.currentOrientation = currentOrientation
|
||||
notifyOrientationUpdated = true
|
||||
}
|
||||
|
||||
let currentIsMirrored = !currentOutput.mirrorDirection.isEmpty
|
||||
if self.currentIsMirrored != currentIsMirrored {
|
||||
self.currentIsMirrored = currentIsMirrored
|
||||
notifyIsMirroredUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if !self.didReportFirstFrame {
|
||||
self.didReportFirstFrame = true
|
||||
self.onFirstFrameReceived?(Float(self.currentAspect))
|
||||
}
|
||||
|
||||
if notifyOrientationUpdated {
|
||||
self.onOrientationUpdated?(self.currentOrientation, self.currentAspect)
|
||||
}
|
||||
|
||||
if notifyIsMirroredUpdated {
|
||||
self.onIsMirroredUpdated?(self.currentIsMirrored)
|
||||
}*/
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transition.setFrame(layer: videoLayer, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
if let videoSpec = self.videoSpec {
|
||||
let rotatedResolution = videoSpec.resolution
|
||||
let videoSize = rotatedResolution.aspectFilled(availableSize)
|
||||
let videoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - videoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - videoSize.height) * 0.5)), size: videoSize)
|
||||
|
||||
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: availableSize.width, height: availableSize.height)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
|
||||
let rotatedVideoResolution = videoResolution
|
||||
|
||||
transition.setPosition(layer: videoLayer, position: videoFrame.center)
|
||||
transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: videoFrame.size))
|
||||
videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2)
|
||||
}
|
||||
} else {
|
||||
if let videoLayer = self.videoLayer {
|
||||
self.videoLayer = nil
|
||||
videoLayer.removeFromSuperlayer()
|
||||
}
|
||||
self.videoDisposable?.dispose()
|
||||
self.videoDisposable = nil
|
||||
self.videoSource = nil
|
||||
self.videoSpec = nil
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class VideoChatParticipantsComponent: Component {
|
||||
let call: PresentationGroupCall
|
||||
let members: PresentationGroupCallMembers?
|
||||
|
||||
init(
|
||||
call: PresentationGroupCall,
|
||||
members: PresentationGroupCallMembers?
|
||||
) {
|
||||
self.call = call
|
||||
self.members = members
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
|
||||
if lhs.members != rhs.members {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemLayout {
|
||||
let containerSize: CGSize
|
||||
let itemCount: Int
|
||||
let itemSize: CGSize
|
||||
let itemSpacing: CGFloat
|
||||
let lastItemSize: CGFloat
|
||||
let itemsPerRow: Int
|
||||
|
||||
init(containerSize: CGSize, itemCount: Int) {
|
||||
self.containerSize = containerSize
|
||||
self.itemCount = itemCount
|
||||
|
||||
let width: CGFloat = containerSize.width
|
||||
|
||||
self.itemSpacing = 1.0
|
||||
|
||||
let itemsPerRow: CGFloat = CGFloat(3)
|
||||
self.itemsPerRow = Int(itemsPerRow)
|
||||
|
||||
let itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow)
|
||||
self.itemSize = CGSize(width: itemSize, height: itemSize)
|
||||
|
||||
self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1)
|
||||
}
|
||||
|
||||
func frame(at index: Int) -> CGRect {
|
||||
let row = index / self.itemsPerRow
|
||||
let column = index % self.itemsPerRow
|
||||
|
||||
let frame = CGRect(origin: CGPoint(x: CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize.width, height: itemSize.height))
|
||||
return frame
|
||||
}
|
||||
|
||||
func contentHeight() -> CGFloat {
|
||||
return self.frame(at: self.itemCount - 1).maxY
|
||||
}
|
||||
|
||||
func visibleItemRange(for rect: CGRect, count: Int) -> (minIndex: Int, maxIndex: Int) {
|
||||
let offsetRect = rect.offsetBy(dx: 0.0, dy: 0.0)
|
||||
var minVisibleRow = Int(floor((offsetRect.minY - self.itemSpacing) / (self.itemSize.height + self.itemSpacing)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.itemSpacing) / (self.itemSize.height + itemSpacing)))
|
||||
|
||||
let minVisibleIndex = minVisibleRow * self.itemsPerRow
|
||||
let maxVisibleIndex = min(count - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
|
||||
|
||||
return (minVisibleIndex, maxVisibleIndex)
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollView: ScrollView
|
||||
|
||||
private var component: VideoChatParticipantsComponent?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var itemViews: [EnginePeer.Id: ComponentView<Empty>] = [:]
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.clipsToBounds = false
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.alwaysBounceHorizontal = false
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = true
|
||||
|
||||
self.addSubview(self.scrollView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
var validItemIds: [EnginePeer.Id] = []
|
||||
if let members = component.members {
|
||||
let visibleItemRange = itemLayout.visibleItemRange(for: self.scrollView.bounds, count: itemLayout.itemCount)
|
||||
if visibleItemRange.maxIndex >= visibleItemRange.minIndex {
|
||||
for i in visibleItemRange.minIndex ... visibleItemRange.maxIndex {
|
||||
let participant = members.participants[i]
|
||||
validItemIds.append(participant.peer.id)
|
||||
|
||||
var itemTransition = transition
|
||||
let itemView: ComponentView<Empty>
|
||||
if let current = self.itemViews[participant.peer.id] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
itemView = ComponentView()
|
||||
self.itemViews[participant.peer.id] = itemView
|
||||
}
|
||||
|
||||
let itemFrame = itemLayout.frame(at: i)
|
||||
|
||||
let _ = itemView.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(ParticipantVideoComponent(
|
||||
call: component.call,
|
||||
participant: participant
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemFrame.size
|
||||
)
|
||||
if let itemComponentView = itemView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
self.scrollView.addSubview(itemComponentView)
|
||||
}
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removedItemIds: [EnginePeer.Id] = []
|
||||
for (itemId, itemView) in self.itemViews {
|
||||
if !validItemIds.contains(itemId) {
|
||||
removedItemIds.append(itemId)
|
||||
|
||||
if let itemComponentView = itemView.view {
|
||||
itemComponentView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
for itemId in removedItemIds {
|
||||
self.itemViews.removeValue(forKey: itemId)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.component = component
|
||||
|
||||
let itemLayout = ItemLayout(containerSize: availableSize, itemCount: component.members?.totalCount ?? 0)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
var requestedVideo: [PresentationGroupCallRequestedVideo] = []
|
||||
if let members = component.members {
|
||||
for participant in members.participants {
|
||||
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .medium) {
|
||||
requestedVideo.append(videoChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
(component.call as! PresentationGroupCallImpl).setRequestedVideoList(items: requestedVideo)
|
||||
|
||||
self.ignoreScrolling = true
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight())
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
219
submodules/TelegramCallsUI/Sources/VideoChatScreen.swift
Normal file
219
submodules/TelegramCallsUI/Sources/VideoChatScreen.swift
Normal file
@ -0,0 +1,219 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import PlainButtonComponent
|
||||
import SwiftSignalKit
|
||||
|
||||
private final class VideoChatScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let call: PresentationGroupCall
|
||||
|
||||
init(
|
||||
call: PresentationGroupCall
|
||||
) {
|
||||
self.call = call
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatScreenComponent, rhs: VideoChatScreenComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var component: VideoChatScreenComponent?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private let closeButton = ComponentView<Empty>()
|
||||
private let participants = ComponentView<Empty>()
|
||||
|
||||
private var members: PresentationGroupCallMembers?
|
||||
private var membersDisposable: Disposable?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.membersDisposable?.dispose()
|
||||
}
|
||||
|
||||
func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
|
||||
if self.component == nil {
|
||||
self.membersDisposable = (component.call.members
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] members in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.members != members {
|
||||
self.members = members
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.environment = environment
|
||||
self.state = state
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundColor = .black
|
||||
}
|
||||
|
||||
let closeButtonSize = self.closeButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(Text(
|
||||
text: "Leave", font: Font.regular(16.0), color: environment.theme.list.itemDestructiveColor)),
|
||||
effectAlignment: .center,
|
||||
minSize: CGSize(width: 44.0, height: 44.0),
|
||||
contentInsets: UIEdgeInsets(),
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = component.call.leave(terminateIfPossible: false).startStandalone()
|
||||
|
||||
if let controller = self.environment?.controller() {
|
||||
controller.dismiss()
|
||||
}
|
||||
},
|
||||
animateAlpha: true,
|
||||
animateScale: true,
|
||||
animateContents: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - closeButtonSize.width, y: availableSize.height - environment.safeInsets.bottom - 16.0 - closeButtonSize.height), size: closeButtonSize)
|
||||
if let closeButtonView = self.closeButton.view {
|
||||
if closeButtonView.superview == nil {
|
||||
self.addSubview(closeButtonView)
|
||||
}
|
||||
transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
|
||||
}
|
||||
|
||||
let participantsSize = self.participants.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(VideoChatParticipantsComponent(
|
||||
call: component.call,
|
||||
members: self.members
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: closeButtonFrame.minY - environment.statusBarHeight)
|
||||
)
|
||||
let participantsFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.statusBarHeight), size: participantsSize)
|
||||
if let participantsView = self.participants.view {
|
||||
if participantsView.superview == nil {
|
||||
self.addSubview(participantsView)
|
||||
}
|
||||
transition.setFrame(view: participantsView, frame: participantsFrame)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatController {
|
||||
public let call: PresentationGroupCall
|
||||
public var currentOverlayController: VoiceChatOverlayController?
|
||||
public var parentNavigationController: NavigationController?
|
||||
|
||||
public var onViewDidAppear: (() -> Void)?
|
||||
public var onViewDidDisappear: (() -> Void)?
|
||||
|
||||
private var isDismissed: Bool = false
|
||||
private var didAppearOnce: Bool = false
|
||||
|
||||
private var idleTimerExtensionDisposable: Disposable?
|
||||
|
||||
public init(
|
||||
call: PresentationGroupCall
|
||||
) {
|
||||
self.call = call
|
||||
|
||||
super.init(
|
||||
context: call.accountContext,
|
||||
component: VideoChatScreenComponent(
|
||||
call: call
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
presentationMode: .default,
|
||||
theme: .dark
|
||||
)
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.idleTimerExtensionDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.isDismissed = false
|
||||
|
||||
if !self.didAppearOnce {
|
||||
self.didAppearOnce = true
|
||||
|
||||
//self.controllerNode.animateIn()
|
||||
|
||||
self.idleTimerExtensionDisposable?.dispose()
|
||||
self.idleTimerExtensionDisposable = self.call.accountContext.sharedContext.applicationBindings.pushIdleTimerExtension()
|
||||
}
|
||||
|
||||
self.onViewDidAppear?()
|
||||
}
|
||||
|
||||
override public func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.idleTimerExtensionDisposable?.dispose()
|
||||
self.idleTimerExtensionDisposable = nil
|
||||
|
||||
self.didAppearOnce = false
|
||||
self.isDismissed = true
|
||||
|
||||
self.onViewDidDisappear?()
|
||||
}
|
||||
|
||||
public func dismiss(closing: Bool, manual: Bool) {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
@ -245,11 +245,13 @@ public protocol VoiceChatController: ViewController {
|
||||
var call: PresentationGroupCall { get }
|
||||
var currentOverlayController: VoiceChatOverlayController? { get }
|
||||
var parentNavigationController: NavigationController? { get set }
|
||||
var onViewDidAppear: (() -> Void)? { get set }
|
||||
var onViewDidDisappear: (() -> Void)? { get set }
|
||||
|
||||
func dismiss(closing: Bool, manual: Bool)
|
||||
}
|
||||
|
||||
public final class VoiceChatControllerImpl: ViewController, VoiceChatController {
|
||||
final class VoiceChatControllerImpl: ViewController, VoiceChatController {
|
||||
enum DisplayMode {
|
||||
case modal(isExpanded: Bool, isFilled: Bool)
|
||||
case fullscreen(controlsHidden: Bool)
|
||||
@ -7094,3 +7096,11 @@ private final class VoiceChatContextReferenceContentSource: ContextReferenceCont
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> VoiceChatController {
|
||||
if sharedContext.immediateExperimentalUISettings.callV2 {
|
||||
return VideoChatScreenV2Impl(call: call)
|
||||
} else {
|
||||
return VoiceChatControllerImpl(sharedContext: sharedContext, accountContext: accountContext, call: call)
|
||||
}
|
||||
}
|
||||
|
@ -172,9 +172,10 @@ public func updateMessageReactionsInteractively(account: Account, messageIds: [M
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
public func sendStarsReactionsInteractively(account: Account, messageId: MessageId, count: Int, isAnonymous: Bool?) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Void in
|
||||
func _internal_sendStarsReactionsInteractively(account: Account, messageId: MessageId, count: Int, isAnonymous: Bool?) -> Signal<Bool, NoError> {
|
||||
return account.postbox.transaction { transaction -> Bool in
|
||||
transaction.setPendingMessageAction(type: .sendStarsReaction, id: messageId, action: SendStarsReactionsAction(randomId: Int64.random(in: Int64.min ... Int64.max)))
|
||||
var resolvedIsAnonymousValue = false
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
@ -206,10 +207,13 @@ public func sendStarsReactionsInteractively(account: Account, messageId: Message
|
||||
|
||||
attributes.append(PendingStarsReactionsMessageAttribute(accountPeerId: account.peerId, count: mappedCount, isAnonymous: resolvedIsAnonymous))
|
||||
|
||||
resolvedIsAnonymousValue = resolvedIsAnonymous
|
||||
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||
})
|
||||
|
||||
return resolvedIsAnonymousValue
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
func cancelPendingSendStarsReactionInteractively(account: Account, messageId: MessageId) -> Signal<Never, NoError> {
|
||||
|
@ -334,8 +334,8 @@ public extension TelegramEngine {
|
||||
).startStandalone()
|
||||
}
|
||||
|
||||
public func sendStarsReaction(id: EngineMessage.Id, count: Int, isAnonymous: Bool?) {
|
||||
let _ = sendStarsReactionsInteractively(account: self.account, messageId: id, count: count, isAnonymous: isAnonymous).startStandalone()
|
||||
public func sendStarsReaction(id: EngineMessage.Id, count: Int, isAnonymous: Bool?) -> Signal<Bool, NoError> {
|
||||
return _internal_sendStarsReactionsInteractively(account: self.account, messageId: id, count: count, isAnonymous: isAnonymous)
|
||||
}
|
||||
|
||||
public func cancelPendingSendStarsReaction(id: EngineMessage.Id) {
|
||||
|
@ -191,7 +191,7 @@ final class PrivateCallPictureInPictureView: UIView {
|
||||
}
|
||||
|
||||
if let videoMetrics = self.videoMetrics {
|
||||
let resolvedRotationAngle = resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: UIApplication.shared.statusBarOrientation)
|
||||
let resolvedRotationAngle = resolveCallVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: UIApplication.shared.statusBarOrientation)
|
||||
|
||||
var rotatedResolution = videoMetrics.resolution
|
||||
var videoIsRotated = false
|
||||
|
@ -9,7 +9,7 @@ private let shadowImage: UIImage? = {
|
||||
UIImage(named: "Call/VideoGradient")?.precomposed()
|
||||
}()
|
||||
|
||||
func resolveVideoRotationAngle(angle: Float, followsDeviceOrientation: Bool, interfaceOrientation: UIInterfaceOrientation) -> Float {
|
||||
public func resolveCallVideoRotationAngle(angle: Float, followsDeviceOrientation: Bool, interfaceOrientation: UIInterfaceOrientation) -> Float {
|
||||
if !followsDeviceOrientation {
|
||||
return angle
|
||||
}
|
||||
@ -408,7 +408,7 @@ final class VideoContainerView: HighlightTrackingButton {
|
||||
self.dragPositionAnimatorLink = nil
|
||||
return
|
||||
}
|
||||
let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, resolvedRotationAngle: resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation), applyDragPosition: false)
|
||||
let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, resolvedRotationAngle: resolveCallVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation), applyDragPosition: false)
|
||||
let targetPosition = videoLayout.rotatedVideoFrame.center
|
||||
|
||||
self.dragVelocity = self.updateVelocityUsingSpring(
|
||||
@ -558,7 +558,7 @@ final class VideoContainerView: HighlightTrackingButton {
|
||||
}
|
||||
self.appliedVideoMetrics = videoMetrics
|
||||
|
||||
let resolvedRotationAngle = resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation)
|
||||
let resolvedRotationAngle = resolveCallVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation)
|
||||
|
||||
if params.isMinimized {
|
||||
self.isFillingBounds = false
|
||||
@ -588,7 +588,7 @@ final class VideoContainerView: HighlightTrackingButton {
|
||||
if let disappearingVideoLayer = self.disappearingVideoLayer {
|
||||
self.disappearingVideoLayer = nil
|
||||
|
||||
let disappearingVideoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: disappearingVideoLayer.videoMetrics, resolvedRotationAngle: resolveVideoRotationAngle(angle: disappearingVideoLayer.videoMetrics.rotationAngle, followsDeviceOrientation: disappearingVideoLayer.videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation), applyDragPosition: true)
|
||||
let disappearingVideoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: disappearingVideoLayer.videoMetrics, resolvedRotationAngle: resolveCallVideoRotationAngle(angle: disappearingVideoLayer.videoMetrics.rotationAngle, followsDeviceOrientation: disappearingVideoLayer.videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation), applyDragPosition: true)
|
||||
let initialDisappearingVideoSize = disappearingVideoLayout.effectiveVideoFrame.size
|
||||
|
||||
if !disappearingVideoLayer.isAlphaAnimationInitiated {
|
||||
|
@ -4464,6 +4464,23 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
|
||||
}
|
||||
|
||||
var selectedItems: Set<AnyHashable> = Set()
|
||||
if let myReaction = component.slice.item.storyItem.myReaction {
|
||||
switch myReaction {
|
||||
case .builtin, .stars:
|
||||
if let availableReactions = component.availableReactions {
|
||||
for availableReaction in availableReactions.reactionItems {
|
||||
if availableReaction.reaction.rawValue == myReaction {
|
||||
selectedItems.insert(AnyHashable(availableReaction.stillAnimation.fileId))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .custom(fileId):
|
||||
selectedItems.insert(AnyHashable(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)))
|
||||
}
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent.emojiInputData(
|
||||
context: component.context,
|
||||
animationCache: animationCache,
|
||||
@ -4475,7 +4492,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
areUnicodeEmojiEnabled: false,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: component.context.account.peerId,
|
||||
selectedItems: Set(),
|
||||
selectedItems: selectedItems,
|
||||
premiumIfSavedMessages: false
|
||||
)
|
||||
},
|
||||
|
@ -441,8 +441,13 @@ extension ChatControllerImpl {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.context.engine.messages.sendStarsReaction(id: message.id, count: 1, isAnonymous: nil)
|
||||
strongSelf.displayOrUpdateSendStarsUndo(messageId: message.id, count: 1)
|
||||
let _ = (strongSelf.context.engine.messages.sendStarsReaction(id: message.id, count: 1, isAnonymous: nil)
|
||||
|> deliverOnMainQueue).startStandalone(next: { isAnonymous in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.displayOrUpdateSendStarsUndo(messageId: message.id, count: 1, isAnonymous: isAnonymous)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
let chosenReaction: MessageReaction.Reaction = chosenUpdatedReaction.reaction
|
||||
|
@ -1744,8 +1744,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.context.engine.messages.sendStarsReaction(id: message.id, count: 1, isAnonymous: nil)
|
||||
strongSelf.displayOrUpdateSendStarsUndo(messageId: message.id, count: 1)
|
||||
let _ = (strongSelf.context.engine.messages.sendStarsReaction(id: message.id, count: 1, isAnonymous: nil)
|
||||
|> deliverOnMainQueue).startStandalone(next: { isAnonymous in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.displayOrUpdateSendStarsUndo(messageId: message.id, count: 1, isAnonymous: isAnonymous)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
var removedReaction: MessageReaction.Reaction?
|
||||
|
@ -317,7 +317,7 @@ extension ChatControllerImpl {
|
||||
|> mapToSignal { result -> Signal<ContextController.Tip?, NoError> in
|
||||
if case let .result(info, items, _) = result, let presentationContext = presentationContext {
|
||||
let tip: ContextController.Tip = .animatedEmoji(
|
||||
text: presentationData.strings.ChatContextMenu_ReactionEmojiSetSingle(info.title).string,
|
||||
text: presentationData.strings.ChatContextMenu_SingleReactionEmojiSet(info.title).string,
|
||||
arguments: TextNodeWithEntities.Arguments(
|
||||
context: context,
|
||||
cache: presentationContext.animationCache,
|
||||
@ -485,14 +485,14 @@ extension ChatControllerImpl {
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount), isAnonymous: isAnonymous)
|
||||
self.displayOrUpdateSendStarsUndo(messageId: message.id, count: Int(amount))
|
||||
let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount), isAnonymous: isAnonymous).startStandalone()
|
||||
self.displayOrUpdateSendStarsUndo(messageId: message.id, count: Int(amount), isAnonymous: isAnonymous)
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func displayOrUpdateSendStarsUndo(messageId: EngineMessage.Id, count: Int) {
|
||||
func displayOrUpdateSendStarsUndo(messageId: EngineMessage.Id, count: Int, isAnonymous: Bool) {
|
||||
if self.currentSendStarsUndoMessageId != messageId {
|
||||
if let current = self.currentSendStarsUndoController {
|
||||
self.currentSendStarsUndoController = nil
|
||||
@ -506,7 +506,12 @@ extension ChatControllerImpl {
|
||||
self.currentSendStarsUndoCount = count
|
||||
}
|
||||
|
||||
let title: String = self.presentationData.strings.Chat_ToastStarsSent_Title(Int32(self.currentSendStarsUndoCount))
|
||||
let title: String
|
||||
if isAnonymous {
|
||||
title = self.presentationData.strings.Chat_ToastStarsSent_AnonymousTitle(Int32(self.currentSendStarsUndoCount))
|
||||
} else {
|
||||
title = self.presentationData.strings.Chat_ToastStarsSent_Title(Int32(self.currentSendStarsUndoCount))
|
||||
}
|
||||
|
||||
let textItems = extractAnimatedTextString(string: self.presentationData.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [
|
||||
0: .number(self.currentSendStarsUndoCount, minDigits: 1),
|
||||
|
@ -907,7 +907,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
navigationController.pushViewController(groupCallController)
|
||||
} else {
|
||||
strongSelf.hasGroupCallOnScreenPromise.set(true)
|
||||
let groupCallController = VoiceChatControllerImpl(sharedContext: strongSelf, accountContext: call.accountContext, call: call)
|
||||
let groupCallController = makeVoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call)
|
||||
groupCallController.onViewDidAppear = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.hasGroupCallOnScreenPromise.set(true)
|
||||
|
Loading…
x
Reference in New Issue
Block a user