Various improvements

This commit is contained in:
Isaac 2025-06-06 20:20:42 +08:00
parent cd3c27fcc2
commit d486f529a3
20 changed files with 406 additions and 76 deletions

View File

@ -14418,3 +14418,7 @@ Sorry for the inconvenience.";
"Monoforum.DeleteTopic.Title" = "Delete %@"; "Monoforum.DeleteTopic.Title" = "Delete %@";
"Channel.RemoveFeeAlert.Text" = "Are you sure you want to allow **%@** to message your channel for free?"; "Channel.RemoveFeeAlert.Text" = "Are you sure you want to allow **%@** to message your channel for free?";
"StarsBalance.ChannelBalance" = "Your channel balance is %@"; "StarsBalance.ChannelBalance" = "Your channel balance is %@";
"Chat.TitleJoinGroupCall" = "Join";
"Invitation.JoinGroupCall.EnableMicrophone" = "Switch on the microphone";
"Chat.PinnedGroupCallTitle" = "Pinned Group Call";

View File

@ -996,8 +996,9 @@ public enum JoinSubjectScreenMode {
public let members: [EnginePeer] public let members: [EnginePeer]
public let totalMemberCount: Int public let totalMemberCount: Int
public let info: JoinCallLinkInformation public let info: JoinCallLinkInformation
public let enableMicrophoneByDefault: Bool
public init(id: Int64, accessHash: Int64, slug: String, inviter: EnginePeer?, members: [EnginePeer], totalMemberCount: Int, info: JoinCallLinkInformation) { public init(id: Int64, accessHash: Int64, slug: String, inviter: EnginePeer?, members: [EnginePeer], totalMemberCount: Int, info: JoinCallLinkInformation, enableMicrophoneByDefault: Bool) {
self.id = id self.id = id
self.accessHash = accessHash self.accessHash = accessHash
self.slug = slug self.slug = slug
@ -1005,6 +1006,7 @@ public enum JoinSubjectScreenMode {
self.members = members self.members = members
self.totalMemberCount = totalMemberCount self.totalMemberCount = totalMemberCount
self.info = info self.info = info
self.enableMicrophoneByDefault = enableMicrophoneByDefault
} }
} }
@ -1376,7 +1378,7 @@ public protocol AccountContext: AnyObject {
func scheduleGroupCall(peerId: PeerId, parentController: ViewController) func scheduleGroupCall(peerId: PeerId, parentController: ViewController)
func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: EngineGroupCallDescription) func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: EngineGroupCallDescription)
func joinConferenceCall(call: JoinCallLinkInformation, isVideo: Bool) func joinConferenceCall(call: JoinCallLinkInformation, isVideo: Bool, unmuteByDefault: Bool)
func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void) func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void)
} }

View File

@ -429,6 +429,18 @@ public struct PresentationGroupCallInvitedPeer: Equatable {
} }
} }
public struct PresentationGroupCallPersistentSettings: Codable {
public static let `default` = PresentationGroupCallPersistentSettings(
isMicrophoneEnabledByDefault: true
)
public var isMicrophoneEnabledByDefault: Bool
public init(isMicrophoneEnabledByDefault: Bool) {
self.isMicrophoneEnabledByDefault = isMicrophoneEnabledByDefault
}
}
public protocol PresentationGroupCall: AnyObject { public protocol PresentationGroupCall: AnyObject {
var account: Account { get } var account: Account { get }
var accountContext: AccountContext { get } var accountContext: AccountContext { get }
@ -580,6 +592,7 @@ public protocol PresentationCallManager: AnyObject {
reference: InternalGroupCallReference, reference: InternalGroupCallReference,
beginWithVideo: Bool, beginWithVideo: Bool,
invitePeerIds: [EnginePeer.Id], invitePeerIds: [EnginePeer.Id],
endCurrentIfAny: Bool endCurrentIfAny: Bool,
unmuteByDefault: Bool
) -> JoinGroupCallManagerResult ) -> JoinGroupCallManagerResult
} }

View File

