This commit is contained in:
Isaac 2025-11-04 17:26:02 +04:00
parent d11fe92d67
commit b427a8d4ca
8 changed files with 217 additions and 151 deletions

View file

@ -17,13 +17,16 @@ final class TabBarControllerNode: ASDisplayNode {
private struct Params: Equatable {
let layout: ContainerViewLayout
let toolbar: Toolbar?
let isTabBarHidden: Bool
init(
layout: ContainerViewLayout,
toolbar: Toolbar?
toolbar: Toolbar?,
isTabBarHidden: Bool
) {
self.layout = layout
self.toolbar = toolbar
self.isTabBarHidden = isTabBarHidden
}
}
@ -160,7 +163,7 @@ final class TabBarControllerNode: ASDisplayNode {
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, toolbar: Toolbar?, transition: ContainedViewLayoutTransition) -> CGFloat {
let params = Params(layout: layout, toolbar: toolbar)
let params = Params(layout: layout, toolbar: toolbar, isTabBarHidden: self.tabBarHidden)
if let layoutResult = self.layoutResult, layoutResult.params == params {
return layoutResult.bottomInset
} else {

View file

@ -950,7 +950,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
var sharedAudioContext = sharedAudioContext
if sharedAudioContext == nil {
var useSharedAudio = true
var useSharedAudio = !isStream
var canReuseCurrent = true
if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data {
if data["ios_killswitch_group_shared_audio"] != nil {

View file

@ -178,6 +178,7 @@ final class CameraLiveStreamComponent: Component {
hideUI: false,
visibilityFraction: 1.0,
isPanning: false,
isCentral: true,
pinchState: nil,
presentController: { c, a in
// guard let self, let environment = self.environment else {

View file

@ -1624,6 +1624,7 @@ private final class StoryContainerScreenComponent: Component {
hideUI: (i == focusedIndex && (self.itemSetPanState?.didBegin == false || self.itemSetPinchState != nil)),
visibilityFraction: 1.0 - abs(panFraction + cubeAdditionalRotationFraction),
isPanning: self.itemSetPanState?.didBegin == true,
isCentral: i == focusedIndex,
pinchState: self.itemSetPinchState,
presentController: { [weak self] c, a in
guard let self, let environment = self.environment else {

View file

@ -44,7 +44,7 @@ public final class StoryContentItem: Equatable {
}
open class View: UIView {
open func setProgressMode(_ progressMode: ProgressMode) {
open func setProgressMode(mode: StoryContentItem.ProgressMode, isCentral: Bool) {
}
open func rewind() {

View file

@ -183,7 +183,7 @@ final class StoryItemContentComponent: Component {
private var unsupportedText: ComponentView<Empty>?
private var unsupportedButton: ComponentView<Empty>?
private var progressMode: StoryContentItem.ProgressMode = .pause
private var progressMode: (mode: StoryContentItem.ProgressMode, isCentral: Bool) = (.pause, false)
private var currentProgressTimer: SwiftSignalKit.Timer?
private var currentProgressTimerValue: Double = 0.0
private var videoProgressDisposable: Disposable?
@ -302,7 +302,7 @@ final class StoryItemContentComponent: Component {
if self.videoNode != nil {
return
}
if case .pause = self.progressMode {
if case .pause = self.progressMode.mode {
return
}
@ -357,7 +357,7 @@ final class StoryItemContentComponent: Component {
}
var shouldLoop = false
if self.progressMode == .blurred {
if self.progressMode.mode == .blurred {
shouldLoop = true
} else if let component = self.component, component.item.isPending {
shouldLoop = true
@ -410,11 +410,62 @@ final class StoryItemContentComponent: Component {
})
}
}
if case let .liveStream(liveStream) = currentMessageMedia {
let mediaStreamCall: PresentationGroupCallImpl
if let current = self.mediaStreamCall {
mediaStreamCall = current
} else {
let initialCall = EngineGroupCallDescription(
id: liveStream.call.id,
accessHash: liveStream.call.accessHash,
title: nil,
scheduleTimestamp: nil,
subscribedToScheduled: false,
isStream: true
)
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
mediaStreamCall = PresentationGroupCallImpl(
accountContext: component.context,
audioSession: component.context.sharedContext.mediaManager.audioSession,
callKitIntegration: nil,
getDeviceAccessData: {
(
presentationData: presentationData,
present: { c, a in
},
openSettings: {
}
)
},
initialCall: (initialCall, .id(id: liveStream.call.id, accessHash: liveStream.call.accessHash)),
internalId: CallSessionInternalId(),
peerId: nil,
isChannel: false,
invite: nil,
joinAsPeerId: nil,
isStream: true,
keyPair: nil,
conferenceSourceId: nil,
isConference: false,
beginWithVideo: false,
sharedAudioContext: nil,
unmuteByDefault: false
)
self.mediaStreamCall = mediaStreamCall
if update && !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
}
}
}
override func setProgressMode(_ progressMode: StoryContentItem.ProgressMode) {
if self.progressMode != progressMode {
self.progressMode = progressMode
override func setProgressMode(mode: StoryContentItem.ProgressMode, isCentral: Bool) {
if self.progressMode.mode != mode || self.progressMode.isCentral != isCentral {
self.progressMode = (mode, isCentral)
self.updateProgressMode(update: true)
if let component = self.component, !self.overlaysView.bounds.isEmpty {
@ -459,8 +510,10 @@ final class StoryItemContentComponent: Component {
private func updateProgressMode(update: Bool) {
if let videoNode = self.videoNode {
let canPlay = self.progressMode != .pause && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
var canPlay = self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
if case .pause = self.progressMode.mode {
canPlay = false
}
if canPlay {
videoNode.play()
} else {
@ -468,13 +521,32 @@ final class StoryItemContentComponent: Component {
}
}
var shouldUpdate = false
if let mediaStreamCall = self.mediaStreamCall {
//print("call progressMode: \(self.progressMode)")
var canPlay = true
if case .pause = self.progressMode.mode, (!self.progressMode.isCentral || !self.hierarchyTrackingLayer.isInHierarchy) {
canPlay = false
}
if !canPlay {
self.mediaStreamCall = nil
shouldUpdate = true
let _ = mediaStreamCall.leave(terminateIfPossible: false).startStandalone()
}
}
self.initializeVideoIfReady(update: update)
self.updateVideoPlaybackProgress()
self.updateProgressTimer()
if shouldUpdate {
self.state?.updated(transition: .immediate, isLocal: true)
}
}
private func updateProgressTimer() {
var needsTimer = self.progressMode != .pause && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
var needsTimer = self.progressMode.mode != .pause && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
if let component = self.component {
if component.item.isPending {
if case .file = self.currentMessageMedia {
@ -490,7 +562,7 @@ final class StoryItemContentComponent: Component {
timeout: 1.0 / 60.0,
repeat: true,
completion: { [weak self] in
guard let self, self.progressMode != .pause, self.contentLoaded, self.hierarchyTrackingLayer.isInHierarchy else {
guard let self, self.progressMode.mode != .pause, self.contentLoaded, self.hierarchyTrackingLayer.isInHierarchy else {
return
}
@ -510,7 +582,7 @@ final class StoryItemContentComponent: Component {
}
}
if self.progressMode != .play {
if self.progressMode.mode != .play {
return
}
@ -681,7 +753,7 @@ final class StoryItemContentComponent: Component {
size: size,
isCaptureProtected: component.item.isForwardingDisabled,
attemptSynchronous: synchronousLoad,
isActive: self.progressMode == .play,
isActive: self.progressMode.mode == .play,
transition: transition
)
}
@ -842,7 +914,7 @@ final class StoryItemContentComponent: Component {
}
}
if case let .liveStream(liveStream) = messageMedia {
if case let .liveStream(liveStream) = messageMedia, let mediaStreamCall = self.mediaStreamCall {
var mediaStreamTransition = transition
let mediaStream: ComponentView<Empty>
if let current = self.mediaStream {
@ -853,55 +925,6 @@ final class StoryItemContentComponent: Component {
self.mediaStream = mediaStream
}
let mediaStreamCall: PresentationGroupCallImpl
if let current = self.mediaStreamCall {
mediaStreamCall = current
} else {
let initialCall = EngineGroupCallDescription(
id: liveStream.call.id,
accessHash: liveStream.call.accessHash,
title: nil,
scheduleTimestamp: nil,
subscribedToScheduled: false,
isStream: true
)
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
mediaStreamCall = PresentationGroupCallImpl(
accountContext: component.context,
audioSession: component.context.sharedContext.mediaManager.audioSession,
callKitIntegration: nil,
getDeviceAccessData: {
(
presentationData: presentationData,
present: { c, a in
},
openSettings: {
}
)
},
initialCall: (initialCall, .id(id: liveStream.call.id, accessHash: liveStream.call.accessHash)),
internalId: CallSessionInternalId(),
peerId: nil,
isChannel: false,
invite: nil,
joinAsPeerId: nil,
isStream: true,
keyPair: nil,
conferenceSourceId: nil,
isConference: false,
beginWithVideo: false,
sharedAudioContext: nil,
unmuteByDefault: false
)
self.mediaStreamCall = mediaStreamCall
let _ = mediaStreamCall.accountContext.engine.calls.getGroupCallStreamCredentials(peerId: mediaStreamCall.accountContext.account.peerId, isLiveStream: true, revokePreviousCredentials: false).startStandalone(next: { params in
print("url: \(params.url), streamKey: \(params.streamKey)")
})
}
let liveChat: ComponentView<Empty>
if let current = self.liveChat {
liveChat = current

View file

@ -115,6 +115,7 @@ public final class StoryItemSetContainerComponent: Component {
public let hideUI: Bool
public let visibilityFraction: CGFloat
public let isPanning: Bool
public let isCentral: Bool
public let pinchState: PinchState?
public let presentController: (ViewController, Any?) -> Void
public let presentInGlobalOverlay: (ViewController, Any?) -> Void
@ -155,6 +156,7 @@ public final class StoryItemSetContainerComponent: Component {
hideUI: Bool,
visibilityFraction: CGFloat,
isPanning: Bool,
isCentral: Bool,
pinchState: PinchState?,
presentController: @escaping (ViewController, Any?) -> Void,
presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void,
@ -194,6 +196,7 @@ public final class StoryItemSetContainerComponent: Component {
self.hideUI = hideUI
self.visibilityFraction = visibilityFraction
self.isPanning = isPanning
self.isCentral = isCentral
self.pinchState = pinchState
self.presentController = presentController
self.presentInGlobalOverlay = presentInGlobalOverlay
@ -269,6 +272,9 @@ public final class StoryItemSetContainerComponent: Component {
if lhs.isPanning != rhs.isPanning {
return false
}
if lhs.isCentral != rhs.isCentral {
return false
}
if lhs.pinchState != rhs.pinchState {
return false
}
@ -1715,7 +1721,7 @@ public final class StoryItemSetContainerComponent: Component {
itemProgressMode = .pause
}
view.setProgressMode(itemProgressMode)
view.setProgressMode(mode: itemProgressMode, isCentral: index == centralIndex && component.isCentral)
var isChannel = false
var canShare = true
@ -1952,6 +1958,9 @@ public final class StoryItemSetContainerComponent: Component {
}
func updateIsProgressPaused() {
guard let component = self.component else {
return
}
let progressMode = self.itemProgressMode()
var centralId: StoryId?
if let component = self.component {
@ -1965,7 +1974,7 @@ public final class StoryItemSetContainerComponent: Component {
if id != centralId {
itemMode = .pause
}
view.setProgressMode(itemMode)
view.setProgressMode(mode: itemMode, isCentral: id == centralId && component.isCentral)
}
}
}
@ -2865,7 +2874,7 @@ public final class StoryItemSetContainerComponent: Component {
}
let inputPlaceholder: MessageInputPanelComponent.Placeholder
if let stealthModeTimeout = component.stealthModeTimeout {
if let stealthModeTimeout = component.stealthModeTimeout, !isLiveStream {
let minutes = Int(stealthModeTimeout / 60)
let seconds = Int(stealthModeTimeout % 60)

View file

@ -56,7 +56,7 @@ import ChatSendAsContextMenu
private var ObjCKey_DeinitWatcher: Int?
final class StoryItemSetContainerSendMessage {
final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
enum InputMode {
case text
case media
@ -4032,20 +4032,53 @@ final class StoryItemSetContainerSendMessage {
}
func performSendStars(view: StoryItemSetContainerComponent.View, buttonView: UIView?, count: Int, isFromExpandedView: Bool) {
guard let component = view.component else {
return
}
if isFromExpandedView {
if let currentSendStarsUndoController = self.currentSendStarsUndoController {
self.currentSendStarsUndoController = nil
currentSendStarsUndoController.dismiss()
Task { @MainActor [weak self, weak view] in
guard let self, let view, let component = view.component else {
return
}
self.commitSendStars(view: view, count: count, delay: false)
} else {
Task { @MainActor [weak view] in
guard let view, let component = view.component else {
guard let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View else {
return
}
if isFromExpandedView {
if let currentSendStarsUndoController = self.currentSendStarsUndoController {
self.currentSendStarsUndoController = nil
currentSendStarsUndoController.dismiss()
}
self.commitSendStars(view: view, count: count, delay: false)
} else {
let starsContextState = await component.context.starsContext?.state.get()
guard let balance = starsContextState?.balance else {
return
}
var totalExpectedStars = count
if let pendingMyStars = visibleItemView.liveChatState?.starStats?.pendingMyStars, pendingMyStars > 0 {
totalExpectedStars += Int(pendingMyStars)
}
if Int64(totalExpectedStars) > balance.value {
guard let starsContext = component.context.starsContext, let navigationController = component.controller()?.navigationController as? NavigationController else {
return
}
guard let targetPeerId = component.slice.item.peerId else {
return
}
let customTheme = component.theme
let options = await component.context.engine.payments.starsTopUpOptions().get()
let controller = component.context.sharedContext.makeStarsPurchaseScreen(
context: component.context,
starsContext: starsContext,
options: options,
purpose: .generic,
targetPeerId: targetPeerId,
customTheme: customTheme,
completion: { _ in }
)
navigationController.pushViewController(controller)
return
}
@ -4126,78 +4159,74 @@ final class StoryItemSetContainerSendMessage {
}
)
}
}
guard let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View else {
return
}
self.currentLiveStreamStarsIsActive = true
self.currentLiveStreamStarsIsActiveTimer?.invalidate()
self.currentLiveStreamStarsIsActiveTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self, weak view] _ in
guard let self, let view else {
return
}
self.currentLiveStreamStarsIsActive = false
view.state?.updated(transition: .spring(duration: 0.4))
})
var totalStars = 0
if let pendingMyStars = visibleItemView.liveChatState?.starStats?.pendingMyStars, pendingMyStars > 0 {
totalStars += count
totalStars += Int(pendingMyStars)
self.commitSendStars(view: view, count: count, delay: true)
} else {
let minAmount: Int64 = 1
var count = count
count = max(Int(minAmount), count)
totalStars += count
self.commitSendStars(view: view, count: count, delay: true)
}
let title: String
/*if case .anonymous = privacy {
title = self.presentationData.strings.Chat_ToastStarsSent_AnonymousTitle(Int32(self.currentSendStarsUndoCount))
} else if case .peer = privacy, let privacyPeer {
let rawTitle = self.presentationData.strings.Chat_ToastStarsSent_TitleChannel(Int32(self.currentSendStarsUndoCount))
title = rawTitle.replacingOccurrences(of: "{name}", with: privacyPeer.compactDisplayTitle)
} else*/ do {
title = component.strings.Chat_ToastStarsSent_Title(Int32(totalStars))
}
let textItems = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [
0: .number(totalStars, minDigits: 1),
1: .text(component.strings.Chat_ToastStarsSent_TextStarAmount(Int32(totalStars)))
])
if let current = self.currentSendStarsUndoController {
current.content = .starsSent(context: component.context, title: title, text: textItems, hasUndo: true)
} else {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
let controller = UndoOverlayController(presentationData: presentationData, content: .starsSent(context: component.context, title: title, text: textItems, hasUndo: true), elevatedLayout: false, position: .top, action: { [weak view] action in
guard let view else {
return false
self.currentLiveStreamStarsIsActive = true
self.currentLiveStreamStarsIsActiveTimer?.invalidate()
self.currentLiveStreamStarsIsActiveTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self, weak view] _ in
guard let self, let view else {
return
}
if case .undo = action {
guard let component = view.component else {
return false
}
guard case .liveStream = component.slice.item.storyItem.media else {
return false
}
guard let visibleItem = view.visibleItems[component.slice.item.id], let itemView = visibleItem.view.view as? StoryItemContentComponent.View else {
return false
}
guard let call = itemView.mediaStreamCall else {
return false
}
call.cancelSendStars()
}
return false
self.currentLiveStreamStarsIsActive = false
view.state?.updated(transition: .spring(duration: 0.4))
})
self.currentSendStarsUndoController = controller
self.view?.component?.controller()?.present(controller, in: .current)
var totalStars = 0
if let pendingMyStars = visibleItemView.liveChatState?.starStats?.pendingMyStars, pendingMyStars > 0 {
totalStars += count
totalStars += Int(pendingMyStars)
self.commitSendStars(view: view, count: count, delay: true)
} else {
let minAmount: Int64 = 1
var count = count
count = max(Int(minAmount), count)
totalStars += count
self.commitSendStars(view: view, count: count, delay: true)
}
let title: String
/*if case .anonymous = privacy {
title = self.presentationData.strings.Chat_ToastStarsSent_AnonymousTitle(Int32(self.currentSendStarsUndoCount))
} else if case .peer = privacy, let privacyPeer {
let rawTitle = self.presentationData.strings.Chat_ToastStarsSent_TitleChannel(Int32(self.currentSendStarsUndoCount))
title = rawTitle.replacingOccurrences(of: "{name}", with: privacyPeer.compactDisplayTitle)
} else*/ do {
title = component.strings.Chat_ToastStarsSent_Title(Int32(totalStars))
}
let textItems = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [
0: .number(totalStars, minDigits: 1),
1: .text(component.strings.Chat_ToastStarsSent_TextStarAmount(Int32(totalStars)))
])
if let current = self.currentSendStarsUndoController {
current.content = .starsSent(context: component.context, title: title, text: textItems, hasUndo: true)
} else {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
let controller = UndoOverlayController(presentationData: presentationData, content: .starsSent(context: component.context, title: title, text: textItems, hasUndo: true), elevatedLayout: false, position: .top, action: { [weak view] action in
guard let view else {
return false
}
if case .undo = action {
guard let component = view.component else {
return false
}
guard case .liveStream = component.slice.item.storyItem.media else {
return false
}
guard let visibleItem = view.visibleItems[component.slice.item.id], let itemView = visibleItem.view.view as? StoryItemContentComponent.View else {
return false
}
guard let call = itemView.mediaStreamCall else {
return false
}
call.cancelSendStars()
}
return false
})
self.currentSendStarsUndoController = controller
self.view?.component?.controller()?.present(controller, in: .current)
}
}
}
}