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 %@";
"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";

View File

@ -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)
}

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 {
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
}

View File

@ -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

View File

@ -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
}

View File

@ -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))

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -178,5 +178,21 @@ public extension TelegramEngine {
public func getGroupCallStreamCredentials(peerId: EnginePeer.Id, revokePreviousCredentials: Bool) -> Signal<GroupCallStreamCredentials, GetGroupCallStreamCredentialsError> {
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
|> 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)

View File

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

View File

@ -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<Empty>?
private let titleTransformContainer: UIView
private let bottomPanelContainer: UIView
private let actionButton = ComponentView<Empty>()
@ -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<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
switch component.mode {
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 {
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<EnginePeer?, NoError>
@ -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)
}

View File

@ -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<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
|> 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 {

View File

@ -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?()
}

View File

@ -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")