@ -278,7 +278,8 @@ public final class CallListController: TelegramBaseController {
reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash),
beginWithVideo: isVideo, beginWithVideo: isVideo,
invitePeerIds: peerIds, invitePeerIds: peerIds,
endCurrentIfAny: true endCurrentIfAny: true,
unmuteByDefault: true
) )
completion?() completion?()
} }
@ -714,7 +715,17 @@ public final class CallListController: TelegramBaseController {
guard let self else { guard let self else {
return return
} }
self.context.joinConferenceCall(call: resolvedCallLink, isVideo: conferenceCall.flags.contains(.isVideo))
let _ = (self.context.engine.calls.getGroupCallPersistentSettings(callId: resolvedCallLink.id)
|> deliverOnMainQueue).startStandalone(next: { [weak self] value in
guard let self else {
return
}
let value: PresentationGroupCallPersistentSettings = value?.get(PresentationGroupCallPersistentSettings.self) ?? PresentationGroupCallPersistentSettings.default
self.context.joinConferenceCall(call: resolvedCallLink, isVideo: conferenceCall.flags.contains(.isVideo), unmuteByDefault: value.isMicrophoneEnabledByDefault)
})
}, error: { [weak self] error in }, error: { [weak self] error in
guard let self else { guard let self else {
return return

View File

@ -2163,8 +2163,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
switch peerValue.peer { switch peerValue.peer {
case .user, .secretChat: case .user, .secretChat:
if let peerPresence = peerPresence, case .present = peerPresence.status { if let peerPresence = peerPresence {
inputActivities = inputActivitiesValue if case .present = peerPresence.status {
inputActivities = inputActivitiesValue
} else if item.context.sharedContext.immediateExperimentalUISettings.alwaysDisplayTyping {
inputActivities = inputActivitiesValue
} else {
inputActivities = nil
}
} else { } else {
inputActivities = nil inputActivities = nil
} }

View File

@ -72,6 +72,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case redactSensitiveData(PresentationTheme, Bool) case redactSensitiveData(PresentationTheme, Bool)
case keepChatNavigationStack(PresentationTheme, Bool) case keepChatNavigationStack(PresentationTheme, Bool)
case skipReadHistory(PresentationTheme, Bool) case skipReadHistory(PresentationTheme, Bool)
case alwaysDisplayTyping(Bool)
case dustEffect(Bool) case dustEffect(Bool)
case crashOnSlowQueries(PresentationTheme, Bool) case crashOnSlowQueries(PresentationTheme, Bool)
case crashOnMemoryPressure(PresentationTheme, Bool) case crashOnMemoryPressure(PresentationTheme, Bool)
@ -131,7 +132,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logging.rawValue return DebugControllerSection.logging.rawValue
case .webViewInspection, .resetWebViewCache: case .webViewInspection, .resetWebViewCache:
return DebugControllerSection.web.rawValue return DebugControllerSection.web.rawValue
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: case .keepChatNavigationStack, .skipReadHistory, .alwaysDisplayTyping, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
return DebugControllerSection.experiments.rawValue return DebugControllerSection.experiments.rawValue
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .allForumsHaveTabs, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation: case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .allForumsHaveTabs, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation:
return DebugControllerSection.experiments.rawValue return DebugControllerSection.experiments.rawValue
@ -182,8 +183,10 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 15 return 15
case .skipReadHistory: case .skipReadHistory:
return 16 return 16
case .dustEffect: case .alwaysDisplayTyping:
return 17 return 17
case .dustEffect:
return 18
case .crashOnSlowQueries: case .crashOnSlowQueries:
return 20 return 20
case .crashOnMemoryPressure: case .crashOnMemoryPressure:
@ -959,6 +962,14 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return settings return settings
}).start() }).start()
}) })
case let .alwaysDisplayTyping(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Show Typing", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
var settings = settings
settings.alwaysDisplayTyping = value
return settings
}).start()
})
case let .dustEffect(value): case let .dustEffect(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Dust Debug", value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, title: "Dust Debug", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
@ -1494,6 +1505,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
#if DEBUG #if DEBUG
entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory))
#endif #endif
entries.append(.alwaysDisplayTyping(experimentalSettings.alwaysDisplayTyping))
entries.append(.dustEffect(experimentalSettings.dustEffect)) entries.append(.dustEffect(experimentalSettings.dustEffect))
} }
entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries)) entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries))

View File

