mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
Various improvements
This commit is contained in:
parent
adf218345a
commit
114401f62b
12
.claude/settings.local.json
Normal file
12
.claude/settings.local.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(swift build)",
|
||||
"Bash(.build/debug/makeproject:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(grep:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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! {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user