Various improvements

This commit is contained in:
Isaac 2024-08-20 22:09:13 +08:00
parent bdfe639f3e
commit 67ed88e951
17 changed files with 815 additions and 34 deletions

View File

@ -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.";

View File

@ -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 {

View File

@ -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))

View File

@ -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
}
}

View File

@ -111,6 +111,7 @@ swift_library(
"//submodules/ImageBlur",
"//submodules/MetalEngine",
"//submodules/TelegramUI/Components/Calls/VoiceChatActionButton",
"//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//visibility:public",

View File

@ -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)
}
}

View 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()
}
}

View File

@ -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)
}
}

View File

@ -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> {

View File

@ -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) {

View File

@ -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

View File

@ -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 {

View File

@ -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
)
},

View File

@ -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

View File

@ -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?

View File

@ -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),

View File

@ -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)