@ -1090,7 +1090,17 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
guard let self else { guard let self else {
return return
} }
self.context.joinConferenceCall(call: resolvedCallLink, isVideo: conferenceCall.flags.contains(.isVideo))
let _ = (self.context.engine.calls.getGroupCallPersistentSettings(callId: resolvedCallLink.id)
|> deliverOnMainQueue).startStandalone(next: { [weak self] value in
guard let self else {
return
}
let value: PresentationGroupCallPersistentSettings = value?.get(PresentationGroupCallPersistentSettings.self) ?? PresentationGroupCallPersistentSettings.default
self.context.joinConferenceCall(call: resolvedCallLink, isVideo: conferenceCall.flags.contains(.isVideo), unmuteByDefault: value.isMicrophoneEnabledByDefault)
})
}, error: { [weak self] error in }, error: { [weak self] error in
guard let self else { guard let self else {
return return

View File

@ -64,14 +64,23 @@ public final class CallKitIntegration {
} }
func answerCall(uuid: UUID) { func answerCall(uuid: UUID) {
#if DEBUG
print("CallKitIntegration: Answer call \(uuid)")
#endif
sharedProviderDelegate?.answerCall(uuid: uuid) sharedProviderDelegate?.answerCall(uuid: uuid)
} }
public func dropCall(uuid: UUID) { public func dropCall(uuid: UUID) {
#if DEBUG
print("CallKitIntegration: Drop call \(uuid)")
#endif
sharedProviderDelegate?.dropCall(uuid: uuid) sharedProviderDelegate?.dropCall(uuid: uuid)
} }
public func reportIncomingCall(uuid: UUID, stableId: Int64, handle: String, phoneNumber: String?, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) { public func reportIncomingCall(uuid: UUID, stableId: Int64, handle: String, phoneNumber: String?, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) {
#if DEBUG
print("CallKitIntegration: Report incoming call \(uuid)")
#endif
sharedProviderDelegate?.reportIncomingCall(uuid: uuid, stableId: stableId, handle: handle, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle, completion: completion) sharedProviderDelegate?.reportIncomingCall(uuid: uuid, stableId: stableId, handle: handle, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle, completion: completion)
} }
@ -183,6 +192,8 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
} }
func dropCall(uuid: UUID) { func dropCall(uuid: UUID) {
self.alreadyReportedIncomingCalls.insert(uuid)
Logger.shared.log("CallKitIntegration", "report call ended \(uuid)") Logger.shared.log("CallKitIntegration", "report call ended \(uuid)")
self.provider.reportCall(with: uuid, endedAt: nil, reason: CXCallEndedReason.remoteEnded) self.provider.reportCall(with: uuid, endedAt: nil, reason: CXCallEndedReason.remoteEnded)

View File

@ -362,9 +362,58 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
) )
strongSelf.updateCurrentCall(call) strongSelf.updateCurrentCall(call)
})) }))
} else if let currentCall = self.currentCall, currentCall.peerId == firstState.1.id, currentCall.peerId.id._internalGetInt64Value() < firstState.0.account.peerId.id._internalGetInt64Value() {
let _ = currentCall.hangUp().startStandalone()
self.currentCallDisposable.set((combineLatest(
firstState.0.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, PreferencesKeys.appConfiguration]) |> take(1),
accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings]) |> take(1)
)
|> deliverOnMainQueue).start(next: { [weak self] preferences, sharedData in
guard let strongSelf = self else {
return
}
if strongSelf.currentUpgradedToConferenceCallId == firstState.2.id {
return
}
let configuration = preferences.values[PreferencesKeys.voipConfiguration]?.get(VoipConfiguration.self) ?? .defaultValue
let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) ?? .defaultSettings
let experimentalSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? .defaultSettings
let appConfiguration = preferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
let call = PresentationCallImpl(
context: firstState.0,
audioSession: strongSelf.audioSession,
callSessionManager: firstState.0.account.callSessionManager,
callKitIntegration: enableCallKit ? callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings) : nil,
serializedData: configuration.serializedData,
dataSaving: effectiveDataSaving(for: strongSelf.callSettings, autodownloadSettings: autodownloadSettings),
getDeviceAccessData: strongSelf.getDeviceAccessData,
initialState: nil,
internalId: firstState.2.id,
peerId: firstState.2.peerId,
isOutgoing: false,
incomingConferenceSource: firstState.2.conferenceSource,
peer: EnginePeer(firstState.1),
proxyServer: strongSelf.proxyServer,
auxiliaryServers: [],
currentNetworkType: firstState.4,
updatedNetworkType: firstState.0.account.networkType,
startWithVideo: firstState.2.isVideo,
isVideoPossible: firstState.2.isVideoPossible,
enableStunMarking: shouldEnableStunMarking(appConfiguration: appConfiguration),
enableTCP: experimentalSettings.enableVoipTcp,
preferredVideoCodec: experimentalSettings.preferredVideoCodec
)
strongSelf.updateCurrentCall(call)
call.answer()
}))
} else { } else {
for (context, _, state, _, _) in ringingStates { for (context, _, state, _, _) in ringingStates {
if state.id != self.currentCall?.internalId { if state.id != self.currentCall?.internalId {
self.callKitIntegration?.dropCall(uuid: state.id)
context.account.callSessionManager.drop(internalId: state.id, reason: .busy, debugLog: .single(nil)) context.account.callSessionManager.drop(internalId: state.id, reason: .busy, debugLog: .single(nil))
} }
} }
@ -1085,7 +1134,8 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
reference: InternalGroupCallReference, reference: InternalGroupCallReference,
beginWithVideo: Bool, beginWithVideo: Bool,
invitePeerIds: [EnginePeer.Id], invitePeerIds: [EnginePeer.Id],
endCurrentIfAny: Bool endCurrentIfAny: Bool,
unmuteByDefault: Bool
) -> JoinGroupCallManagerResult { ) -> JoinGroupCallManagerResult {
let begin: () -> Void = { [weak self] in let begin: () -> Void = { [weak self] in
guard let self else { guard let self else {
@ -1114,7 +1164,8 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
conferenceSourceId: nil, conferenceSourceId: nil,
isConference: true, isConference: true,
beginWithVideo: beginWithVideo, beginWithVideo: beginWithVideo,
sharedAudioContext: nil sharedAudioContext: nil,
unmuteByDefault: unmuteByDefault
) )
for peerId in invitePeerIds { for peerId in invitePeerIds {
let _ = call.invitePeer(peerId, isVideo: beginWithVideo) let _ = call.invitePeer(peerId, isVideo: beginWithVideo)

View File

@ -839,7 +839,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
conferenceSourceId: CallSessionInternalId?, conferenceSourceId: CallSessionInternalId?,
isConference: Bool, isConference: Bool,
beginWithVideo: Bool, beginWithVideo: Bool,
sharedAudioContext: SharedCallAudioContext? sharedAudioContext: SharedCallAudioContext?,
unmuteByDefault: Bool? = nil
) { ) {
self.account = accountContext.account self.account = accountContext.account
self.accountContext = accountContext self.accountContext = accountContext
@ -873,10 +874,18 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.beginWithVideo = beginWithVideo self.beginWithVideo = beginWithVideo
self.keyPair = keyPair self.keyPair = keyPair
if self.isConference && conferenceSourceId == nil { if let unmuteByDefault {
self.isMutedValue = .unmuted if unmuteByDefault {
self.isMutedPromise.set(self.isMutedValue) self.isMutedValue = .unmuted
self.stateValue.muteState = nil self.isMutedPromise.set(self.isMutedValue)
self.stateValue.muteState = nil
}
} else {
if self.isConference && conferenceSourceId == nil {
self.isMutedValue = .unmuted
self.isMutedPromise.set(self.isMutedValue)
self.stateValue.muteState = nil
}
} }
if let keyPair, let initialCall { if let keyPair, let initialCall {
@ -2959,6 +2968,18 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
self.genericCallContext?.setIsMuted(isEffectivelyMuted) self.genericCallContext?.setIsMuted(isEffectivelyMuted)
if let callId = self.callId {
let context = self.accountContext
let _ = (context.engine.calls.getGroupCallPersistentSettings(callId: callId)
|> deliverOnMainQueue).startStandalone(next: { value in
var value: PresentationGroupCallPersistentSettings = value?.get(PresentationGroupCallPersistentSettings.self) ?? PresentationGroupCallPersistentSettings.default
value.isMicrophoneEnabledByDefault = !isVisuallyMuted
if let entry = CodableEntry(value) {
context.engine.calls.setGroupCallPersistentSettings(callId: callId, value: entry)
}
})
}
if isVisuallyMuted { if isVisuallyMuted {
self.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false) self.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false)
} else { } else {

View File

@ -142,6 +142,7 @@ public struct Namespaces {
public static let recommendedBots: Int8 = 44 public static let recommendedBots: Int8 = 44
public static let channelsForPublicReaction: Int8 = 45 public static let channelsForPublicReaction: Int8 = 45
public static let cachedGroupsInCommon: Int8 = 46 public static let cachedGroupsInCommon: Int8 = 46
public static let groupCallPersistentSettings: Int8 = 47
} }
public struct UnorderedItemList { public struct UnorderedItemList {

View File

@ -178,5 +178,21 @@ public extension TelegramEngine {
public func getGroupCallStreamCredentials(peerId: EnginePeer.Id, revokePreviousCredentials: Bool) -> Signal<GroupCallStreamCredentials, GetGroupCallStreamCredentialsError> { public func getGroupCallStreamCredentials(peerId: EnginePeer.Id, revokePreviousCredentials: Bool) -> Signal<GroupCallStreamCredentials, GetGroupCallStreamCredentialsError> {
return _internal_getGroupCallStreamCredentials(account: self.account, peerId: peerId, revokePreviousCredentials: revokePreviousCredentials) return _internal_getGroupCallStreamCredentials(account: self.account, peerId: peerId, revokePreviousCredentials: revokePreviousCredentials)
} }
public func getGroupCallPersistentSettings(callId: Int64) -> Signal<CodableEntry?, NoError> {
return self.account.postbox.transaction { transaction -> CodableEntry? in
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: callId)
return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.groupCallPersistentSettings, key: key))
}
}
public func setGroupCallPersistentSettings(callId: Int64, value: CodableEntry) {
let _ = self.account.postbox.transaction({ transaction -> Void in
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: callId)
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.groupCallPersistentSettings, key: key), entry: value)
}).startStandalone()
}
} }
} }

