Various improvements

This commit is contained in:
Isaac 2025-08-22 15:31:31 +02:00
parent adf218345a
commit 114401f62b
13 changed files with 815 additions and 401 deletions

View File

@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(swift build)",
"Bash(.build/debug/makeproject:*)",
"Bash(mkdir:*)",
"Bash(find:*)",
"Bash(grep:*)"
],
"deny": []
}
}

View File

@ -51,6 +51,7 @@ public final class PresentationCallImpl: PresentationCall {
private var sessionState: CallSession?
private var callContextState: OngoingCallContextState?
private var ongoingContext: OngoingCallContext?
private var conferenceCallContext: PresentationGroupCallImpl?
private var ongoingContextStateDisposable: Disposable?
private var ongoingContextIsFailedDisposable: Disposable?
private var ongoingContextIsDroppedDisposable: Disposable?
@ -170,7 +171,6 @@ public final class PresentationCallImpl: PresentationCall {
public private(set) var pendingInviteToConferencePeerIds: [(id: EnginePeer.Id, isVideo: Bool)] = []
private var localVideoEndpointId: String?
private var remoteVideoEndpointId: String?
private var isMovedToConference: Bool = false
@ -483,7 +483,12 @@ public final class PresentationCallImpl: PresentationCall {
self.sessionState = nil
self.callContextState = nil
let debugLogValue = Promise<String?>()
self.ongoingContext?.stop(sendDebugLogs: false, debugLogValue: debugLogValue)
if let conferenceCallContext = self.conferenceCallContext {
let _ = conferenceCallContext.leave(terminateIfPossible: false).startStandalone()
self.conferenceCallContext = nil
} else {
self.ongoingContext?.stop(sendDebugLogs: false, debugLogValue: debugLogValue)
}
self.ongoingContext = nil
self.ongoingContextStateDisposable?.dispose()
self.ongoingContextStateDisposable = nil
@ -552,7 +557,6 @@ public final class PresentationCallImpl: PresentationCall {
self.pendingInviteToConferencePeerIds.removeAll()
self.localVideoEndpointId = nil
self.remoteVideoEndpointId = nil
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
@ -598,6 +602,32 @@ public final class PresentationCallImpl: PresentationCall {
})
canUpdate = true
}
} else if let conferenceCallContext = self.conferenceCallContext {
if self.receptionDisposable == nil, case .active = sessionState.state {
self.reception = 4
if self.isOutgoing {
self.callKitIntegration?.reportOutgoingCallConnected(uuid: sessionState.id, at: Date())
}
var canUpdate = false
self.receptionDisposable = (conferenceCallContext.signalBars
|> delay(1.0, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] reception in
if let strongSelf = self {
if let sessionState = strongSelf.sessionState {
if canUpdate {
strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: reception, audioSessionControl: strongSelf.audioSessionControl)
} else {
strongSelf.reception = reception
}
} else {
strongSelf.reception = reception
}
}
})
canUpdate = true
}
}
if case .video = sessionState.type {
@ -774,31 +804,31 @@ public final class PresentationCallImpl: PresentationCall {
isConference = true
}
if let callContextState = callContextState, !isConference, case let .active(_, _, keyVisualHash, _, _, _, _, _, _) = sessionState.state {
if let callContextState, !isConference, case let .active(_, _, keyVisualHash, _, _, _, _, _, _) = sessionState.state {
switch callContextState.state {
case .initializing:
presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls)
case .failed:
presentationState = PresentationCallState(state: .terminating(.error(.disconnected)), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls)
self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil))
case .connected:
let timestamp: Double
if let activeTimestamp = self.activeTimestamp {
timestamp = activeTimestamp
} else {
timestamp = CFAbsoluteTimeGetCurrent()
self.activeTimestamp = timestamp
}
presentationState = PresentationCallState(state: .active(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls)
case .reconnecting:
let timestamp: Double
if let activeTimestamp = self.activeTimestamp {
timestamp = activeTimestamp
} else {
timestamp = CFAbsoluteTimeGetCurrent()
self.activeTimestamp = timestamp
}
presentationState = PresentationCallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls)
case .initializing:
presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls)
case .failed:
presentationState = PresentationCallState(state: .terminating(.error(.disconnected)), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls)
self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil))
case .connected:
let timestamp: Double
if let activeTimestamp = self.activeTimestamp {
timestamp = activeTimestamp
} else {
timestamp = CFAbsoluteTimeGetCurrent()
self.activeTimestamp = timestamp
}
presentationState = PresentationCallState(state: .active(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls)
case .reconnecting:
let timestamp: Double
if let activeTimestamp = self.activeTimestamp {
timestamp = activeTimestamp
} else {
timestamp = CFAbsoluteTimeGetCurrent()
self.activeTimestamp = timestamp
}
presentationState = PresentationCallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls)
}
} else if !isConference, case let .active(_, _, keyVisualHash, _, _, _, _, _, _) = sessionState.state {
presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls)
@ -1000,70 +1030,247 @@ public final class PresentationCallImpl: PresentationCall {
if (self.sharedAudioContext != nil || audioSessionControl != nil), !wasActive || (self.sharedAudioContext == nil && previousControl == nil) {
let logName = "\(id.id)_\(id.accessHash)"
let updatedConnections = connections
let contextAudioSessionActive: Signal<Bool, NoError>
if self.sharedAudioContext != nil {
contextAudioSessionActive = .single(true)
} else {
contextAudioSessionActive = self.audioSessionActive.get()
var inlineConferenceSlug: String?
if let dict = try? JSONSerialization.jsonObject(with: (customParameters ?? "{}").data(using: .utf8)!) as? [String: Any] {
inlineConferenceSlug = dict["inline_conference"] as? String
}
let ongoingContext = OngoingCallContext(account: self.context.account, callSessionManager: self.callSessionManager, callId: id, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: updatedConnections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowP2P: allowsP2P, enableTCP: self.enableTCP, enableStunMarking: self.enableStunMarking, audioSessionActive: contextAudioSessionActive, logName: logName, preferredVideoCodec: self.preferredVideoCodec, audioDevice: self.sharedAudioContext?.audioDevice)
self.ongoingContext = ongoingContext
ongoingContext.setIsMuted(self.isMutedValue)
if let requestedVideoAspect = self.requestedVideoAspect {
ongoingContext.setRequestedVideoAspect(requestedVideoAspect)
}
self.debugInfoValue.set(ongoingContext.debugInfo())
self.ongoingContextStateDisposable = (ongoingContext.state
|> deliverOnMainQueue).start(next: { [weak self] contextState in
if let strongSelf = self {
if let sessionState = strongSelf.sessionState {
strongSelf.updateSessionState(sessionState: sessionState, callContextState: contextState, reception: strongSelf.reception, audioSessionControl: strongSelf.audioSessionControl)
} else {
strongSelf.callContextState = contextState
}
}
})
self.audioLevelDisposable = (ongoingContext.audioLevel
|> deliverOnMainQueue).start(next: { [weak self] level in
if let strongSelf = self {
strongSelf.audioLevelPromise.set(level)
}
})
func batteryLevelIsLowSignal() -> Signal<Bool, NoError> {
return Signal { subscriber in
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
var previousBatteryLevelIsLow = false
let timer = SwiftSignalKit.Timer(timeout: 30.0, repeat: true, completion: {
let batteryLevelIsLow = device.batteryLevel >= 0.0 && device.batteryLevel < 0.1 && device.batteryState != .charging
if batteryLevelIsLow != previousBatteryLevelIsLow {
previousBatteryLevelIsLow = batteryLevelIsLow
subscriber.putNext(batteryLevelIsLow)
if let inlineConferenceSlug {
if self.conferenceCallDisposable == nil {
let conferenceCallSignal = self.context.engine.calls.getCurrentGroupCall(reference: .link(slug: inlineConferenceSlug))
self.conferenceCallDisposable = (conferenceCallSignal
|> deliverOnMainQueue).startStrict(next: { [weak self] groupCall in
guard let self else {
return
}
let keyPair: TelegramKeyPair? = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair()
guard let keyPair, let groupCall else {
self.sessionStateDisposable?.dispose()
self.updateSessionState(
sessionState: CallSession(
id: self.internalId,
stableId: self.conferenceStableId,
isOutgoing: false,
type: self.isVideo ? .video : .audio,
state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()),
isVideoPossible: true
),
callContextState: nil,
reception: nil,
audioSessionControl: self.audioSessionControl
)
return
}
let conferenceCallContext = PresentationGroupCallImpl(
accountContext: self.context,
audioSession: self.audioSession,
callKitIntegration: self.callKitIntegration,
getDeviceAccessData: self.getDeviceAccessData,
initialCall: (EngineGroupCallDescription(
id: groupCall.info.id,
accessHash: groupCall.info.accessHash,
title: nil,
scheduleTimestamp: nil,
subscribedToScheduled: false,
isStream: false
), .link(slug: inlineConferenceSlug)),
internalId: self.internalId,
peerId: nil,
isChannel: false,
invite: nil,
joinAsPeerId: nil,
isStream: false,
keyPair: keyPair,
conferenceSourceId: self.internalId,
isConference: true,
beginWithVideo: false,
sharedAudioContext: self.sharedAudioContext
)
self.conferenceCallContext = conferenceCallContext
conferenceCallContext.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted)
if let videoCapturer = self.videoCapturer {
conferenceCallContext.requestVideo(capturer: videoCapturer)
}
self.ongoingContextStateDisposable = (combineLatest(queue: .mainQueue(),
conferenceCallContext.state,
conferenceCallContext.members
) |> deliverOnMainQueue).start(next: { [weak self] contextState, contextMembers in
guard let self else {
return
}
let mappedState: OngoingCallContextState.State
if contextState.networkState == .connected {
mappedState = .connected
} else {
mappedState = .reconnecting
}
var localVideoState: OngoingCallContextState.VideoState = .inactive
var remoteVideoState: OngoingCallContextState.RemoteVideoState = .inactive
var remoteVideoEndpointId: String?
var remoteVideoDescription: PresentationGroupCallRequestedVideo?
var remoteAudioState: OngoingCallContextState.RemoteAudioState = .active
if let contextMembers {
for participant in contextMembers.participants {
if participant.peer?.id == self.context.account.peerId {
if participant.videoDescription != nil {
localVideoState = .active
}
} else if participant.peer?.id == self.peerId {
remoteVideoDescription = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .full)
if remoteVideoDescription != nil {
remoteVideoState = .active
remoteVideoEndpointId = remoteVideoDescription?.endpointId
}
remoteAudioState = participant.muteState == nil ? .active : .muted
}
}
}
self.remoteVideoEndpointId = remoteVideoEndpointId
var requestedVideoList: [PresentationGroupCallRequestedVideo] = []
if let remoteVideoDescription {
requestedVideoList.append(remoteVideoDescription)
}
self.conferenceCallContext?.setRequestedVideoList(items: requestedVideoList)
let mappedContextState = OngoingCallContextState(
state: mappedState,
videoState: localVideoState,
remoteVideoState: remoteVideoState,
remoteAudioState: remoteAudioState,
remoteBatteryLevel: .normal
)
if let sessionState = self.sessionState {
self.updateSessionState(sessionState: sessionState, callContextState: mappedContextState, reception: self.reception, audioSessionControl: self.audioSessionControl)
} else {
self.callContextState = mappedContextState
}
})
self.audioLevelDisposable = (conferenceCallContext.audioLevels
|> deliverOnMainQueue).start(next: { [weak self] levels in
guard let self else {
return
}
var level: Float = 0.0
for (peerId, _, levelValue, _) in levels {
if peerId == self.peerId {
level = levelValue
break
}
}
self.audioLevelPromise.set(level)
})
func batteryLevelIsLowSignal() -> Signal<Bool, NoError> {
return Signal { subscriber in
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
var previousBatteryLevelIsLow = false
let timer = SwiftSignalKit.Timer(timeout: 30.0, repeat: true, completion: {
let batteryLevelIsLow = device.batteryLevel >= 0.0 && device.batteryLevel < 0.1 && device.batteryState != .charging
if batteryLevelIsLow != previousBatteryLevelIsLow {
previousBatteryLevelIsLow = batteryLevelIsLow
subscriber.putNext(batteryLevelIsLow)
}
}, queue: Queue.mainQueue())
timer.start()
return ActionDisposable {
device.isBatteryMonitoringEnabled = false
timer.invalidate()
}
}
}
}, error: { [weak self] _ in
guard let self else {
return
}
self.sessionStateDisposable?.dispose()
self.updateSessionState(sessionState: CallSession(
id: self.internalId,
stableId: self.conferenceStableId,
isOutgoing: false,
type: .audio,
state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()),
isVideoPossible: true
),
callContextState: nil, reception: nil, audioSessionControl: self.audioSessionControl)
})
}
} else {
let updatedConnections = connections
let contextAudioSessionActive: Signal<Bool, NoError>
if self.sharedAudioContext != nil {
contextAudioSessionActive = .single(true)
} else {
contextAudioSessionActive = self.audioSessionActive.get()
}
let ongoingContext = OngoingCallContext(account: self.context.account, callSessionManager: self.callSessionManager, callId: id, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: updatedConnections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowP2P: allowsP2P, enableTCP: self.enableTCP, enableStunMarking: self.enableStunMarking, audioSessionActive: contextAudioSessionActive, logName: logName, preferredVideoCodec: self.preferredVideoCodec, audioDevice: self.sharedAudioContext?.audioDevice)
self.ongoingContext = ongoingContext
ongoingContext.setIsMuted(self.isMutedValue)
if let requestedVideoAspect = self.requestedVideoAspect {
ongoingContext.setRequestedVideoAspect(requestedVideoAspect)
}
self.debugInfoValue.set(ongoingContext.debugInfo())
self.ongoingContextStateDisposable = (ongoingContext.state
|> deliverOnMainQueue).start(next: { [weak self] contextState in
if let strongSelf = self {
if let sessionState = strongSelf.sessionState {
strongSelf.updateSessionState(sessionState: sessionState, callContextState: contextState, reception: strongSelf.reception, audioSessionControl: strongSelf.audioSessionControl)
} else {
strongSelf.callContextState = contextState
}
}
})
self.audioLevelDisposable = (ongoingContext.audioLevel
|> deliverOnMainQueue).start(next: { [weak self] level in
if let strongSelf = self {
strongSelf.audioLevelPromise.set(level)
}
})
func batteryLevelIsLowSignal() -> Signal<Bool, NoError> {
return Signal { subscriber in
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
var previousBatteryLevelIsLow = false
let timer = SwiftSignalKit.Timer(timeout: 30.0, repeat: true, completion: {
let batteryLevelIsLow = device.batteryLevel >= 0.0 && device.batteryLevel < 0.1 && device.batteryState != .charging
if batteryLevelIsLow != previousBatteryLevelIsLow {
previousBatteryLevelIsLow = batteryLevelIsLow
subscriber.putNext(batteryLevelIsLow)
}
}, queue: Queue.mainQueue())
timer.start()
return ActionDisposable {
device.isBatteryMonitoringEnabled = false
timer.invalidate()
}
}, queue: Queue.mainQueue())
timer.start()
return ActionDisposable {
device.isBatteryMonitoringEnabled = false
timer.invalidate()
}
}
self.batteryLevelDisposable = (batteryLevelIsLowSignal()
|> deliverOnMainQueue).start(next: { [weak self] batteryLevelIsLow in
if let strongSelf = self, let ongoingContext = strongSelf.ongoingContext {
ongoingContext.setIsLowBatteryLevel(batteryLevelIsLow)
}
})
}
self.batteryLevelDisposable = (batteryLevelIsLowSignal()
|> deliverOnMainQueue).start(next: { [weak self] batteryLevelIsLow in
if let strongSelf = self, let ongoingContext = strongSelf.ongoingContext {
ongoingContext.setIsLowBatteryLevel(batteryLevelIsLow)
}
})
}
}
case .switchedToConference:
@ -1072,7 +1279,12 @@ public final class PresentationCallImpl: PresentationCall {
self.audioSessionShouldBeActive.set(true)
if wasActive {
let debugLogValue = Promise<String?>()
self.ongoingContext?.stop(sendDebugLogs: options.contains(.sendDebugLogs), debugLogValue: debugLogValue)
if let conferenceCallContext = self.conferenceCallContext {
debugLogValue.set(conferenceCallContext.debugLog.get())
let _ = conferenceCallContext.leave(terminateIfPossible: false).startStandalone()
} else {
self.ongoingContext?.stop(sendDebugLogs: options.contains(.sendDebugLogs), debugLogValue: debugLogValue)
}
}
case .dropping:
break
@ -1080,7 +1292,12 @@ public final class PresentationCallImpl: PresentationCall {
self.audioSessionShouldBeActive.set(false)
if wasActive {
let debugLogValue = Promise<String?>()
self.ongoingContext?.stop(debugLogValue: debugLogValue)
if let conferenceCallContext = self.conferenceCallContext {
debugLogValue.set(conferenceCallContext.debugLog.get())
let _ = conferenceCallContext.leave(terminateIfPossible: false).startStandalone()
} else {
self.ongoingContext?.stop(debugLogValue: debugLogValue)
}
}
}
var terminating = false
@ -1301,7 +1518,12 @@ public final class PresentationCallImpl: PresentationCall {
let debugLogValue = Promise<String?>()
self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: debugLogValue.get())
self.ongoingContext?.stop(debugLogValue: debugLogValue)
if let conferenceCallContext = self.conferenceCallContext {
debugLogValue.set(conferenceCallContext.debugLog.get())
let _ = conferenceCallContext.leave(terminateIfPossible: false).startStandalone()
} else {
self.ongoingContext?.stop(debugLogValue: debugLogValue)
}
return self.hungUpPromise.get()
}
@ -1312,7 +1534,12 @@ public final class PresentationCallImpl: PresentationCall {
}
self.callSessionManager.drop(internalId: self.internalId, reason: .busy, debugLog: .single(nil))
let debugLog = Promise<String?>()
self.ongoingContext?.stop(debugLogValue: debugLog)
if let conferenceCallContext = self.conferenceCallContext {
debugLog.set(conferenceCallContext.debugLog.get())
let _ = conferenceCallContext.leave(terminateIfPossible: false).startStandalone()
} else {
self.ongoingContext?.stop(debugLogValue: debugLog)
}
}
public func toggleIsMuted() {
@ -1328,7 +1555,11 @@ public final class PresentationCallImpl: PresentationCall {
}
self.isMutedValue = value
self.isMutedPromise.set(self.isMutedValue)
self.ongoingContext?.setIsMuted(self.isMutedValue)
if let conferenceCallContext = self.conferenceCallContext {
conferenceCallContext.setIsMuted(action: value ? .muted(isPushToTalkActive: false) : .unmuted)
} else {
self.ongoingContext?.setIsMuted(self.isMutedValue)
}
}
public func requestVideo() {
@ -1340,7 +1571,9 @@ public final class PresentationCallImpl: PresentationCall {
self.videoCapturer = videoCapturer
}
if let videoCapturer = self.videoCapturer {
if let ongoingContext = self.ongoingContext {
if let conferenceCallContext = self.conferenceCallContext {
conferenceCallContext.requestVideo(capturer: videoCapturer)
} else if let ongoingContext = self.ongoingContext {
ongoingContext.requestVideo(videoCapturer)
}
}
@ -1354,7 +1587,9 @@ public final class PresentationCallImpl: PresentationCall {
self.videoCapturer = capturer
}
if let videoCapturer = self.videoCapturer {
if let ongoingContext = self.ongoingContext {
if let conferenceCallContext = self.conferenceCallContext {
conferenceCallContext.requestVideo(capturer: videoCapturer)
} else if let ongoingContext = self.ongoingContext {
ongoingContext.requestVideo(videoCapturer)
}
}
@ -1374,7 +1609,9 @@ public final class PresentationCallImpl: PresentationCall {
}
if let _ = self.videoCapturer {
self.videoCapturer = nil
if let ongoingContext = self.ongoingContext {
if let conferenceCallContext = self.conferenceCallContext {
conferenceCallContext.disableVideo()
} else if let ongoingContext = self.ongoingContext {
ongoingContext.disableVideo()
}
}
@ -1404,7 +1641,11 @@ public final class PresentationCallImpl: PresentationCall {
guard let strongSelf = self else {
return
}
strongSelf.ongoingContext?.addExternalAudioData(data: data)
if let conferenceCallContext = strongSelf.conferenceCallContext {
conferenceCallContext.genericCallContext?.addExternalAudioData(data: data)
} else {
strongSelf.ongoingContext?.addExternalAudioData(data: data)
}
}))
self.screencastStateDisposable.set((screencastBufferServerContext.isActive
|> distinctUntilChanged
@ -1428,7 +1669,9 @@ public final class PresentationCallImpl: PresentationCall {
if let screencastCapturer = self.screencastCapturer {
self.isScreencastActive = true
if let ongoingContext = self.ongoingContext {
if let conferenceCallContext = self.conferenceCallContext {
conferenceCallContext.requestVideo(capturer: screencastCapturer)
} else if let ongoingContext = self.ongoingContext {
ongoingContext.requestVideo(screencastCapturer)
}
}
@ -1443,6 +1686,7 @@ public final class PresentationCallImpl: PresentationCall {
self.videoCapturer = nil
}
self.isScreencastActive = false
self.conferenceCallContext?.disableVideo()
self.ongoingContext?.disableVideo()
self.conferenceCallImpl?.disableVideo()
if reset {
@ -1524,7 +1768,9 @@ public final class PresentationCallImpl: PresentationCall {
return nil
}
if isIncoming {
if let ongoingContext = self.ongoingContext {
if let conferenceCallContext = self.conferenceCallContext {
return conferenceCallContext.video(endpointId: self.remoteVideoEndpointId ?? "any_remote")
} else if let ongoingContext = self.ongoingContext {
return ongoingContext.video(isIncoming: isIncoming)
} else {
return nil
@ -1646,7 +1892,12 @@ public final class PresentationCallImpl: PresentationCall {
}
func deactivateIncomingAudio() {
self.ongoingContext?.deactivateIncomingAudio()
if let conferenceCallContext = self.conferenceCallContext {
let _ = conferenceCallContext
//TODO:release
} else {
self.ongoingContext?.deactivateIncomingAudio()
}
}
}

View File

@ -40,13 +40,13 @@ private extension PresentationGroupCallState {
}
}
private enum CurrentImpl {
enum CurrentImpl {
case call(OngoingGroupCallContext)
case mediaStream(WrappedMediaStreamingContext)
case externalMediaStream(DirectMediaStreamingContext)
}
private extension CurrentImpl {
extension CurrentImpl {
var joinPayload: Signal<(String, UInt32), NoError> {
switch self {
case let .call(callContext):
@ -555,7 +555,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var currentLocalSsrc: UInt32?
private var currentLocalEndpointId: String?
private var genericCallContext: CurrentImpl?
private(set) var genericCallContext: CurrentImpl?
private var currentConnectionMode: OngoingGroupCallContext.ConnectionMode = .none
private var didInitializeConnectionMode: Bool = false

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 213
return 214
}
public func parseMessage(_ data: Data!) -> Any! {

View File

@ -10,7 +10,7 @@ func _internal_applyMaxReadIndexInteractively(postbox: Postbox, stateManager: Ac
}
}
func _internal_applyMaxReadIndexInteractively(transaction: Transaction, stateManager: AccountStateManager, index: MessageIndex) {
func _internal_applyMaxReadIndexInteractively(transaction: Transaction, stateManager: AccountStateManager, index: MessageIndex) {
let messageIds = transaction.applyInteractiveReadMaxIndex(index)
if let channel = transaction.getPeer(index.id.peerId) as? TelegramChannel, channel.isForumOrMonoForum {

View File

@ -343,6 +343,8 @@ private class ReplyThreadHistoryContextImpl {
return (nil, nil, nil, nil)
}
var markMainAsRead = false
if var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
if messageIndex.id.id >= data.maxIncomingReadId {
if let count = transaction.getThreadMessageCount(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, fromIdExclusive: data.maxIncomingReadId, toIndex: messageIndex) {
@ -365,9 +367,30 @@ private class ReplyThreadHistoryContextImpl {
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: entry)
}
if data.incomingUnreadCount == 0, let channel = peer as? TelegramChannel, channel.linkedBotId != nil {
var hasOtherUnread = false
for item in transaction.getMessageHistoryThreadIndex(peerId: peerId, limit: 20) {
guard let data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: item.threadId)?.data.get(MessageHistoryThreadData.self) else {
continue
}
if data.incomingUnreadCount != 0 {
hasOtherUnread = true
break
}
}
if !hasOtherUnread {
markMainAsRead = true
}
}
}
}
if markMainAsRead {
_internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: account.stateManager, index: messageIndex)
}
var subPeerId: Api.InputPeer?
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
subPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)

View File

@ -1040,7 +1040,7 @@ public final class InteractiveTextNodeLayout: NSObject {
}
}
height += self.insets.top + self.insets.bottom + 2.0
return CGSize(width: self.size.width + self.insets.left + self.insets.right, height: ceil(height))
return CGSize(width: self.size.width, height: ceil(height))
}
}

View File

@ -98,6 +98,7 @@ func loadTexture(image: UIImage, device: MTLDevice) -> MTLTexture? {
let data = dataForImage(image)
texture?.replace(region: region, mipmapLevel: 0, withBytes: data, bytesPerRow: bytesPerRow)
data.deallocate()
return texture
}

View File

@ -125,7 +125,7 @@ import PostSuggestionsSettingsScreen
import ChatSendStarsScreen
extension ChatControllerImpl {
func reloadChatLocation(chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, historyNode: ChatHistoryListNodeImpl, apply: @escaping ((Bool) -> Void) -> Void) {
func reloadChatLocation(chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, historyNode: ChatHistoryListNodeImpl, apply: @escaping ((ContainedViewLayoutTransition?) -> Void) -> Void) {
self.contentDataReady.set(false)
self.contentDataDisposable?.dispose()
@ -176,7 +176,7 @@ extension ChatControllerImpl {
return
}
apply({ [weak self, weak contentData] forceAnimation in
apply({ [weak self, weak contentData] forceAnimationTransition in
guard let self, let contentData, self.pendingContentData?.contentData === contentData else {
return
}
@ -184,7 +184,7 @@ extension ChatControllerImpl {
self.contentData = contentData
self.pendingContentData = nil
self.contentDataUpdated(synchronous: true, forceAnimation: forceAnimation, previousState: contentData.state)
self.contentDataUpdated(synchronous: true, forceAnimationTransition: forceAnimationTransition, previousState: contentData.state)
self.chatThemeEmoticonPromise.set(contentData.chatThemeEmoticonPromise.get())
self.chatWallpaperPromise.set(contentData.chatWallpaperPromise.get())
@ -207,11 +207,11 @@ extension ChatControllerImpl {
return
}
self.contentDataUpdated(synchronous: false, forceAnimation: false, previousState: previousState)
self.contentDataUpdated(synchronous: false, forceAnimationTransition: nil, previousState: previousState)
}
})
if self.newTopicEventsDisposable == nil, let peerId = chatLocation.peerId, (chatLocation.threadId == EngineMessage.newTopicThreadId || chatLocation.threadId == nil) {
if self.newTopicEventsDisposable == nil, let peerId = chatLocation.peerId, chatLocation.threadId == EngineMessage.newTopicThreadId {
self.newTopicEventsDisposable = (self.context.account.pendingMessageManager.newTopicEvents(peerId: peerId)
|> mapToSignal { event -> Signal<Int64, NoError> in
if case let .didMove(fromThreadId, toThreadId) = event {
@ -229,14 +229,14 @@ extension ChatControllerImpl {
if chatLocation.peerId != peerId {
self.updateInitialChatBotForumLocationThread(linkedForumId: peerId, threadId: threadId)
} else {
self.updateChatLocationThread(threadId: threadId, animationDirection: .right)
self.updateChatLocationThread(threadId: threadId, animationDirection: nil, replaceInline: true, transferInputState: true)
}
})
}
})
}
func contentDataUpdated(synchronous: Bool, forceAnimation: Bool, previousState: ContentData.State) {
func contentDataUpdated(synchronous: Bool, forceAnimationTransition: ContainedViewLayoutTransition?, previousState: ContentData.State) {
guard let contentData = self.contentData else {
return
}
@ -394,11 +394,15 @@ extension ChatControllerImpl {
if previousState.pinnedMessage != contentData.state.pinnedMessage {
animated = true
}
if forceAnimation {
animated = true
var transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate
if let forceAnimationTransition {
transition = forceAnimationTransition
}
if !self.willAppear {
transition = .immediate
}
self.updateChatPresentationInterfaceState(animated: animated && self.willAppear, interactive: false, { presentationInterfaceState in
self.updateChatPresentationInterfaceState(transition: transition, interactive: false, { presentationInterfaceState in
var presentationInterfaceState = presentationInterfaceState
presentationInterfaceState = presentationInterfaceState.updatedPeer({ _ in
return contentData.state.renderedPeer

View File

@ -5613,7 +5613,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
self.reloadChatLocation(chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, historyNode: self.chatDisplayNode.historyNode, apply: { $0(false) })
self.reloadChatLocation(chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, historyNode: self.chatDisplayNode.historyNode, apply: { $0(nil) })
self.botCallbackAlertMessageDisposable = (self.botCallbackAlertMessage.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] message in
@ -7330,9 +7330,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
interfaceState = interfaceState.withUpdatedHistoryScrollState(scrollState)
}
interfaceState = interfaceState.withUpdatedInputLanguage(self.chatDisplayNode.currentTextInputLanguage)
/*if case .peer = self.chatLocation, let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum {
interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState()).withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil)
}*/
let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: threadId, { _ in
return interfaceState
}).startStandalone()
@ -10165,9 +10162,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.currentChatSwitchDirection = nil
self.chatLocation = updatedChatLocation
historyNode.areContentAnimationsEnabled = true
self.chatDisplayNode.prepareSwitchToChatLocation(historyNode: historyNode, animationDirection: nil)
self.chatDisplayNode.prepareSwitchToChatLocation(chatLocation: chatLocation, historyNode: historyNode, animationDirection: nil)
apply(self.didAppear)
apply(self.didAppear ? .animated(duration: 0.4, curve: .spring) : .immediate)
self.currentChatSwitchDirection = nil
self.isUpdatingChatLocationThread = false
@ -10211,37 +10208,76 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.currentChatSwitchDirection = nil
self.chatLocation = updatedChatLocation
historyNode.areContentAnimationsEnabled = true
self.chatDisplayNode.prepareSwitchToChatLocation(historyNode: historyNode, animationDirection: nil)
self.chatDisplayNode.prepareSwitchToChatLocation(chatLocation: updatedChatLocation, historyNode: historyNode, animationDirection: nil)
apply(true)
apply(.animated(duration: 0.4, curve: .spring))
self.currentChatSwitchDirection = nil
self.isUpdatingChatLocationThread = false
})
}
public func updateChatLocationThread(threadId: Int64?, animationDirection: ChatControllerAnimateInnerChatSwitchDirection? = nil) {
public func updateChatLocationThread(threadId: Int64?, animationDirection: ChatControllerAnimateInnerChatSwitchDirection? = nil, replaceInline: Bool = false, transferInputState: Bool = false, completion: (() -> Void)? = nil) {
Task { @MainActor [weak self] in
guard let self else {
completion?()
return
}
if self.isUpdatingChatLocationThread {
completion?()
return
}
guard let peerId = self.chatLocation.peerId else {
completion?()
return
}
if self.chatLocation.threadId == threadId {
completion?()
return
}
guard let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer else {
completion?()
return
}
self.saveInterfaceState()
self.chatDisplayNode.dismissTextInput()
var clearInputState = false
if transferInputState {
var peerId: PeerId
var currentThreadId: Int64?
switch self.chatLocation {
case let .peer(peerIdValue):
peerId = peerIdValue
case let .replyThread(replyThreadMessage):
peerId = replyThreadMessage.peerId
currentThreadId = replyThreadMessage.threadId
case .customChatContents:
return
}
let timestamp = Int32(Date().timeIntervalSince1970)
var interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp)
let composeInputState = interfaceState.composeInputState
interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState())
interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil)
interfaceState = interfaceState.withUpdatedInputLanguage(self.chatDisplayNode.currentTextInputLanguage)
let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: currentThreadId, { _ in
return interfaceState
}).startStandalone()
let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: threadId, { current in
if currentThreadId != EngineMessage.newTopicThreadId {
return current.withUpdatedComposeInputState(composeInputState)
} else {
return current.withUpdatedComposeInputState(ChatTextInputState())
}
}).startStandalone()
if currentThreadId == EngineMessage.newTopicThreadId {
clearInputState = true
}
} else {
self.saveInterfaceState()
self.chatDisplayNode.dismissTextInput()
}
let updatedChatLocation: ChatLocation
if let threadId {
@ -10301,7 +10337,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let avatarSnapshot = self.chatInfoNavigationButton?.buttonItem.customDisplayNode?.view.window != nil ? (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.prepareSnapshotState() : nil
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
let historyNode = self.chatDisplayNode.createHistoryNodeForChatLocation(chatLocation: updatedChatLocation, chatLocationContextHolder: chatLocationContextHolder)
let historyNode = replaceInline ? self.chatDisplayNode.historyNode : self.chatDisplayNode.createHistoryNodeForChatLocation(chatLocation: updatedChatLocation, chatLocationContextHolder: chatLocationContextHolder)
self.isUpdatingChatLocationThread = true
self.reloadChatLocation(chatLocation: updatedChatLocation, chatLocationContextHolder: chatLocationContextHolder, historyNode: historyNode, apply: { [weak self, weak historyNode] apply in
guard let self, let historyNode else {
@ -10311,9 +10347,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.currentChatSwitchDirection = animationDirection
self.chatLocation = updatedChatLocation
historyNode.areContentAnimationsEnabled = true
self.chatDisplayNode.prepareSwitchToChatLocation(historyNode: historyNode, animationDirection: animationDirection)
self.chatDisplayNode.prepareSwitchToChatLocation(chatLocation: updatedChatLocation, historyNode: historyNode, animationDirection: animationDirection)
apply(true)
apply(.animated(duration: transferInputState ? 0.3 : 0.4, curve: .spring))
if let navigationSnapshot, let animationDirection {
let mappedAnimationDirection: ChatTitleView.AnimateFromSnapshotDirection
@ -10337,6 +10373,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.currentChatSwitchDirection = nil
self.isUpdatingChatLocationThread = false
if clearInputState {
//DispatchQueue.main.async { [weak self] in
// guard let self else {
// return
// }
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedComposeInputState(ChatTextInputState()) } })
//}
}
completion?()
})
}
}

View File

@ -2034,7 +2034,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
previousHistoryNode?.removeFromSupernode()
})
transition.animatePosition(layer: self.messageTransitionNode.layer, from: CGPoint(x: offsetMultiplier.x * layout.size.width, y: offsetMultiplier.y * layout.size.height), to: CGPoint(), removeOnCompletion: true, additive: true)
//transition.animatePosition(layer: self.messageTransitionNode.layer, from: CGPoint(x: offsetMultiplier.x * layout.size.width, y: offsetMultiplier.y * layout.size.height), to: CGPoint(), removeOnCompletion: true, additive: true)
transition.animatePosition(layer: previousMessageTransitionNode.layer, from: CGPoint(), to: CGPoint(x: -offsetMultiplier.x * layout.size.width, y: -offsetMultiplier.y * layout.size.height), removeOnCompletion: false, additive: true, completion: { [weak previousMessageTransitionNode] _ in
previousMessageTransitionNode?.removeFromSupernode()
})
@ -4464,282 +4464,296 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
func sendCurrentMessage(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, postpone: Bool = false, messageEffect: ChatSendMessageEffect? = nil, completion: @escaping () -> Void = {}) {
if let textInputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode {
self.historyNode.justSentTextMessage = true
if let textInputNode = textInputPanelNode.textInputNode, textInputNode.isFirstResponder() {
Keyboard.applyAutocorrection(textView: textInputNode.textView)
guard let textInputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode else {
return
}
self.historyNode.justSentTextMessage = true
if let textInputNode = textInputPanelNode.textInputNode, textInputNode.isFirstResponder() {
Keyboard.applyAutocorrection(textView: textInputNode.textView)
}
var effectivePresentationInterfaceState = self.chatPresentationInterfaceState
if let textInputPanelNode = self.textInputPanelNode {
effectivePresentationInterfaceState = effectivePresentationInterfaceState.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputPanelNode.inputTextState) }
}
if let _ = effectivePresentationInterfaceState.interfaceState.editMessage, effectivePresentationInterfaceState.interfaceState.postSuggestionState == nil {
self.interfaceInteraction?.editMessage()
} else {
var isScheduledMessages = false
if case .scheduledMessages = effectivePresentationInterfaceState.subject {
isScheduledMessages = true
}
var effectivePresentationInterfaceState = self.chatPresentationInterfaceState
if let textInputPanelNode = self.textInputPanelNode {
effectivePresentationInterfaceState = effectivePresentationInterfaceState.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputPanelNode.inputTextState) }
if let _ = effectivePresentationInterfaceState.slowmodeState, !isScheduledMessages && scheduleTime == nil {
if let rect = self.frameForInputActionButton() {
self.interfaceInteraction?.displaySlowmodeTooltip(self.view, rect)
}
return
}
if let _ = effectivePresentationInterfaceState.interfaceState.editMessage, effectivePresentationInterfaceState.interfaceState.postSuggestionState == nil {
self.interfaceInteraction?.editMessage()
var messages: [EnqueueMessage] = []
let effectiveInputText: NSAttributedString
if effectivePresentationInterfaceState.interfaceState.editMessage != nil && effectivePresentationInterfaceState.interfaceState.postSuggestionState != nil {
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.effectiveInputState.inputText)
} else {
var isScheduledMessages = false
if case .scheduledMessages = effectivePresentationInterfaceState.subject {
isScheduledMessages = true
}
if let _ = effectivePresentationInterfaceState.slowmodeState, !isScheduledMessages && scheduleTime == nil {
if let rect = self.frameForInputActionButton() {
self.interfaceInteraction?.displaySlowmodeTooltip(self.view, rect)
}
return
}
var messages: [EnqueueMessage] = []
let effectiveInputText: NSAttributedString
if effectivePresentationInterfaceState.interfaceState.editMessage != nil && effectivePresentationInterfaceState.interfaceState.postSuggestionState != nil {
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.effectiveInputState.inputText)
} else {
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)
}
let peerSpecificEmojiPack = (self.controller?.contentData?.state.peerView?.cachedData as? CachedChannelData)?.emojiPack
var inlineStickers: [MediaId: Media] = [:]
var firstLockedPremiumEmoji: TelegramMediaFile?
var bubbleUpEmojiOrStickersetsById: [Int64: ItemCollectionId] = [:]
effectiveInputText.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: effectiveInputText.length), using: { value, _, _ in
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
if let file = value.file {
inlineStickers[file.fileId] = file
if let packId = value.interactivelySelectedFromPackId {
bubbleUpEmojiOrStickersetsById[file.fileId.id] = packId
}
var isPeerSpecific = false
for attribute in file.attributes {
if case let .CustomEmoji(_, _, _, packReference) = attribute, case let .id(id, _) = packReference {
isPeerSpecific = id == peerSpecificEmojiPack?.id.id
}
}
if file.isPremiumEmoji && !self.chatPresentationInterfaceState.isPremium && self.chatPresentationInterfaceState.chatLocation.peerId != self.context.account.peerId && !isPeerSpecific {
if firstLockedPremiumEmoji == nil {
firstLockedPremiumEmoji = file
}
}
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)
}
let peerSpecificEmojiPack = (self.controller?.contentData?.state.peerView?.cachedData as? CachedChannelData)?.emojiPack
var inlineStickers: [MediaId: Media] = [:]
var firstLockedPremiumEmoji: TelegramMediaFile?
var bubbleUpEmojiOrStickersetsById: [Int64: ItemCollectionId] = [:]
effectiveInputText.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: effectiveInputText.length), using: { value, _, _ in
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
if let file = value.file {
inlineStickers[file.fileId] = file
if let packId = value.interactivelySelectedFromPackId {
bubbleUpEmojiOrStickersetsById[file.fileId.id] = packId
}
}
})
if let firstLockedPremiumEmoji = firstLockedPremiumEmoji {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.controllerInteraction.displayUndo(.sticker(context: context, file: firstLockedPremiumEmoji, loop: true, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.dismissTextInput()
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: strongSelf.context, subject: .animatedEmoji, action: {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .animatedEmoji)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
var isPeerSpecific = false
for attribute in file.attributes {
if case let .CustomEmoji(_, _, _, packReference) = attribute, case let .id(id, _) = packReference {
isPeerSpecific = id == peerSpecificEmojiPack?.id.id
}
}
strongSelf.controller?.present(controller, in: .window(.root), with: nil)
}))
return
if file.isPremiumEmoji && !self.chatPresentationInterfaceState.isPremium && self.chatPresentationInterfaceState.chatLocation.peerId != self.context.account.peerId && !isPeerSpecific {
if firstLockedPremiumEmoji == nil {
firstLockedPremiumEmoji = file
}
}
}
}
})
if let firstLockedPremiumEmoji = firstLockedPremiumEmoji {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.controllerInteraction.displayUndo(.sticker(context: context, file: firstLockedPremiumEmoji, loop: true, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.dismissTextInput()
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: strongSelf.context, subject: .animatedEmoji, action: {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .animatedEmoji)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
strongSelf.controller?.present(controller, in: .window(.root), with: nil)
}))
if let replyMessageSubject = self.chatPresentationInterfaceState.interfaceState.replyMessageSubject, let quote = replyMessageSubject.quote {
if let replyMessage = self.chatPresentationInterfaceState.replyMessage {
let nsText = replyMessage.text as NSString
var startIndex = 0
var found = false
while true {
let range = nsText.range(of: quote.text, range: NSRange(location: startIndex, length: nsText.length - startIndex))
if range.location != NSNotFound {
let subEntities = messageTextEntitiesInRange(entities: replyMessage.textEntitiesAttribute?.entities ?? [], range: range, onlyQuoteable: true)
if subEntities == quote.entities {
found = true
break
}
startIndex = range.upperBound
} else {
return
}
if let replyMessageSubject = self.chatPresentationInterfaceState.interfaceState.replyMessageSubject, let quote = replyMessageSubject.quote {
if let replyMessage = self.chatPresentationInterfaceState.replyMessage {
let nsText = replyMessage.text as NSString
var startIndex = 0
var found = false
while true {
let range = nsText.range(of: quote.text, range: NSRange(location: startIndex, length: nsText.length - startIndex))
if range.location != NSNotFound {
let subEntities = messageTextEntitiesInRange(entities: replyMessage.textEntitiesAttribute?.entities ?? [], range: range, onlyQuoteable: true)
if subEntities == quote.entities {
found = true
break
}
startIndex = range.upperBound
} else {
break
}
}
if !found {
let authorName: String = (replyMessage.author.flatMap(EnginePeer.init))?.compactDisplayTitle ?? ""
let errorTextData = self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedText(authorName)
let errorText = errorTextData.string
self.controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.context.sharedContext.currentPresentationData.with({ $0 })), title: self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedTitle, text: errorText, actions: [
TextAlertAction(type: .genericAction, title: self.chatPresentationInterfaceState.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedActionEdit, action: { [weak self] in
guard let self, let controller = self.controller else {
return
}
controller.updateChatPresentationInterfaceState(interactive: false, { presentationInterfaceState in
return presentationInterfaceState.updatedInterfaceState { interfaceState in
guard var replyMessageSubject = interfaceState.replyMessageSubject else {
return interfaceState
}
replyMessageSubject.quote = nil
return interfaceState.withUpdatedReplyMessageSubject(replyMessageSubject)
}
})
presentChatLinkOptions(selfController: controller, sourceNode: controller.displayNode)
}),
], parseMarkdown: true), in: .window(.root))
if !found {
let authorName: String = (replyMessage.author.flatMap(EnginePeer.init))?.compactDisplayTitle ?? ""
let errorTextData = self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedText(authorName)
let errorText = errorTextData.string
self.controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.context.sharedContext.currentPresentationData.with({ $0 })), title: self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedTitle, text: errorText, actions: [
TextAlertAction(type: .genericAction, title: self.chatPresentationInterfaceState.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: self.chatPresentationInterfaceState.strings.Chat_ErrorQuoteOutdatedActionEdit, action: { [weak self] in
guard let self, let controller = self.controller else {
return
}
controller.updateChatPresentationInterfaceState(interactive: false, { presentationInterfaceState in
return presentationInterfaceState.updatedInterfaceState { interfaceState in
guard var replyMessageSubject = interfaceState.replyMessageSubject else {
return interfaceState
}
replyMessageSubject.quote = nil
return interfaceState.withUpdatedReplyMessageSubject(replyMessageSubject)
}
})
presentChatLinkOptions(selfController: controller, sourceNode: controller.displayNode)
}),
], parseMarkdown: true), in: .window(.root))
return
}
return
}
}
}
let timestamp = CACurrentMediaTime()
if self.lastSendTimestamp + 0.15 > timestamp {
return
}
self.lastSendTimestamp = timestamp
self.updateTypingActivity(false)
let trimmedInputText = effectiveInputText.string.trimmingCharacters(in: .whitespacesAndNewlines)
let peerId = effectivePresentationInterfaceState.chatLocation.peerId
if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = self.interactiveEmojis, interactiveEmojis.emojis.contains(trimmedInputText), effectiveInputText.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) == nil {
messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: trimmedInputText)), threadId: self.chatLocation.threadId, replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
} else {
let inputText = convertMarkdownToAttributes(effectiveInputText)
let timestamp = CACurrentMediaTime()
if self.lastSendTimestamp + 0.15 > timestamp {
return
}
self.lastSendTimestamp = timestamp
self.updateTypingActivity(false)
let trimmedInputText = effectiveInputText.string.trimmingCharacters(in: .whitespacesAndNewlines)
let peerId = effectivePresentationInterfaceState.chatLocation.peerId
if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = self.interactiveEmojis, interactiveEmojis.emojis.contains(trimmedInputText), effectiveInputText.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) == nil {
messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: trimmedInputText)), threadId: self.chatLocation.threadId, replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
} else {
let inputText = convertMarkdownToAttributes(effectiveInputText)
var mediaReference: AnyMediaReference?
var webpage: TelegramMediaWebpage?
if let urlPreview = self.chatPresentationInterfaceState.urlPreview {
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) {
} else {
webpage = urlPreview.webPage
}
var mediaReference: AnyMediaReference?
var webpage: TelegramMediaWebpage?
if let urlPreview = self.chatPresentationInterfaceState.urlPreview {
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) {
} else {
webpage = urlPreview.webPage
}
mediaReference = webpage.flatMap(AnyMediaReference.standalone)
if let postSuggestionState = effectivePresentationInterfaceState.interfaceState.postSuggestionState, let editingOriginalMessageId = postSuggestionState.editingOriginalMessageId {
if let editMessageState = effectivePresentationInterfaceState.editMessageState, let mediaReferenceValue = editMessageState.mediaReference {
mediaReference = mediaReferenceValue
} else {
if let message = self.historyNode.messageInCurrentHistoryView(editingOriginalMessageId) {
for media in message.media {
if media is TelegramMediaFile || media is TelegramMediaImage {
mediaReference = .message(message: MessageReference(message), media: media)
}
}
mediaReference = webpage.flatMap(AnyMediaReference.standalone)
if let postSuggestionState = effectivePresentationInterfaceState.interfaceState.postSuggestionState, let editingOriginalMessageId = postSuggestionState.editingOriginalMessageId {
if let editMessageState = effectivePresentationInterfaceState.editMessageState, let mediaReferenceValue = editMessageState.mediaReference {
mediaReference = mediaReferenceValue
} else {
if let message = self.historyNode.messageInCurrentHistoryView(editingOriginalMessageId) {
for media in message.media {
if media is TelegramMediaFile || media is TelegramMediaImage {
mediaReference = .message(message: MessageReference(message), media: media)
}
}
}
}
for text in breakChatInputText(trimChatInputText(inputText)) {
if text.length != 0 {
var attributes: [MessageAttribute] = []
let entities: [MessageTextEntity]
if case let .customChatContents(customChatContents) = self.chatPresentationInterfaceState.subject, case .businessLinkSetup = customChatContents.kind {
entities = generateChatInputTextEntities(text, generateLinks: false)
} else {
entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: 0))
}
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
if let urlPreview = self.chatPresentationInterfaceState.urlPreview {
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) {
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
} else {
attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !urlPreview.positionBelowText, forceLargeMedia: urlPreview.largeMedia, isManuallyAdded: true, isSafe: false))
}
}
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
for entity in entities {
if case let .CustomEmoji(_, fileId) = entity.type {
if let packId = bubbleUpEmojiOrStickersetsById[fileId] {
if !bubbleUpEmojiOrStickersets.contains(packId) {
bubbleUpEmojiOrStickersets.append(packId)
}
}
}
}
if bubbleUpEmojiOrStickersets.count > 1 {
bubbleUpEmojiOrStickersets.removeAll()
}
messages.append(.message(text: text.string, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: self.chatLocation.threadId, replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets))
mediaReference = nil
}
}
if let mediaReferenceValue = mediaReference {
mediaReference = nil
messages.append(.message(text: "", attributes: [], inlineStickers: inlineStickers, mediaReference: mediaReferenceValue, threadId: self.chatLocation.threadId, replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
}
var forwardingToSameChat = false
if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation, id.namespace == Namespaces.Peer.CloudUser, id != self.context.account.peerId, let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds, forwardMessageIds.count == 1 {
for messageId in forwardMessageIds {
if messageId.peerId == id {
forwardingToSameChat = true
}
}
}
if !messages.isEmpty && forwardingToSameChat {
self.controllerInteraction.displaySwipeToReplyHint()
}
}
var postEmptyMessages = false
if case let .customChatContents(customChatContents) = self.chatPresentationInterfaceState.subject {
switch customChatContents.kind {
case .hashTagSearch:
break
case .quickReplyMessageInput:
break
case .businessLinkSetup:
postEmptyMessages = true
}
}
if !messages.isEmpty, let messageEffect {
messages[0] = messages[0].withUpdatedAttributes { attributes in
var attributes = attributes
attributes.append(EffectMessageAttribute(id: messageEffect.id))
return attributes
}
}
if !messages.isEmpty || postEmptyMessages || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil {
if let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds {
for text in breakChatInputText(trimChatInputText(inputText)) {
if text.length != 0 {
var attributes: [MessageAttribute] = []
attributes.append(ForwardOptionsMessageAttribute(hideNames: self.chatPresentationInterfaceState.interfaceState.forwardOptionsState?.hideNames == true, hideCaptions: self.chatPresentationInterfaceState.interfaceState.forwardOptionsState?.hideCaptions == true))
var replyThreadId: Int64?
if case let .replyThread(replyThreadMessage) = self.chatPresentationInterfaceState.chatLocation {
replyThreadId = replyThreadMessage.threadId
let entities: [MessageTextEntity]
if case let .customChatContents(customChatContents) = self.chatPresentationInterfaceState.subject, case .businessLinkSetup = customChatContents.kind {
entities = generateChatInputTextEntities(text, generateLinks: false)
} else {
entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: 0))
}
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
if let urlPreview = self.chatPresentationInterfaceState.urlPreview {
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) {
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
} else {
attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !urlPreview.positionBelowText, forceLargeMedia: urlPreview.largeMedia, isManuallyAdded: true, isSafe: false))
}
}
for id in forwardMessageIds.sorted() {
messages.append(.forward(source: id, threadId: replyThreadId, grouping: .auto, attributes: attributes, correlationId: nil))
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
for entity in entities {
if case let .CustomEmoji(_, fileId) = entity.type {
if let packId = bubbleUpEmojiOrStickersetsById[fileId] {
if !bubbleUpEmojiOrStickersets.contains(packId) {
bubbleUpEmojiOrStickersets.append(packId)
}
}
}
}
if bubbleUpEmojiOrStickersets.count > 1 {
bubbleUpEmojiOrStickersets.removeAll()
}
messages.append(.message(text: text.string, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: self.chatLocation.threadId, replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets))
mediaReference = nil
}
}
if let mediaReferenceValue = mediaReference {
mediaReference = nil
messages.append(.message(text: "", attributes: [], inlineStickers: inlineStickers, mediaReference: mediaReferenceValue, threadId: self.chatLocation.threadId, replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
}
var forwardingToSameChat = false
if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation, id.namespace == Namespaces.Peer.CloudUser, id != self.context.account.peerId, let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds, forwardMessageIds.count == 1 {
for messageId in forwardMessageIds {
if messageId.peerId == id {
forwardingToSameChat = true
}
}
var usedCorrelationId: Int64?
}
if !messages.isEmpty && forwardingToSameChat {
self.controllerInteraction.displaySwipeToReplyHint()
}
}
var postEmptyMessages = false
if case let .customChatContents(customChatContents) = self.chatPresentationInterfaceState.subject {
switch customChatContents.kind {
case .hashTagSearch:
break
case .quickReplyMessageInput:
break
case .businessLinkSetup:
postEmptyMessages = true
}
}
if !messages.isEmpty, let messageEffect {
messages[0] = messages[0].withUpdatedAttributes { attributes in
var attributes = attributes
attributes.append(EffectMessageAttribute(id: messageEffect.id))
return attributes
}
}
if !messages.isEmpty || postEmptyMessages || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil {
if let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds {
var attributes: [MessageAttribute] = []
attributes.append(ForwardOptionsMessageAttribute(hideNames: self.chatPresentationInterfaceState.interfaceState.forwardOptionsState?.hideNames == true, hideCaptions: self.chatPresentationInterfaceState.interfaceState.forwardOptionsState?.hideCaptions == true))
var replyThreadId: Int64?
if case let .replyThread(replyThreadMessage) = self.chatPresentationInterfaceState.chatLocation {
replyThreadId = replyThreadMessage.threadId
}
for id in forwardMessageIds.sorted() {
messages.append(.forward(source: id, threadId: replyThreadId, grouping: .auto, attributes: attributes, correlationId: nil))
}
}
let doSend: (Int64?) -> Void = { [weak self] overrideThreadId in
guard let self else {
return
}
var messages = messages
if let overrideThreadId {
messages = messages.map { message in
return message.withUpdatedThreadId(overrideThreadId)
}
}
var usedCorrelationId: Int64?
if !messages.isEmpty, case .message = messages[messages.count - 1] {
let correlationId = Int64.random(in: 0 ..< Int64.max)
messages[messages.count - 1] = messages[messages.count - 1].withUpdatedCorrelationId(correlationId)
var replyPanel: ReplyAccessoryPanelNode?
if let accessoryPanelNode = self.accessoryPanelNode as? ReplyAccessoryPanelNode {
replyPanel = accessoryPanelNode
@ -4751,35 +4765,67 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
})
}
}
self.setupSendActionOnViewUpdate({ [weak self] in
if let strongSelf = self, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode {
strongSelf.collapseInput()
strongSelf.ignoreUpdateHeight = true
textInputPanelNode.text = ""
strongSelf.requestUpdateChatInterfaceState(.immediate, true, { state in
var state = state
state = state.withUpdatedReplyMessageSubject(nil)
state = state.withUpdatedSendMessageEffect(nil)
if state.postSuggestionState != nil {
state = state.withUpdatedPostSuggestionState(nil)
state = state.withUpdatedEditMessage(nil)
}
state = state.withUpdatedForwardMessageIds(nil)
state = state.withUpdatedForwardOptionsState(nil)
state = state.withUpdatedComposeDisableUrlPreviews([])
return state
})
strongSelf.ignoreUpdateHeight = false
guard let self, let textInputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode else {
return
}
self.collapseInput()
self.ignoreUpdateHeight = true
textInputPanelNode.text = ""
self.requestUpdateChatInterfaceState(.immediate, overrideThreadId == nil, { state in
var state = state
state = state.withUpdatedReplyMessageSubject(nil)
state = state.withUpdatedSendMessageEffect(nil)
if state.postSuggestionState != nil {
state = state.withUpdatedPostSuggestionState(nil)
state = state.withUpdatedEditMessage(nil)
}
state = state.withUpdatedForwardMessageIds(nil)
state = state.withUpdatedForwardOptionsState(nil)
state = state.withUpdatedComposeDisableUrlPreviews([])
return state
})
self.ignoreUpdateHeight = false
}, usedCorrelationId)
completion()
self.sendMessages(messages, silentPosting, scheduleTime, messages.count > 1, postpone)
}
var targetThreadId: Int64?
if self.chatLocation.threadId == nil, let channel = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.linkedBotId != nil {
if let message = messages.first {
switch message {
case let .message(_, _, _, _, _, replyToMessageId, _, _, _, _):
if let _ = replyToMessageId {
if let replyMessage = self.chatPresentationInterfaceState.replyMessage {
targetThreadId = replyMessage.threadId
}
} else {
targetThreadId = EngineMessage.newTopicThreadId
}
case let .forward(_, threadId, _, _, _):
targetThreadId = threadId
}
}
}
if let targetThreadId {
self.historyNode.stopHistoryUpdates()
self.controller?.updateChatLocationThread(threadId: targetThreadId, animationDirection: .right, transferInputState: true, completion: { [weak self] in
guard let self else {
return
}
let _ = self
doSend(targetThreadId)
})
} else {
doSend(nil)
}
}
}
}
@ -5167,6 +5213,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
self.historyNode.enableExtractedBackgrounds = true
let chatLocation = self.chatLocation
self.historyNode.setLoadStateUpdated { [weak self] loadState, animated in
guard let strongSelf = self else {
return
@ -5194,6 +5241,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
} else if case .messages = loadState {
strongSelf.didDisplayEmptyGreeting = true
}
if chatLocation.threadId == EngineMessage.newTopicThreadId {
emptyType = nil
}
strongSelf.updateIsEmpty(emptyType, wasLoading: wasLoading, animated: animated)
}
@ -5286,11 +5336,15 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
return historyNode
}
func prepareSwitchToChatLocation(historyNode: ChatHistoryListNodeImpl, animationDirection: ChatControllerAnimateInnerChatSwitchDirection?) {
func prepareSwitchToChatLocation(chatLocation: ChatLocation, historyNode: ChatHistoryListNodeImpl, animationDirection: ChatControllerAnimateInnerChatSwitchDirection?) {
self.chatLocation = historyNode.chatLocation
self.pendingSwitchToChatLocation = PendingSwitchToChatLocation(
historyNode: historyNode,
animationDirection: animationDirection
)
if historyNode === self.historyNode {
historyNode.updateChatLocation(chatLocation: chatLocation)
} else {
self.pendingSwitchToChatLocation = PendingSwitchToChatLocation(
historyNode: historyNode,
animationDirection: animationDirection
)
}
}
}

