diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index fb6831f450..16fdb56117 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14418,3 +14418,7 @@ Sorry for the inconvenience."; "Monoforum.DeleteTopic.Title" = "Delete %@"; "Channel.RemoveFeeAlert.Text" = "Are you sure you want to allow **%@** to message your channel for free?"; "StarsBalance.ChannelBalance" = "Your channel balance is %@"; + +"Chat.TitleJoinGroupCall" = "Join"; +"Invitation.JoinGroupCall.EnableMicrophone" = "Switch on the microphone"; +"Chat.PinnedGroupCallTitle" = "Pinned Group Call"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index f56c8dbc76..ba1a4f541d 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -996,8 +996,9 @@ public enum JoinSubjectScreenMode { public let members: [EnginePeer] public let totalMemberCount: Int 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.accessHash = accessHash self.slug = slug @@ -1005,6 +1006,7 @@ public enum JoinSubjectScreenMode { self.members = members self.totalMemberCount = totalMemberCount self.info = info + self.enableMicrophoneByDefault = enableMicrophoneByDefault } } @@ -1376,7 +1378,7 @@ public protocol AccountContext: AnyObject { func scheduleGroupCall(peerId: PeerId, parentController: ViewController) 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) } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 040aa2bab3..2a6110ce99 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -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 { var account: Account { get } var accountContext: AccountContext { get } @@ -580,6 +592,7 @@ public protocol PresentationCallManager: AnyObject { reference: InternalGroupCallReference, beginWithVideo: Bool, invitePeerIds: [EnginePeer.Id], - endCurrentIfAny: Bool + endCurrentIfAny: Bool, + unmuteByDefault: Bool ) -> JoinGroupCallManagerResult } diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 407693bec4..2fddede855 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -278,7 +278,8 @@ public final class CallListController: TelegramBaseController { reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), beginWithVideo: isVideo, invitePeerIds: peerIds, - endCurrentIfAny: true + endCurrentIfAny: true, + unmuteByDefault: true ) completion?() } @@ -714,7 +715,17 @@ public final class CallListController: TelegramBaseController { guard let self else { 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 guard let self else { return diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 21201a83c3..f04e0edc59 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2163,8 +2163,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { switch peerValue.peer { case .user, .secretChat: - if let peerPresence = peerPresence, case .present = peerPresence.status { - inputActivities = inputActivitiesValue + if let peerPresence = peerPresence { + if case .present = peerPresence.status { + inputActivities = inputActivitiesValue + } else if item.context.sharedContext.immediateExperimentalUISettings.alwaysDisplayTyping { + inputActivities = inputActivitiesValue + } else { + inputActivities = nil + } } else { inputActivities = nil } diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 66d1765b66..77384b09b5 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -72,6 +72,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case redactSensitiveData(PresentationTheme, Bool) case keepChatNavigationStack(PresentationTheme, Bool) case skipReadHistory(PresentationTheme, Bool) + case alwaysDisplayTyping(Bool) case dustEffect(Bool) case crashOnSlowQueries(PresentationTheme, Bool) case crashOnMemoryPressure(PresentationTheme, Bool) @@ -131,7 +132,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .webViewInspection, .resetWebViewCache: return DebugControllerSection.web.rawValue - case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: + case .keepChatNavigationStack, .skipReadHistory, .alwaysDisplayTyping, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: 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: return DebugControllerSection.experiments.rawValue @@ -182,8 +183,10 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 15 case .skipReadHistory: return 16 - case .dustEffect: + case .alwaysDisplayTyping: return 17 + case .dustEffect: + return 18 case .crashOnSlowQueries: return 20 case .crashOnMemoryPressure: @@ -959,6 +962,14 @@ private enum DebugControllerEntry: ItemListNodeEntry { return settings }).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): 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 @@ -1494,6 +1505,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present #if DEBUG entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) #endif + entries.append(.alwaysDisplayTyping(experimentalSettings.alwaysDisplayTyping)) entries.append(.dustEffect(experimentalSettings.dustEffect)) } entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries)) diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 3f87d69206..d62e48ffe8 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -1090,7 +1090,17 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { guard let self else { 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 guard let self else { return diff --git a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift index 323457dbb7..22f5047e08 100644 --- a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift +++ b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift @@ -64,14 +64,23 @@ public final class CallKitIntegration { } func answerCall(uuid: UUID) { + #if DEBUG + print("CallKitIntegration: Answer call \(uuid)") + #endif sharedProviderDelegate?.answerCall(uuid: uuid) } public func dropCall(uuid: UUID) { + #if DEBUG + print("CallKitIntegration: Drop call \(uuid)") + #endif sharedProviderDelegate?.dropCall(uuid: uuid) } 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) } @@ -183,6 +192,8 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate { } func dropCall(uuid: UUID) { + self.alreadyReportedIncomingCalls.insert(uuid) + Logger.shared.log("CallKitIntegration", "report call ended \(uuid)") self.provider.reportCall(with: uuid, endedAt: nil, reason: CXCallEndedReason.remoteEnded) diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index ceec744d40..02174a1357 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -362,9 +362,58 @@ public final class PresentationCallManagerImpl: PresentationCallManager { ) 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 { for (context, _, state, _, _) in ringingStates { if state.id != self.currentCall?.internalId { + self.callKitIntegration?.dropCall(uuid: state.id) context.account.callSessionManager.drop(internalId: state.id, reason: .busy, debugLog: .single(nil)) } } @@ -1085,7 +1134,8 @@ public final class PresentationCallManagerImpl: PresentationCallManager { reference: InternalGroupCallReference, beginWithVideo: Bool, invitePeerIds: [EnginePeer.Id], - endCurrentIfAny: Bool + endCurrentIfAny: Bool, + unmuteByDefault: Bool ) -> JoinGroupCallManagerResult { let begin: () -> Void = { [weak self] in guard let self else { @@ -1114,7 +1164,8 @@ public final class PresentationCallManagerImpl: PresentationCallManager { conferenceSourceId: nil, isConference: true, beginWithVideo: beginWithVideo, - sharedAudioContext: nil + sharedAudioContext: nil, + unmuteByDefault: unmuteByDefault ) for peerId in invitePeerIds { let _ = call.invitePeer(peerId, isVideo: beginWithVideo) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 8637bc87fc..68f829c7f9 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -839,7 +839,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { conferenceSourceId: CallSessionInternalId?, isConference: Bool, beginWithVideo: Bool, - sharedAudioContext: SharedCallAudioContext? + sharedAudioContext: SharedCallAudioContext?, + unmuteByDefault: Bool? = nil ) { self.account = accountContext.account self.accountContext = accountContext @@ -873,10 +874,18 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.beginWithVideo = beginWithVideo self.keyPair = keyPair - if self.isConference && conferenceSourceId == nil { - self.isMutedValue = .unmuted - self.isMutedPromise.set(self.isMutedValue) - self.stateValue.muteState = nil + if let unmuteByDefault { + if unmuteByDefault { + self.isMutedValue = .unmuted + 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 { @@ -2959,6 +2968,18 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } 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 { self.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false) } else { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index b35e834e07..3695c1c33c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -142,6 +142,7 @@ public struct Namespaces { public static let recommendedBots: Int8 = 44 public static let channelsForPublicReaction: Int8 = 45 public static let cachedGroupsInCommon: Int8 = 46 + public static let groupCallPersistentSettings: Int8 = 47 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift index 3730b5c2cd..3b2c2f2762 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift @@ -178,5 +178,21 @@ public extension TelegramEngine { public func getGroupCallStreamCredentials(peerId: EnginePeer.Id, revokePreviousCredentials: Bool) -> Signal { return _internal_getGroupCallStreamCredentials(account: self.account, peerId: peerId, revokePreviousCredentials: revokePreviousCredentials) } + + public func getGroupCallPersistentSettings(callId: Int64) -> Signal { + 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() + } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 8d5710fe52..fb6f78b742 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1387,15 +1387,21 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { let _ = (signal |> deliverOnMainQueue).startStandalone(next: { [weak navigationController] resolvedCallLink in - navigationController?.pushViewController(context.sharedContext.makeJoinSubjectScreen(context: context, mode: JoinSubjectScreenMode.groupCall(JoinSubjectScreenMode.GroupCall( - id: resolvedCallLink.id, - accessHash: resolvedCallLink.accessHash, - slug: link, - inviter: resolvedCallLink.inviter, - members: resolvedCallLink.members, - totalMemberCount: resolvedCallLink.totalMemberCount, - info: resolvedCallLink - )))) + let _ = (context.engine.calls.getGroupCallPersistentSettings(callId: resolvedCallLink.id) + |> deliverOnMainQueue).startStandalone(next: { value in + let value: PresentationGroupCallPersistentSettings = value?.get(PresentationGroupCallPersistentSettings.self) ?? PresentationGroupCallPersistentSettings.default + + navigationController?.pushViewController(context.sharedContext.makeJoinSubjectScreen(context: context, mode: JoinSubjectScreenMode.groupCall(JoinSubjectScreenMode.GroupCall( + id: resolvedCallLink.id, + accessHash: resolvedCallLink.accessHash, + slug: link, + inviter: resolvedCallLink.inviter, + members: resolvedCallLink.members, + totalMemberCount: resolvedCallLink.totalMemberCount, + info: resolvedCallLink, + enableMicrophoneByDefault: value.isMicrophoneEnabledByDefault + )))) + }) }) case let .localization(identifier): strongSelf.presentController(LanguageLinkPreviewController(context: strongSelf.context, identifier: identifier), .window(.root), nil) diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/BUILD b/submodules/TelegramUI/Components/JoinSubjectScreen/BUILD index ae5f1212af..28daf54302 100644 --- a/submodules/TelegramUI/Components/JoinSubjectScreen/BUILD +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/BUILD @@ -29,6 +29,8 @@ swift_library( "//submodules/UndoUI", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/PresentationDataUtils", + "//submodules/TelegramUI/Components/CheckComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift index c8d934620e..d613ad86a6 100644 --- a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift @@ -12,12 +12,15 @@ import BalancedTextComponent import ButtonComponent import BundleIconComponent import Markdown +import Postbox import TelegramCore import AvatarNode import TelegramStringFormatting import AnimatedAvatarSetNode import UndoUI import PresentationDataUtils +import CheckComponent +import PlainButtonComponent private final class JoinSubjectScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -83,6 +86,8 @@ private final class JoinSubjectScreenComponent: Component { private var previewPeersAvatarsNode: AnimatedAvatarSetNode? private var previewPeersAvatarsContext: AnimatedAvatarSetContext? + private var callMicrophoneOption: ComponentView? + private let titleTransformContainer: UIView private let bottomPanelContainer: UIView private let actionButton = ComponentView() @@ -102,6 +107,8 @@ private final class JoinSubjectScreenComponent: Component { private var topOffsetDistance: CGFloat? private var cachedCloseImage: UIImage? + + private var callMicrophoneIsEnabled: Bool = true private var isJoining: Bool = false private var joinDisposable: Disposable? @@ -394,7 +401,7 @@ private final class JoinSubjectScreenComponent: Component { self.environment?.controller()?.dismiss() }) 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() } @@ -414,6 +421,12 @@ private final class JoinSubjectScreenComponent: Component { let sideInset: CGFloat = 16.0 + environment.safeInsets.left if self.component == nil { + switch component.mode { + case .group: + break + case let .groupCall(groupCall): + self.callMicrophoneIsEnabled = groupCall.enableMicrophoneByDefault + } } self.component = component @@ -834,6 +847,85 @@ private final class JoinSubjectScreenComponent: Component { } } + if case .groupCall = component.mode { + let callMicrophoneOption: ComponentView + 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 switch component.mode { case .group: diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 0e2514e3b1..b5fc100555 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -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 { return } @@ -701,7 +701,8 @@ public final class AccountContextImpl: AccountContext { reference: call.reference, beginWithVideo: isVideo, invitePeerIds: [], - endCurrentIfAny: false + endCurrentIfAny: false, + unmuteByDefault: unmuteByDefault ) if case let .alreadyInProgress(currentPeerId) = result { let dataInput: Signal @@ -749,7 +750,8 @@ public final class AccountContextImpl: AccountContext { reference: call.reference, beginWithVideo: isVideo, invitePeerIds: [], - endCurrentIfAny: true + endCurrentIfAny: true, + unmuteByDefault: unmuteByDefault ) })]), on: .root) default: @@ -772,7 +774,8 @@ public final class AccountContextImpl: AccountContext { reference: call.reference, beginWithVideo: isVideo, invitePeerIds: [], - endCurrentIfAny: true + endCurrentIfAny: true, + unmuteByDefault: unmuteByDefault ) })]), on: .root) } diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 35f4f9a047..792cd7860e 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -80,6 +80,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let fetchDisposable = MetaDisposable() private var statusDisposable: Disposable? + private var progressDisposable: Disposable? private let animationCache: AnimationCache? private let animationRenderer: MultiAnimationRenderer? @@ -234,6 +235,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.fetchDisposable.dispose() self.statusDisposable?.dispose() self.translationDisposable.dispose() + self.progressDisposable?.dispose() } private var theme: PresentationTheme? @@ -260,37 +262,10 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { return status == .pinnedMessage } |> deliverOnMainQueue).startStrict(next: { [weak self] isLoading in - guard let strongSelf = self else { + guard let self else { return } - let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) - 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) - } - } + self.updateIsLoading(isLoading: isLoading) }) } @@ -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 { actionTitle = nil } @@ -412,7 +395,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - if let actionTitle = actionTitle { + if let actionTitle { var actionButtonTransition = transition var animateButtonIn = false if self.actionButton.isHidden { @@ -540,6 +523,37 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { 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?) { let message = pinnedMessage.message @@ -667,6 +681,9 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { if let media = media as? TelegramMediaInvoice { titleStrings = [.text(0, NSAttributedString(string: media.title, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))] 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 { case .text: controllerInteraction.sendMessage(button.title) + return case let .url(url): var isConcealed = true if url.hasPrefix("tg://") { isConcealed = false } controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: isConcealed, progress: Promise())) + return case .requestMap: controllerInteraction.shareCurrentLocation() + return case .requestPhone: controllerInteraction.shareAccountContact() + return case .openWebApp: controllerInteraction.requestMessageActionCallback(message.id, nil, true, false) + return case let .callback(requiresPassword, data): controllerInteraction.requestMessageActionCallback(message.id, data, false, requiresPassword) + return case let .switchInline(samePeer, query, peerTypes): var botPeer: Peer? @@ -940,10 +963,13 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { if let botPeer = botPeer, let addressName = botPeer.addressName { controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)", peerTypes) } + return case .payment: controllerInteraction.openCheckoutOrReceipt(message.id, nil) + return case let .urlAuth(url, buttonId): controllerInteraction.requestMessageActionUrlAuth(url, .message(id: message.id, buttonId: buttonId)) + return case .setupPoll: break case let .openUserProfile(peerId): @@ -953,17 +979,47 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { controllerInteraction.openPeer(peer, .info(nil), nil, .default) } }) + return case let .openWebView(url, simple): controllerInteraction.openWebView(button.title, url, simple, .generic) + return case .requestPeer: break case let .copyText(payload): controllerInteraction.copyText(payload) + return } 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() + 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 + } + } } } } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 51494fcd2a..6335a69285 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -396,20 +396,26 @@ func openResolvedUrlImpl( let _ = (signal |> 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 { - context.sharedContext.navigateToCurrentCall() - return - } - - navigationController?.pushViewController(context.sharedContext.makeJoinSubjectScreen(context: context, mode: JoinSubjectScreenMode.groupCall(JoinSubjectScreenMode.GroupCall( - id: resolvedCallLink.id, - accessHash: resolvedCallLink.accessHash, - slug: link, - inviter: resolvedCallLink.inviter, - members: resolvedCallLink.members, - totalMemberCount: resolvedCallLink.totalMemberCount, - info: resolvedCallLink - )))) + let _ = (context.engine.calls.getGroupCallPersistentSettings(callId: resolvedCallLink.id) + |> deliverOnMainQueue).startStandalone(next: { value in + 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 { + context.sharedContext.navigateToCurrentCall() + return + } + + navigationController?.pushViewController(context.sharedContext.makeJoinSubjectScreen(context: context, mode: JoinSubjectScreenMode.groupCall(JoinSubjectScreenMode.GroupCall( + id: resolvedCallLink.id, + accessHash: resolvedCallLink.accessHash, + slug: link, + inviter: resolvedCallLink.inviter, + members: resolvedCallLink.members, + totalMemberCount: resolvedCallLink.totalMemberCount, + info: resolvedCallLink, + enableMicrophoneByDefault: value.isMicrophoneEnabledByDefault + )))) + }) }, error: { _ in var elevatedLayout = true if case .chat = urlContext { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index dfdf148d15..f3487b63d3 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2065,7 +2065,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), beginWithVideo: isVideo, invitePeerIds: peerIds, - endCurrentIfAny: true + endCurrentIfAny: true, + unmuteByDefault: true ) completion?() } diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index b453452d14..085147992c 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -27,6 +27,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var keepChatNavigationStack: Bool public var skipReadHistory: Bool + public var alwaysDisplayTyping: Bool public var crashOnLongQueries: Bool public var chatListPhotos: Bool public var knockoutWallpaper: Bool @@ -72,6 +73,7 @@ public struct ExperimentalUISettings: Codable, Equatable { return ExperimentalUISettings( keepChatNavigationStack: false, skipReadHistory: false, + alwaysDisplayTyping: false, crashOnLongQueries: false, chatListPhotos: false, knockoutWallpaper: false, @@ -118,6 +120,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public init( keepChatNavigationStack: Bool, skipReadHistory: Bool, + alwaysDisplayTyping: Bool, crashOnLongQueries: Bool, chatListPhotos: Bool, knockoutWallpaper: Bool, @@ -161,6 +164,7 @@ public struct ExperimentalUISettings: Codable, Equatable { ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory + self.alwaysDisplayTyping = alwaysDisplayTyping self.crashOnLongQueries = crashOnLongQueries self.chatListPhotos = chatListPhotos self.knockoutWallpaper = knockoutWallpaper @@ -208,6 +212,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.keepChatNavigationStack = (try container.decodeIfPresent(Int32.self, forKey: "keepChatNavigationStack") ?? 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.chatListPhotos = (try container.decodeIfPresent(Int32.self, forKey: "chatListPhotos") ?? 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.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.chatListPhotos ? 1 : 0) as Int32, forKey: "chatListPhotos") try container.encode((self.knockoutWallpaper ? 1 : 0) as Int32, forKey: "knockoutWallpaper")