View File

@ -1387,15 +1387,21 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
let _ = (signal let _ = (signal
|> deliverOnMainQueue).startStandalone(next: { [weak navigationController] resolvedCallLink in |> deliverOnMainQueue).startStandalone(next: { [weak navigationController] resolvedCallLink in
navigationController?.pushViewController(context.sharedContext.makeJoinSubjectScreen(context: context, mode: JoinSubjectScreenMode.groupCall(JoinSubjectScreenMode.GroupCall( let _ = (context.engine.calls.getGroupCallPersistentSettings(callId: resolvedCallLink.id)
id: resolvedCallLink.id, |> deliverOnMainQueue).startStandalone(next: { value in
accessHash: resolvedCallLink.accessHash, let value: PresentationGroupCallPersistentSettings = value?.get(PresentationGroupCallPersistentSettings.self) ?? PresentationGroupCallPersistentSettings.default
slug: link,
inviter: resolvedCallLink.inviter, navigationController?.pushViewController(context.sharedContext.makeJoinSubjectScreen(context: context, mode: JoinSubjectScreenMode.groupCall(JoinSubjectScreenMode.GroupCall(
members: resolvedCallLink.members, id: resolvedCallLink.id,
totalMemberCount: resolvedCallLink.totalMemberCount, accessHash: resolvedCallLink.accessHash,
info: resolvedCallLink slug: link,
)))) inviter: resolvedCallLink.inviter,
members: resolvedCallLink.members,
totalMemberCount: resolvedCallLink.totalMemberCount,
info: resolvedCallLink,
enableMicrophoneByDefault: value.isMicrophoneEnabledByDefault
))))
})
}) })
case let .localization(identifier): case let .localization(identifier):
strongSelf.presentController(LanguageLinkPreviewController(context: strongSelf.context, identifier: identifier), .window(.root), nil) strongSelf.presentController(LanguageLinkPreviewController(context: strongSelf.context, identifier: identifier), .window(.root), nil)

View File