View File

@ -985,7 +985,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
self.preloadPages = false
self.beginChatHistoryTransitions(resetScrolling: false, switchedToAnotherSource: false)
self.beginReadHistoryManagement()
if let subject = subject, case let .message(messageSubject, highlight, _, setupReply) = subject {
@ -1232,6 +1231,16 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
self.beginChatHistoryTransitions(resetScrolling: true, switchedToAnotherSource: false)
}
public func updateChatLocation(chatLocation: ChatLocation) {
if self.chatLocation == chatLocation {
return
}
self.chatLocation = chatLocation
self.beginChatHistoryTransitions(resetScrolling: true, switchedToAnotherSource: false)
self.beginReadHistoryManagement()
}
private func beginAdMessageManagement(adMessages: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), NoError>) {
self.adMessagesDisposable = (adMessages
|> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages, _, _ in
@ -2336,6 +2345,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
self.historyDisposable.set(historyViewTransitionDisposable.strict())
}
func stopHistoryUpdates() {
self.historyDisposable.set(nil)
}
private func beginReadHistoryManagement() {
let previousMaxIncomingMessageIndexByNamespace = Atomic<[MessageId.Namespace: MessageIndex]>(value: [:])
let readHistory = combineLatest(self.maxVisibleIncomingMessageIndex.get(), self.canReadHistory.get())
@ -2371,6 +2384,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
}).strict())
self.canReadHistoryDisposable?.dispose()
self.canReadHistoryDisposable = (self.canReadHistory.get() |> deliverOnMainQueue).startStrict(next: { [weak self, weak context] value in
if let strongSelf = self {
if strongSelf.canReadHistoryValue != value {

View File

@ -203,6 +203,14 @@ public struct OngoingCallContextState: Equatable {
public let remoteVideoState: RemoteVideoState
public let remoteAudioState: RemoteAudioState
public let remoteBatteryLevel: RemoteBatteryLevel
public init(state: State, videoState: VideoState, remoteVideoState: RemoteVideoState, remoteAudioState: RemoteAudioState, remoteBatteryLevel: RemoteBatteryLevel) {
self.state = state
self.videoState = videoState
self.remoteVideoState = remoteVideoState
self.remoteAudioState = remoteAudioState
self.remoteBatteryLevel = remoteBatteryLevel
}
}
private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue, OngoingCallThreadLocalContextQueueWebrtc {