@ -29,6 +29,8 @@ swift_library(
"//submodules/UndoUI", "//submodules/UndoUI",
"//submodules/SSignalKit/SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit",
"//submodules/PresentationDataUtils", "//submodules/PresentationDataUtils",
"//submodules/TelegramUI/Components/CheckComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -12,12 +12,15 @@ import BalancedTextComponent
import ButtonComponent import ButtonComponent
import BundleIconComponent import BundleIconComponent
import Markdown import Markdown
import Postbox
import TelegramCore import TelegramCore
import AvatarNode import AvatarNode
import TelegramStringFormatting import TelegramStringFormatting
import AnimatedAvatarSetNode import AnimatedAvatarSetNode
import UndoUI import UndoUI
import PresentationDataUtils import PresentationDataUtils
import CheckComponent
import PlainButtonComponent
private final class JoinSubjectScreenComponent: Component { private final class JoinSubjectScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -83,6 +86,8 @@ private final class JoinSubjectScreenComponent: Component {
private var previewPeersAvatarsNode: AnimatedAvatarSetNode? private var previewPeersAvatarsNode: AnimatedAvatarSetNode?
private var previewPeersAvatarsContext: AnimatedAvatarSetContext? private var previewPeersAvatarsContext: AnimatedAvatarSetContext?
private var callMicrophoneOption: ComponentView<Empty>?
private let titleTransformContainer: UIView private let titleTransformContainer: UIView
private let bottomPanelContainer: UIView private let bottomPanelContainer: UIView
private let actionButton = ComponentView<Empty>() private let actionButton = ComponentView<Empty>()
@ -102,6 +107,8 @@ private final class JoinSubjectScreenComponent: Component {
private var topOffsetDistance: CGFloat? private var topOffsetDistance: CGFloat?
private var cachedCloseImage: UIImage? private var cachedCloseImage: UIImage?
private var callMicrophoneIsEnabled: Bool = true
private var isJoining: Bool = false private var isJoining: Bool = false
private var joinDisposable: Disposable? private var joinDisposable: Disposable?
@ -394,7 +401,7 @@ private final class JoinSubjectScreenComponent: Component {
self.environment?.controller()?.dismiss() self.environment?.controller()?.dismiss()
}) })
case let .groupCall(groupCall): case let .groupCall(groupCall):
component.context.joinConferenceCall(call: groupCall.info, isVideo: false) component.context.joinConferenceCall(call: groupCall.info, isVideo: false, unmuteByDefault: self.callMicrophoneIsEnabled)
self.environment?.controller()?.dismiss() self.environment?.controller()?.dismiss()
} }
@ -414,6 +421,12 @@ private final class JoinSubjectScreenComponent: Component {
let sideInset: CGFloat = 16.0 + environment.safeInsets.left let sideInset: CGFloat = 16.0 + environment.safeInsets.left
if self.component == nil { if self.component == nil {
switch component.mode {
case .group:
break
case let .groupCall(groupCall):
self.callMicrophoneIsEnabled = groupCall.enableMicrophoneByDefault
}
} }
self.component = component self.component = component
@ -834,6 +847,85 @@ private final class JoinSubjectScreenComponent: Component {
} }
} }
if case .groupCall = component.mode {
let callMicrophoneOption: ComponentView<Empty>
var callMicrophoneOptionTransition = transition
if let current = self.callMicrophoneOption {
callMicrophoneOption = current
} else {
callMicrophoneOptionTransition = callMicrophoneOptionTransition.withAnimation(.none)
callMicrophoneOption = ComponentView()
self.callMicrophoneOption = callMicrophoneOption
}
let checkTheme = CheckComponent.Theme(
backgroundColor: environment.theme.list.itemCheckColors.fillColor,
strokeColor: environment.theme.list.itemCheckColors.foregroundColor,
borderColor: environment.theme.list.itemCheckColors.strokeColor,
overlayBorder: false,
hasInset: false,
hasShadow: false
)
let callMicrophoneOptionSize = callMicrophoneOption.update(
transition: callMicrophoneOptionTransition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(HStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent(
theme: checkTheme,
size: CGSize(width: 18.0, height: 18.0),
selected: self.callMicrophoneIsEnabled
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Invitation_JoinGroupCall_EnableMicrophone, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor))
)))
], spacing: 10.0)),
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
self.callMicrophoneIsEnabled = !self.callMicrophoneIsEnabled
let callMicrophoneIsEnabled = self.callMicrophoneIsEnabled
if case let .groupCall(groupCall) = component.mode {
let context = component.context
let _ = (component.context.engine.calls.getGroupCallPersistentSettings(callId: groupCall.id)
|> deliverOnMainQueue).startStandalone(next: { value in
var value: PresentationGroupCallPersistentSettings = value?.get(PresentationGroupCallPersistentSettings.self) ?? PresentationGroupCallPersistentSettings.default
value.isMicrophoneEnabledByDefault = callMicrophoneIsEnabled
if let entry = CodableEntry(value) {
context.engine.calls.setGroupCallPersistentSettings(callId: groupCall.id, value: entry)
}
})
}
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
},
animateAlpha: false,
animateScale: false
)),
environment: {
},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
let callMicrophoneOptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - callMicrophoneOptionSize.width) * 0.5), y: contentHeight), size: callMicrophoneOptionSize)
if let callMicrophoneOptionView = callMicrophoneOption.view {
if callMicrophoneOptionView.superview == nil {
self.scrollContentView.addSubview(callMicrophoneOptionView)
}
callMicrophoneOptionTransition.setFrame(view: callMicrophoneOptionView, frame: callMicrophoneOptionFrame)
}
contentHeight += callMicrophoneOptionSize.height + 23.0
} else {
if let callMicrophoneOption = self.callMicrophoneOption {
self.callMicrophoneOption = nil
callMicrophoneOption.view?.removeFromSuperview()
}
}
let actionButtonTitle: String let actionButtonTitle: String
switch component.mode { switch component.mode {
case .group: case .group:

View File

@ -684,7 +684,7 @@ public final class AccountContextImpl: AccountContext {
} }
} }
public func joinConferenceCall(call: JoinCallLinkInformation, isVideo: Bool) { public func joinConferenceCall(call: JoinCallLinkInformation, isVideo: Bool, unmuteByDefault: Bool) {
guard let callManager = self.sharedContext.callManager else { guard let callManager = self.sharedContext.callManager else {
return return
} }
@ -701,7 +701,8 @@ public final class AccountContextImpl: AccountContext {
reference: call.reference, reference: call.reference,
beginWithVideo: isVideo, beginWithVideo: isVideo,
invitePeerIds: [], invitePeerIds: [],
endCurrentIfAny: false endCurrentIfAny: false,
unmuteByDefault: unmuteByDefault
) )
if case let .alreadyInProgress(currentPeerId) = result { if case let .alreadyInProgress(currentPeerId) = result {
let dataInput: Signal<EnginePeer?, NoError> let dataInput: Signal<EnginePeer?, NoError>
@ -749,7 +750,8 @@ public final class AccountContextImpl: AccountContext {
reference: call.reference, reference: call.reference,
beginWithVideo: isVideo, beginWithVideo: isVideo,
invitePeerIds: [], invitePeerIds: [],
endCurrentIfAny: true endCurrentIfAny: true,
unmuteByDefault: unmuteByDefault
) )
})]), on: .root) })]), on: .root)
default: default:
@ -772,7 +774,8 @@ public final class AccountContextImpl: AccountContext {
reference: call.reference, reference: call.reference,
beginWithVideo: isVideo, beginWithVideo: isVideo,
invitePeerIds: [], invitePeerIds: [],
endCurrentIfAny: true endCurrentIfAny: true,
unmuteByDefault: unmuteByDefault
) )
})]), on: .root) })]), on: .root)
} }

View File

@ -80,6 +80,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
private let fetchDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable()
private var statusDisposable: Disposable? private var statusDisposable: Disposable?
private var progressDisposable: Disposable?
private let animationCache: AnimationCache? private let animationCache: AnimationCache?
private let animationRenderer: MultiAnimationRenderer? private let animationRenderer: MultiAnimationRenderer?
@ -234,6 +235,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
self.fetchDisposable.dispose() self.fetchDisposable.dispose()
self.statusDisposable?.dispose() self.statusDisposable?.dispose()
self.translationDisposable.dispose() self.translationDisposable.dispose()
self.progressDisposable?.dispose()
} }
private var theme: PresentationTheme? private var theme: PresentationTheme?
@ -260,37 +262,10 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
return status == .pinnedMessage return status == .pinnedMessage
} }
|> deliverOnMainQueue).startStrict(next: { [weak self] isLoading in |> deliverOnMainQueue).startStrict(next: { [weak self] isLoading in
guard let strongSelf = self else { guard let self else {
return return
} }
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) self.updateIsLoading(isLoading: isLoading)
if isLoading {
if strongSelf.activityIndicator.alpha.isZero {
transition.updateAlpha(node: strongSelf.activityIndicator, alpha: 1.0)
transition.updateSublayerTransformScale(node: strongSelf.activityIndicatorContainer, scale: 1.0)
transition.updateAlpha(node: strongSelf.buttonsContainer, alpha: 0.0)
transition.updateSublayerTransformScale(node: strongSelf.buttonsContainer, scale: 0.1)
if let theme = strongSelf.theme {
strongSelf.activityIndicator.transitionToState(.progress(color: theme.chat.inputPanel.panelControlAccentColor, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: false, completion: {
})
}
}
} else {
if !strongSelf.activityIndicator.alpha.isZero {
transition.updateAlpha(node: strongSelf.activityIndicator, alpha: 0.0, completion: { [weak self] completed in
if completed {
self?.activityIndicator.transitionToState(.none, animated: false, completion: {
})
}
})
transition.updateSublayerTransformScale(node: strongSelf.activityIndicatorContainer, scale: 0.1)
transition.updateAlpha(node: strongSelf.buttonsContainer, alpha: 1.0)
transition.updateSublayerTransformScale(node: strongSelf.buttonsContainer, scale: 1.0)
}
}
}) })
} }
@ -333,6 +308,14 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
} }
} }
} }
if actionTitle == nil {
for media in message.message.media {
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.type == "telegram_call" {
actionTitle = interfaceState.strings.Chat_TitleJoinGroupCall
}
}
}
} else { } else {
actionTitle = nil actionTitle = nil
} }
@ -412,7 +395,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0))
if let actionTitle = actionTitle { if let actionTitle {
var actionButtonTransition = transition var actionButtonTransition = transition
var animateButtonIn = false var animateButtonIn = false
if self.actionButton.isHidden { if self.actionButton.isHidden {
@ -540,6 +523,37 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
return LayoutResult(backgroundHeight: panelHeight, insetHeight: panelHeight, hitTestSlop: 0.0) return LayoutResult(backgroundHeight: panelHeight, insetHeight: panelHeight, hitTestSlop: 0.0)
} }
private func updateIsLoading(isLoading: Bool) {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
if isLoading {
if self.activityIndicator.alpha.isZero {
transition.updateAlpha(node: self.activityIndicator, alpha: 1.0)
transition.updateSublayerTransformScale(node: self.activityIndicatorContainer, scale: 1.0)
transition.updateAlpha(node: self.buttonsContainer, alpha: 0.0)
transition.updateSublayerTransformScale(node: self.buttonsContainer, scale: 0.1)
if let theme = self.theme {
self.activityIndicator.transitionToState(.progress(color: theme.chat.inputPanel.panelControlAccentColor, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: false, completion: {
})
}
}
} else {
if !self.activityIndicator.alpha.isZero {
transition.updateAlpha(node: self.activityIndicator, alpha: 0.0, completion: { [weak self] completed in
if completed {
self?.activityIndicator.transitionToState(.none, animated: false, completion: {
})
}
})
transition.updateSublayerTransformScale(node: self.activityIndicatorContainer, scale: 0.1)
transition.updateAlpha(node: self.buttonsContainer, alpha: 1.0)
transition.updateSublayerTransformScale(node: self.buttonsContainer, scale: 1.0)
}
}
}
private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool, translateToLanguage: String?) { private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool, translateToLanguage: String?) {
let message = pinnedMessage.message let message = pinnedMessage.message
@ -667,6 +681,9 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
if let media = media as? TelegramMediaInvoice { if let media = media as? TelegramMediaInvoice {
titleStrings = [.text(0, NSAttributedString(string: media.title, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))] titleStrings = [.text(0, NSAttributedString(string: media.title, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))]
break break
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.type == "telegram_call" {
titleStrings = [.text(0, NSAttributedString(string: strings.Chat_PinnedGroupCallTitle, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))]
break
} }
} }
} }
@ -903,20 +920,26 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
switch button.action { switch button.action {
case .text: case .text:
controllerInteraction.sendMessage(button.title) controllerInteraction.sendMessage(button.title)
return
case let .url(url): case let .url(url):
var isConcealed = true var isConcealed = true
if url.hasPrefix("tg://") { if url.hasPrefix("tg://") {
isConcealed = false isConcealed = false
} }
controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: isConcealed, progress: Promise())) controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: isConcealed, progress: Promise()))
return
case .requestMap: case .requestMap:
controllerInteraction.shareCurrentLocation() controllerInteraction.shareCurrentLocation()
return
case .requestPhone: case .requestPhone:
controllerInteraction.shareAccountContact() controllerInteraction.shareAccountContact()
return
case .openWebApp: case .openWebApp:
controllerInteraction.requestMessageActionCallback(message.id, nil, true, false) controllerInteraction.requestMessageActionCallback(message.id, nil, true, false)
return
case let .callback(requiresPassword, data): case let .callback(requiresPassword, data):
controllerInteraction.requestMessageActionCallback(message.id, data, false, requiresPassword) controllerInteraction.requestMessageActionCallback(message.id, data, false, requiresPassword)
return
case let .switchInline(samePeer, query, peerTypes): case let .switchInline(samePeer, query, peerTypes):
var botPeer: Peer? var botPeer: Peer?
@ -940,10 +963,13 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
if let botPeer = botPeer, let addressName = botPeer.addressName { if let botPeer = botPeer, let addressName = botPeer.addressName {
controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)", peerTypes) controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)", peerTypes)
} }
return
case .payment: case .payment:
controllerInteraction.openCheckoutOrReceipt(message.id, nil) controllerInteraction.openCheckoutOrReceipt(message.id, nil)
return
case let .urlAuth(url, buttonId): case let .urlAuth(url, buttonId):
controllerInteraction.requestMessageActionUrlAuth(url, .message(id: message.id, buttonId: buttonId)) controllerInteraction.requestMessageActionUrlAuth(url, .message(id: message.id, buttonId: buttonId))
return
case .setupPoll: case .setupPoll:
break break
case let .openUserProfile(peerId): case let .openUserProfile(peerId):
@ -953,17 +979,47 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
controllerInteraction.openPeer(peer, .info(nil), nil, .default) controllerInteraction.openPeer(peer, .info(nil), nil, .default)
} }
}) })
return
case let .openWebView(url, simple): case let .openWebView(url, simple):
controllerInteraction.openWebView(button.title, url, simple, .generic) controllerInteraction.openWebView(button.title, url, simple, .generic)
return
case .requestPeer: case .requestPeer:
break break
case let .copyText(payload): case let .copyText(payload):
controllerInteraction.copyText(payload) controllerInteraction.copyText(payload)
return
} }
break break
} }
} }
for media in message.media {
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.type == "telegram_call" {
var isConcealed = true
if content.url.hasPrefix("tg://") {
isConcealed = false
}
let progressPromise = Promise<Bool>()
controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(
url: content.url,
concealed: isConcealed,
message: message,
progress: progressPromise
))
self.progressDisposable?.dispose()
self.progressDisposable = (progressPromise.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
guard let self else {
return
}
self.updateIsLoading(isLoading: value)
})
return
}
}
} }
} }
} }

View File

@ -396,20 +396,26 @@ func openResolvedUrlImpl(
let _ = (signal let _ = (signal
|> deliverOnMainQueue).startStandalone(next: { [weak navigationController] resolvedCallLink in |> deliverOnMainQueue).startStandalone(next: { [weak navigationController] resolvedCallLink in
if let currentGroupCallController = context.sharedContext.currentGroupCallController as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == resolvedCallLink.id { let _ = (context.engine.calls.getGroupCallPersistentSettings(callId: resolvedCallLink.id)
context.sharedContext.navigateToCurrentCall() |> deliverOnMainQueue).startStandalone(next: { value in
return let value: PresentationGroupCallPersistentSettings = value?.get(PresentationGroupCallPersistentSettings.self) ?? PresentationGroupCallPersistentSettings.default
}
if let currentGroupCallController = context.sharedContext.currentGroupCallController as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == resolvedCallLink.id {
navigationController?.pushViewController(context.sharedContext.makeJoinSubjectScreen(context: context, mode: JoinSubjectScreenMode.groupCall(JoinSubjectScreenMode.GroupCall( context.sharedContext.navigateToCurrentCall()
id: resolvedCallLink.id, return
accessHash: resolvedCallLink.accessHash, }
slug: link,
inviter: resolvedCallLink.inviter, navigationController?.pushViewController(context.sharedContext.makeJoinSubjectScreen(context: context, mode: JoinSubjectScreenMode.groupCall(JoinSubjectScreenMode.GroupCall(
members: resolvedCallLink.members, id: resolvedCallLink.id,
totalMemberCount: resolvedCallLink.totalMemberCount, accessHash: resolvedCallLink.accessHash,
info: resolvedCallLink slug: link,
)))) inviter: resolvedCallLink.inviter,
members: resolvedCallLink.members,
totalMemberCount: resolvedCallLink.totalMemberCount,
info: resolvedCallLink,
enableMicrophoneByDefault: value.isMicrophoneEnabledByDefault
))))
})
}, error: { _ in }, error: { _ in
var elevatedLayout = true var elevatedLayout = true
if case .chat = urlContext { if case .chat = urlContext {

View File

@ -2065,7 +2065,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash),
beginWithVideo: isVideo, beginWithVideo: isVideo,
invitePeerIds: peerIds, invitePeerIds: peerIds,
endCurrentIfAny: true endCurrentIfAny: true,
unmuteByDefault: true
) )
completion?() completion?()
} }

View File

@ -27,6 +27,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
public var keepChatNavigationStack: Bool public var keepChatNavigationStack: Bool
public var skipReadHistory: Bool public var skipReadHistory: Bool
public var alwaysDisplayTyping: Bool
public var crashOnLongQueries: Bool public var crashOnLongQueries: Bool
public var chatListPhotos: Bool public var chatListPhotos: Bool
public var knockoutWallpaper: Bool public var knockoutWallpaper: Bool
@ -72,6 +73,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
return ExperimentalUISettings( return ExperimentalUISettings(
keepChatNavigationStack: false, keepChatNavigationStack: false,
skipReadHistory: false, skipReadHistory: false,
alwaysDisplayTyping: false,
crashOnLongQueries: false, crashOnLongQueries: false,
chatListPhotos: false, chatListPhotos: false,
knockoutWallpaper: false, knockoutWallpaper: false,
@ -118,6 +120,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
public init( public init(
keepChatNavigationStack: Bool, keepChatNavigationStack: Bool,
skipReadHistory: Bool, skipReadHistory: Bool,
alwaysDisplayTyping: Bool,
crashOnLongQueries: Bool, crashOnLongQueries: Bool,
chatListPhotos: Bool, chatListPhotos: Bool,
knockoutWallpaper: Bool, knockoutWallpaper: Bool,
@ -161,6 +164,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
) { ) {
self.keepChatNavigationStack = keepChatNavigationStack self.keepChatNavigationStack = keepChatNavigationStack
self.skipReadHistory = skipReadHistory self.skipReadHistory = skipReadHistory
self.alwaysDisplayTyping = alwaysDisplayTyping
self.crashOnLongQueries = crashOnLongQueries self.crashOnLongQueries = crashOnLongQueries
self.chatListPhotos = chatListPhotos self.chatListPhotos = chatListPhotos
self.knockoutWallpaper = knockoutWallpaper self.knockoutWallpaper = knockoutWallpaper
@ -208,6 +212,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.keepChatNavigationStack = (try container.decodeIfPresent(Int32.self, forKey: "keepChatNavigationStack") ?? 0) != 0 self.keepChatNavigationStack = (try container.decodeIfPresent(Int32.self, forKey: "keepChatNavigationStack") ?? 0) != 0
self.skipReadHistory = (try container.decodeIfPresent(Int32.self, forKey: "skipReadHistory") ?? 0) != 0 self.skipReadHistory = (try container.decodeIfPresent(Int32.self, forKey: "skipReadHistory") ?? 0) != 0
self.alwaysDisplayTyping = (try container.decodeIfPresent(Int32.self, forKey: "alwaysDisplayTyping") ?? 0) != 0
self.crashOnLongQueries = (try container.decodeIfPresent(Int32.self, forKey: "crashOnLongQueries") ?? 0) != 0 self.crashOnLongQueries = (try container.decodeIfPresent(Int32.self, forKey: "crashOnLongQueries") ?? 0) != 0
self.chatListPhotos = (try container.decodeIfPresent(Int32.self, forKey: "chatListPhotos") ?? 0) != 0 self.chatListPhotos = (try container.decodeIfPresent(Int32.self, forKey: "chatListPhotos") ?? 0) != 0
self.knockoutWallpaper = (try container.decodeIfPresent(Int32.self, forKey: "knockoutWallpaper") ?? 0) != 0 self.knockoutWallpaper = (try container.decodeIfPresent(Int32.self, forKey: "knockoutWallpaper") ?? 0) != 0
@ -255,6 +260,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
try container.encode((self.keepChatNavigationStack ? 1 : 0) as Int32, forKey: "keepChatNavigationStack") try container.encode((self.keepChatNavigationStack ? 1 : 0) as Int32, forKey: "keepChatNavigationStack")
try container.encode((self.skipReadHistory ? 1 : 0) as Int32, forKey: "skipReadHistory") try container.encode((self.skipReadHistory ? 1 : 0) as Int32, forKey: "skipReadHistory")
try container.encode((self.alwaysDisplayTyping ? 1 : 0) as Int32, forKey: "alwaysDisplayTyping")
try container.encode((self.crashOnLongQueries ? 1 : 0) as Int32, forKey: "crashOnLongQueries") try container.encode((self.crashOnLongQueries ? 1 : 0) as Int32, forKey: "crashOnLongQueries")
try container.encode((self.chatListPhotos ? 1 : 0) as Int32, forKey: "chatListPhotos") try container.encode((self.chatListPhotos ? 1 : 0) as Int32, forKey: "chatListPhotos")
try container.encode((self.knockoutWallpaper ? 1 : 0) as Int32, forKey: "knockoutWallpaper") try container.encode((self.knockoutWallpaper ? 1 : 0) as Int32, forKey: "knockoutWallpaper")