mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
d4bc3277a3
@ -21,7 +21,7 @@ internal:
|
||||
script:
|
||||
- export PATH=/opt/homebrew/opt/ruby/bin:$PATH
|
||||
- export PATH=`gem environment gemdir`/bin:$PATH
|
||||
- python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=adhoc --configuration=release_arm64
|
||||
- python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appcenter-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=adhoc --configuration=release_arm64
|
||||
- python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
|
||||
- rm -rf build
|
||||
- python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=appstore --configuration=release_arm64
|
||||
|
14
build-system/appcenter-configuration.json
Executable file
14
build-system/appcenter-configuration.json
Executable file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"bundle_id": "ph.telegra.Telegraph",
|
||||
"api_id": "8",
|
||||
"api_hash": "7245de8e747a0d6fbe11f7cc14fcc0bb",
|
||||
"team_id": "C67CF9S4VU",
|
||||
"app_center_id": "4c816ed0-df83-423c-846b-a0a8467dc7d2",
|
||||
"is_internal_build": "false",
|
||||
"is_appstore_build": "true",
|
||||
"appstore_id": "686449807",
|
||||
"app_specific_url_scheme": "tg",
|
||||
"premium_iap_product_id": "org.telegram.telegramPremium.monthly",
|
||||
"enable_siri": true,
|
||||
"enable_icloud": true
|
||||
}
|
@ -93,6 +93,7 @@ public enum ContactListFilter {
|
||||
public final class ContactMultiselectionControllerParams {
|
||||
public let context: AccountContext
|
||||
public let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
public let title: String?
|
||||
public let mode: ContactMultiselectionControllerMode
|
||||
public let options: Signal<[ContactListAdditionalOption], NoError>
|
||||
public let filters: [ContactListFilter]
|
||||
@ -106,9 +107,10 @@ public final class ContactMultiselectionControllerParams {
|
||||
public let openProfile: ((EnginePeer) -> Void)?
|
||||
public let sendMessage: ((EnginePeer) -> Void)?
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: ContactMultiselectionControllerMode, options: Signal<[ContactListAdditionalOption], NoError> = .single([]), filters: [ContactListFilter] = [.excludeSelf], onlyWriteable: Bool = false, isGroupInvitation: Bool = false, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? = nil, alwaysEnabled: Bool = false, limit: Int32? = nil, reachedLimit: ((Int32) -> Void)? = nil, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil) {
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String? = nil, mode: ContactMultiselectionControllerMode, options: Signal<[ContactListAdditionalOption], NoError> = .single([]), filters: [ContactListFilter] = [.excludeSelf], onlyWriteable: Bool = false, isGroupInvitation: Bool = false, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? = nil, alwaysEnabled: Bool = false, limit: Int32? = nil, reachedLimit: ((Int32) -> Void)? = nil, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil) {
|
||||
self.context = context
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.title = title
|
||||
self.mode = mode
|
||||
self.options = options
|
||||
self.filters = filters
|
||||
|
@ -173,7 +173,7 @@ public protocol PresentationCall: AnyObject {
|
||||
func setCurrentAudioOutput(_ output: AudioSessionOutput)
|
||||
func debugInfo() -> Signal<(String, String), NoError>
|
||||
|
||||
func upgradeToConference(completion: @escaping (PresentationGroupCall) -> Void) -> Disposable
|
||||
func upgradeToConference(invitePeerIds: [EnginePeer.Id], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable
|
||||
|
||||
func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void)
|
||||
}
|
||||
@ -413,6 +413,8 @@ public protocol PresentationGroupCall: AnyObject {
|
||||
var schedulePending: Bool { get }
|
||||
|
||||
var isStream: Bool { get }
|
||||
var isConference: Bool { get }
|
||||
var encryptionKeyValue: Data? { get }
|
||||
|
||||
var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get }
|
||||
|
||||
|
@ -388,7 +388,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set<EnginePeer.Id>, peerRequiresPremiumForMessaging: [EnginePeer.Id: Bool], peersWithStories: [EnginePeer.Id: PeerStoryStats], authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?, topPeers: [EnginePeer], topPeersPresentation: ContactListPresentation.TopPeers, interaction: ContactListNodeInteraction) -> [ContactListNodeEntry] {
|
||||
private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set<EnginePeer.Id>, peerRequiresPremiumForMessaging: [EnginePeer.Id: Bool], peersWithStories: [EnginePeer.Id: PeerStoryStats], authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?, topPeers: [EnginePeer], topPeersPresentation: ContactListPresentation.TopPeers, isPeerEnabled: ((EnginePeer) -> Bool)?, interaction: ContactListNodeInteraction) -> [ContactListNodeEntry] {
|
||||
var entries: [ContactListNodeEntry] = []
|
||||
|
||||
var commonHeader: ListViewItemHeader?
|
||||
@ -778,6 +778,10 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
|
||||
if requiresPremiumForMessaging {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
if let isPeerEnabled, !isPeerEnabled(EnginePeer(peer)) {
|
||||
enabled = false
|
||||
}
|
||||
default:
|
||||
enabled = true
|
||||
}
|
||||
@ -1638,7 +1642,7 @@ public final class ContactListNode: ASDisplayNode {
|
||||
peers.append(.deviceContact(stableId, contact.0))
|
||||
}
|
||||
|
||||
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: peerRequiresPremiumForMessaging, peersWithStories: [:], authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil, topPeers: [], topPeersPresentation: .none, interaction: interaction)
|
||||
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: peerRequiresPremiumForMessaging, peersWithStories: [:], authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil, topPeers: [], topPeersPresentation: .none, isPeerEnabled: isPeerEnabled, interaction: interaction)
|
||||
let previous = previousEntries.swap(entries)
|
||||
return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch))
|
||||
}
|
||||
@ -1840,7 +1844,7 @@ public final class ContactListNode: ASDisplayNode {
|
||||
isEmpty = true
|
||||
}
|
||||
|
||||
let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: view.2, peersWithStories: view.3, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions, topPeers: topPeers.map { $0.peer }, topPeersPresentation: displayTopPeers, interaction: interaction)
|
||||
let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: view.2, peersWithStories: view.3, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions, topPeers: topPeers.map { $0.peer }, topPeersPresentation: displayTopPeers, isPeerEnabled: isPeerEnabled, interaction: interaction)
|
||||
let previous = previousEntries.swap(entries)
|
||||
let previousSelection = previousSelectionState.swap(selectionState)
|
||||
let previousPendingRemovalPeerIds = previousPendingRemovalPeerIds.swap(pendingRemovalPeerIds)
|
||||
|
@ -1448,7 +1448,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
|
||||
|
||||
strongSelf.titleNode.alpha = item.enabled ? 1.0 : 0.4
|
||||
strongSelf.statusNode.textNode.alpha = item.enabled ? 1.0 : 1.0
|
||||
strongSelf.statusNode.textNode.alpha = item.enabled ? 1.0 : 0.4
|
||||
|
||||
strongSelf.statusNode.visibilityRect = strongSelf.visibilityStatus == false ? CGRect.zero : CGRect.infinite
|
||||
let _ = statusApply(TextNodeWithEntities.Arguments(
|
||||
|
@ -1615,7 +1615,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
||||
preferredAction = .saveToCameraRoll
|
||||
actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved
|
||||
case .video:
|
||||
if let message = messages.first, let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.addressName != nil {
|
||||
} else {
|
||||
preferredAction = .saveToCameraRoll
|
||||
}
|
||||
actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved
|
||||
case .file:
|
||||
preferredAction = .saveToCameraRoll
|
||||
|
@ -382,17 +382,17 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
||||
}
|
||||
|
||||
func animateIn(from scrubberTransition: GalleryItemScrubberTransition?, transition: ContainedViewLayoutTransition) {
|
||||
if let scrubberTransition {
|
||||
if let scrubberTransition = scrubberTransition?.scrubber {
|
||||
let fromRect = scrubberTransition.view.convert(scrubberTransition.view.bounds, to: self)
|
||||
|
||||
let targetCloneView = scrubberTransition.makeView()
|
||||
self.addSubview(targetCloneView)
|
||||
targetCloneView.frame = fromRect
|
||||
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: fromRect.size, destinationSize: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height), progress: 0.0), .immediate)
|
||||
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.Scrubber.TransitionState(sourceSize: fromRect.size, destinationSize: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height), progress: 0.0), .immediate)
|
||||
targetCloneView.alpha = 1.0
|
||||
|
||||
transition.updateFrame(view: targetCloneView, frame: CGRect(origin: CGPoint(x: self.scrubberNode.frame.minX, y: self.scrubberNode.frame.maxY - fromRect.height - 3.0), size: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height)))
|
||||
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: fromRect.size, destinationSize: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height), progress: 1.0), transition)
|
||||
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.Scrubber.TransitionState(sourceSize: fromRect.size, destinationSize: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height), progress: 1.0), transition)
|
||||
let scrubberTransitionView = scrubberTransition.view
|
||||
scrubberTransitionView.isHidden = true
|
||||
ContainedViewLayoutTransition.animated(duration: 0.08, curve: .easeInOut).updateAlpha(layer: targetCloneView.layer, alpha: 0.0, completion: { [weak scrubberTransitionView, weak targetCloneView] _ in
|
||||
@ -421,18 +421,18 @@ final class ChatVideoGalleryItemScrubberView: UIView {
|
||||
func animateOut(to scrubberTransition: GalleryItemScrubberTransition?, transition: ContainedViewLayoutTransition) {
|
||||
self.isAnimatedOut = true
|
||||
|
||||
if let scrubberTransition {
|
||||
if let scrubberTransition = scrubberTransition?.scrubber {
|
||||
let toRect = scrubberTransition.view.convert(scrubberTransition.view.bounds, to: self)
|
||||
let scrubberDestinationRect = CGRect(origin: CGPoint(x: toRect.minX, y: toRect.maxY - 3.0), size: CGSize(width: toRect.width, height: 3.0))
|
||||
|
||||
let targetCloneView = scrubberTransition.makeView()
|
||||
self.addSubview(targetCloneView)
|
||||
targetCloneView.frame = CGRect(origin: CGPoint(x: self.scrubberNode.frame.minX, y: self.scrubberNode.frame.maxY - toRect.height), size: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height))
|
||||
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height), destinationSize: toRect.size, progress: 0.0), .immediate)
|
||||
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.Scrubber.TransitionState(sourceSize: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height), destinationSize: toRect.size, progress: 0.0), .immediate)
|
||||
targetCloneView.alpha = 0.0
|
||||
|
||||
transition.updateFrame(view: targetCloneView, frame: toRect)
|
||||
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height), destinationSize: toRect.size, progress: 1.0), transition)
|
||||
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.Scrubber.TransitionState(sourceSize: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height), destinationSize: toRect.size, progress: 1.0), transition)
|
||||
let scrubberTransitionView = scrubberTransition.view
|
||||
scrubberTransitionView.isHidden = true
|
||||
transition.updateAlpha(layer: targetCloneView.layer, alpha: 1.0, completion: { [weak scrubberTransitionView] _ in
|
||||
|
@ -4,6 +4,7 @@ import AccountContext
|
||||
import Display
|
||||
|
||||
public final class GalleryItemScrubberTransition {
|
||||
public final class Scrubber {
|
||||
public struct TransitionState: Equatable {
|
||||
public var sourceSize: CGSize
|
||||
public var destinationSize: CGSize
|
||||
@ -23,13 +24,53 @@ public final class GalleryItemScrubberTransition {
|
||||
public let view: UIView
|
||||
public let makeView: () -> UIView
|
||||
public let updateView: (UIView, TransitionState, ContainedViewLayoutTransition) -> Void
|
||||
public let insertCloneTransitionView: ((UIView) -> Void)?
|
||||
|
||||
public init(view: UIView, makeView: @escaping () -> UIView, updateView: @escaping (UIView, TransitionState, ContainedViewLayoutTransition) -> Void, insertCloneTransitionView: ((UIView) -> Void)?) {
|
||||
public init(view: UIView, makeView: @escaping () -> UIView, updateView: @escaping (UIView, TransitionState, ContainedViewLayoutTransition) -> Void) {
|
||||
self.view = view
|
||||
self.makeView = makeView
|
||||
self.updateView = updateView
|
||||
self.insertCloneTransitionView = insertCloneTransitionView
|
||||
}
|
||||
}
|
||||
|
||||
public final class Content {
|
||||
public struct TransitionState: Equatable {
|
||||
public var sourceSize: CGSize
|
||||
public var destinationSize: CGSize
|
||||
public var destinationCornerRadius: CGFloat
|
||||
public var progress: CGFloat
|
||||
|
||||
public init(
|
||||
sourceSize: CGSize,
|
||||
destinationSize: CGSize,
|
||||
destinationCornerRadius: CGFloat,
|
||||
progress: CGFloat
|
||||
) {
|
||||
self.sourceSize = sourceSize
|
||||
self.destinationSize = destinationSize
|
||||
self.destinationCornerRadius = destinationCornerRadius
|
||||
self.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
public let sourceView: UIView
|
||||
public let sourceRect: CGRect
|
||||
public let makeView: () -> UIView
|
||||
public let updateView: (UIView, TransitionState, ContainedViewLayoutTransition) -> Void
|
||||
|
||||
public init(sourceView: UIView, sourceRect: CGRect, makeView: @escaping () -> UIView, updateView: @escaping (UIView, TransitionState, ContainedViewLayoutTransition) -> Void) {
|
||||
self.sourceView = sourceView
|
||||
self.sourceRect = sourceRect
|
||||
self.makeView = makeView
|
||||
self.updateView = updateView
|
||||
}
|
||||
}
|
||||
|
||||
public let scrubber: Scrubber?
|
||||
public let content: Content?
|
||||
|
||||
public init(scrubber: Scrubber?, content: Content?) {
|
||||
self.scrubber = scrubber
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1374,6 +1374,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
private var activeEdgeRateState: (initialRate: Double, currentRate: Double)?
|
||||
private var activeEdgeRateIndicator: ComponentView<Empty>?
|
||||
|
||||
private var isAnimatingOut: Bool = false
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
@ -1568,7 +1570,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
self.hideControlsDisposable = (shouldHideControlsSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if let strongSelf = self, !strongSelf.isAnimatingOut {
|
||||
strongSelf.updateControlsVisibility(false)
|
||||
}
|
||||
}).strict()
|
||||
@ -1882,17 +1884,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
if let status = status {
|
||||
let shouldStorePlaybacksState: Bool
|
||||
#if DEBUG && false
|
||||
shouldStorePlaybacksState = status.duration >= 10.0
|
||||
#else
|
||||
shouldStorePlaybacksState = status.duration >= 60.0 * 10.0
|
||||
#endif
|
||||
shouldStorePlaybacksState = status.duration >= 20.0
|
||||
|
||||
if shouldStorePlaybacksState {
|
||||
var timestamp: Double?
|
||||
if status.timestamp > 5.0 && status.timestamp < status.duration - 5.0 {
|
||||
timestamp = status.timestamp
|
||||
}
|
||||
item.storeMediaPlaybackState(message.id, timestamp, status.baseRate)
|
||||
} else {
|
||||
item.storeMediaPlaybackState(message.id, nil, status.baseRate)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
@ -2401,6 +2403,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
var isAnimated = false
|
||||
var seek = MediaPlayerSeek.start
|
||||
if let item = self.item {
|
||||
if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? ForwardVideoTimestampAttribute {
|
||||
seek = .timecode(Double(attribute.timestamp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let content = item.content as? NativeVideoContent {
|
||||
isAnimated = content.fileReference.media.isAnimated
|
||||
if let time = item.timecode {
|
||||
@ -2416,14 +2426,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
seek = .timecode(time)
|
||||
}
|
||||
}
|
||||
|
||||
if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? ForwardVideoTimestampAttribute {
|
||||
seek = .timecode(Double(attribute.timestamp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
videoNode.setBaseRate(self.playbackRate ?? 1.0)
|
||||
@ -2458,7 +2460,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
|
||||
override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
|
||||
guard let videoNode = self.videoNode else {
|
||||
guard let videoNode = self.videoNode, let validLayout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -2487,8 +2489,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
self.context.sharedContext.mediaManager.setOverlayVideoNode(nil)
|
||||
} else {
|
||||
if let scrubberView = self.scrubberView {
|
||||
let scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition()
|
||||
|
||||
if let scrubberView = self.scrubberView {
|
||||
scrubberView.animateIn(from: scrubberTransition, transition: .animated(duration: 0.25, curve: .spring))
|
||||
}
|
||||
|
||||
@ -2563,6 +2566,43 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
|
||||
|
||||
if let scrubberTransition, let contentTransition = scrubberTransition.content {
|
||||
let transitionContentView = contentTransition.makeView()
|
||||
let transitionSelfContentView = contentTransition.makeView()
|
||||
|
||||
addToTransitionSurface(transitionContentView)
|
||||
self.view.insertSubview(transitionSelfContentView, at: 0)
|
||||
transitionSelfContentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false)
|
||||
|
||||
if let transitionContentSuperview = transitionContentView.superview {
|
||||
let transitionContentSourceFrame = contentTransition.sourceView.convert(contentTransition.sourceRect, to: transitionContentSuperview)
|
||||
let transitionContentDestinationFrame = self.view.convert(self.view.bounds, to: transitionContentSuperview)
|
||||
|
||||
let transitionContentSelfSourceFrame = contentTransition.sourceView.convert(contentTransition.sourceRect, to: self.view)
|
||||
let transitionContentSelfDestinationFrame = self.view.convert(self.view.bounds, to: self.view)
|
||||
|
||||
let screenCornerRadius: CGFloat = validLayout.layout.deviceMetrics.screenCornerRadius
|
||||
|
||||
transitionContentView.frame = transitionContentSourceFrame
|
||||
contentTransition.updateView(transitionContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSourceFrame.size, destinationSize: transitionContentDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 0.0), .immediate)
|
||||
|
||||
transitionSelfContentView.frame = transitionContentSelfSourceFrame
|
||||
contentTransition.updateView(transitionSelfContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSelfSourceFrame.size, destinationSize: transitionContentSelfDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 0.0), .immediate)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
|
||||
|
||||
transition.updateFrame(view: transitionContentView, frame: transitionContentDestinationFrame, completion: { [weak transitionContentView] _ in
|
||||
transitionContentView?.removeFromSuperview()
|
||||
})
|
||||
contentTransition.updateView(transitionContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSourceFrame.size, destinationSize: transitionContentDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 1.0), transition)
|
||||
|
||||
transition.updateFrame(view: transitionSelfContentView, frame: transitionContentSelfDestinationFrame, completion: { [weak transitionSelfContentView] _ in
|
||||
transitionSelfContentView?.removeFromSuperview()
|
||||
})
|
||||
contentTransition.updateView(transitionSelfContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSelfSourceFrame.size, destinationSize: transitionContentSelfDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 1.0), transition)
|
||||
}
|
||||
}
|
||||
|
||||
if self.item?.fromPlayingVideo ?? false {
|
||||
Queue.mainQueue().after(0.001) {
|
||||
videoNode.canAttachContent = true
|
||||
@ -2586,17 +2626,21 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
|
||||
override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
|
||||
self.isAnimatingOut = true
|
||||
|
||||
guard let videoNode = self.videoNode else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
let scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition()
|
||||
|
||||
if let scrubberView = self.scrubberView {
|
||||
var scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition()
|
||||
var scrubberEffectiveTransition = scrubberTransition
|
||||
if !self.controlsVisibility() {
|
||||
scrubberTransition = nil
|
||||
scrubberEffectiveTransition = nil
|
||||
}
|
||||
scrubberView.animateOut(to: scrubberTransition, transition: .animated(duration: 0.25, curve: .spring))
|
||||
scrubberView.animateOut(to: scrubberEffectiveTransition, transition: .animated(duration: 0.25, curve: .spring))
|
||||
}
|
||||
|
||||
let transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view)
|
||||
@ -2753,6 +2797,47 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
var scrubberContentTransition = scrubberTransition
|
||||
if !self.controlsVisibility() {
|
||||
scrubberContentTransition = nil
|
||||
}
|
||||
if let scrubberContentTransition, let contentTransition = scrubberContentTransition.content {
|
||||
let transitionContentView = contentTransition.makeView()
|
||||
let transitionSelfContentView = contentTransition.makeView()
|
||||
|
||||
addToTransitionSurface(transitionContentView)
|
||||
//self.view.insertSubview(transitionSelfContentView, at: 0)
|
||||
transitionSelfContentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false)
|
||||
|
||||
if let validLayout = self.validLayout, let transitionContentSuperview = transitionContentView.superview {
|
||||
let transitionContentSourceFrame = contentTransition.sourceView.convert(contentTransition.sourceRect, to: transitionContentSuperview)
|
||||
let transitionContentDestinationFrame = self.view.convert(self.view.bounds, to: transitionContentSuperview)
|
||||
|
||||
let transitionContentSelfSourceFrame = contentTransition.sourceView.convert(contentTransition.sourceRect, to: self.view)
|
||||
let transitionContentSelfDestinationFrame = self.view.convert(self.view.bounds, to: self.view)
|
||||
|
||||
let screenCornerRadius: CGFloat = validLayout.layout.deviceMetrics.screenCornerRadius
|
||||
|
||||
transitionContentView.frame = transitionContentDestinationFrame
|
||||
contentTransition.updateView(transitionContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSourceFrame.size, destinationSize: transitionContentDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 1.0), .immediate)
|
||||
|
||||
transitionSelfContentView.frame = transitionContentSelfDestinationFrame
|
||||
contentTransition.updateView(transitionSelfContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSelfSourceFrame.size, destinationSize: transitionContentSelfDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 1.0), .immediate)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
|
||||
|
||||
transition.updateFrame(view: transitionContentView, frame: transitionContentSourceFrame, completion: { [weak transitionContentView] _ in
|
||||
transitionContentView?.removeFromSuperview()
|
||||
})
|
||||
contentTransition.updateView(transitionContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSourceFrame.size, destinationSize: transitionContentDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 0.0), transition)
|
||||
|
||||
transition.updateFrame(view: transitionSelfContentView, frame: transitionContentSelfSourceFrame, completion: { [weak transitionSelfContentView] _ in
|
||||
transitionSelfContentView?.removeFromSuperview()
|
||||
})
|
||||
contentTransition.updateView(transitionSelfContentView, GalleryItemScrubberTransition.Content.TransitionState(sourceSize: transitionContentSelfSourceFrame.size, destinationSize: transitionContentSelfDestinationFrame.size, destinationCornerRadius: screenCornerRadius, progress: 0.0), transition)
|
||||
}
|
||||
}
|
||||
|
||||
if let pictureInPictureNode = self.pictureInPictureNode {
|
||||
let transformedPlaceholderFrame = node.0.view.convert(node.0.view.bounds, to: pictureInPictureNode.view)
|
||||
let pictureInPictureTransform = CATransform3DScale(pictureInPictureNode.layer.transform, transformedPlaceholderFrame.size.width / pictureInPictureNode.layer.bounds.size.width, transformedPlaceholderFrame.size.height / pictureInPictureNode.layer.bounds.size.height, 1.0)
|
||||
|
@ -950,6 +950,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
||||
}
|
||||
if actions {
|
||||
actionNodes.append(contentsOf: [self.actionsBackgroundNode, self.actionButtonNode, self.actionSeparatorNode])
|
||||
if let startAtTimestampNode = self.startAtTimestampNode {
|
||||
actionNodes.append(startAtTimestampNode)
|
||||
}
|
||||
}
|
||||
updateActionNodesAlpha(actionNodes, alpha: hidden ? 0.0 : 1.0)
|
||||
}
|
||||
@ -1326,6 +1329,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
||||
transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0)
|
||||
if let startAtTimestampNode = self.startAtTimestampNode {
|
||||
transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0)
|
||||
}
|
||||
|
||||
let peerIds: [PeerId]
|
||||
var topicIds: [PeerId: Int64] = [:]
|
||||
@ -1623,6 +1629,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
||||
transition.updateAlpha(node: strongSelf.inputFieldNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: strongSelf.actionSeparatorNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: strongSelf.actionsBackgroundNode, alpha: 0.0)
|
||||
if let startAtTimestampNode = strongSelf.startAtTimestampNode {
|
||||
transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0)
|
||||
}
|
||||
strongSelf.transitionToContentNode(ShareLoadingContainerNode(theme: strongSelf.presentationData.theme, forceNativeAppearance: true), fastOut: true)
|
||||
loadingTimestamp = CACurrentMediaTime()
|
||||
if reportReady {
|
||||
@ -1777,6 +1786,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
||||
transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0)
|
||||
if let startAtTimestampNode = self.startAtTimestampNode {
|
||||
transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0)
|
||||
}
|
||||
|
||||
self.transitionToContentNode(ShareProlongedLoadingContainerNode(theme: self.presentationData.theme, strings: self.presentationData.strings, forceNativeAppearance: true, postbox: self.context?.stateManager.postbox, environment: self.environment), fastOut: true)
|
||||
let timestamp = CACurrentMediaTime()
|
||||
@ -1815,6 +1827,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
||||
transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0)
|
||||
if let startAtTimestampNode = self.startAtTimestampNode {
|
||||
transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0)
|
||||
}
|
||||
|
||||
self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: true), fastOut: true)
|
||||
|
||||
|
@ -115,6 +115,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
|
||||
"//submodules/TelegramUI/Components/BackButtonComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/DirectMediaImageCache",
|
||||
"//submodules/FastBlur",
|
||||
],
|
||||
|
@ -4,6 +4,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NSString *randomCallsEmoji();
|
||||
NSData *dataForEmojiRawKey(NSData *data);
|
||||
NSArray<NSString *> *stringForEmojiHashOfData(NSData *data, NSInteger count);
|
||||
|
||||
#endif /* CallsEmoji_h */
|
||||
|
@ -1,6 +1,8 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CallsEmoji/CallsEmoji.h>
|
||||
|
||||
#import <CommonCrypto/CommonCrypto.h>
|
||||
|
||||
static int32_t positionExtractor(uint8_t *bytes, int32_t i, int32_t count) {
|
||||
int offset = i * 8;
|
||||
int64_t num = (((int64_t)bytes[offset] & 0x7F) << 56) | (((int64_t)bytes[offset+1] & 0xFF) << 48) | (((int64_t)bytes[offset+2] & 0xFF) << 40) | (((int64_t)bytes[offset+3] & 0xFF) << 32) | (((int64_t)bytes[offset+4] & 0xFF) << 24) | (((int64_t)bytes[offset+5] & 0xFF) << 16) | (((int64_t)bytes[offset+6] & 0xFF) << 8) | (((int64_t)bytes[offset+7] & 0xFF));
|
||||
@ -16,6 +18,21 @@ NSString *randomCallsEmoji() {
|
||||
return emojis[arc4random() % emojis.count];
|
||||
}
|
||||
|
||||
NSData *dataForEmojiRawKey(NSData *data) {
|
||||
if (!data) {
|
||||
return nil; // Return nil if the input data is nil
|
||||
}
|
||||
|
||||
// Create a buffer to hold the hash
|
||||
uint8_t hash[CC_SHA256_DIGEST_LENGTH];
|
||||
|
||||
// Compute the SHA-256 hash
|
||||
CC_SHA256(data.bytes, (CC_LONG)data.length, hash);
|
||||
|
||||
// Return the hash as NSData
|
||||
return [NSData dataWithBytes:hash length:CC_SHA256_DIGEST_LENGTH];
|
||||
}
|
||||
|
||||
NSArray<NSString *> *stringForEmojiHashOfData(NSData *data, NSInteger count) {
|
||||
if (data.length != 32) {
|
||||
return @[];
|
||||
|
@ -425,15 +425,18 @@ public final class CallController: ViewController {
|
||||
}
|
||||
|
||||
final class AnimateOutToGroupChat {
|
||||
let containerView: UIView
|
||||
let incomingPeerId: EnginePeer.Id
|
||||
let incomingVideoLayer: CALayer?
|
||||
let incomingVideoPlaceholder: VideoSource.Output?
|
||||
|
||||
init(
|
||||
containerView: UIView,
|
||||
incomingPeerId: EnginePeer.Id,
|
||||
incomingVideoLayer: CALayer?,
|
||||
incomingVideoPlaceholder: VideoSource.Output?
|
||||
) {
|
||||
self.containerView = containerView
|
||||
self.incomingPeerId = incomingPeerId
|
||||
self.incomingVideoLayer = incomingVideoLayer
|
||||
self.incomingVideoPlaceholder = incomingVideoPlaceholder
|
||||
@ -487,6 +490,7 @@ public final class CallController: ViewController {
|
||||
let controller = self.call.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(
|
||||
context: self.call.context,
|
||||
updatedPresentationData: (initial: presentationData, signal: .single(presentationData)),
|
||||
title: "Invite Members",
|
||||
mode: .peerSelection(searchChatList: true, searchGroups: false, searchChannels: false),
|
||||
isPeerEnabled: { peer in
|
||||
guard case let .user(user) = peer else {
|
||||
@ -516,21 +520,19 @@ public final class CallController: ViewController {
|
||||
return
|
||||
}
|
||||
|
||||
controller?.displayProgress = true
|
||||
let call = self.call
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
controller?.dismiss()
|
||||
|
||||
let _ = self.call.upgradeToConference(completion: { [weak call] _ in
|
||||
guard let call else {
|
||||
return
|
||||
}
|
||||
|
||||
for peerId in peerIds {
|
||||
if case let .peer(peerId) = peerId {
|
||||
let _ = (call as? PresentationCallImpl)?.requestAddToConference(peerId: peerId)
|
||||
let invitePeerIds = peerIds.compactMap { item -> EnginePeer.Id? in
|
||||
if case let .peer(peerId) = item {
|
||||
return peerId
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.call.upgradeToConference(invitePeerIds: invitePeerIds, completion: { _ in
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -689,6 +689,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
||||
|
||||
let takenIncomingVideoLayer = self.callScreen.takeIncomingVideoLayer()
|
||||
return CallController.AnimateOutToGroupChat(
|
||||
containerView: self.containerView,
|
||||
incomingPeerId: self.call.peerId,
|
||||
incomingVideoLayer: takenIncomingVideoLayer?.0,
|
||||
incomingVideoPlaceholder: takenIncomingVideoLayer?.1
|
||||
|
@ -15,6 +15,154 @@ import AccountContext
|
||||
import DeviceProximity
|
||||
import PhoneNumberFormat
|
||||
|
||||
final class SharedCallAudioContext {
|
||||
let audioDevice: OngoingCallContext.AudioDevice?
|
||||
let callKitIntegration: CallKitIntegration?
|
||||
|
||||
private var audioSessionDisposable: Disposable?
|
||||
private var audioSessionShouldBeActiveDisposable: Disposable?
|
||||
private var isAudioSessionActiveDisposable: Disposable?
|
||||
|
||||
private(set) var audioSessionControl: ManagedAudioSessionControl?
|
||||
|
||||
private let isAudioSessionActivePromise = Promise<Bool>(false)
|
||||
private var isAudioSessionActive: Signal<Bool, NoError> {
|
||||
return self.isAudioSessionActivePromise.get()
|
||||
}
|
||||
|
||||
private let audioOutputStatePromise = Promise<([AudioSessionOutput], AudioSessionOutput?)>(([], nil))
|
||||
private var audioOutputStateValue: ([AudioSessionOutput], AudioSessionOutput?) = ([], nil)
|
||||
private var currentAudioOutputValue: AudioSessionOutput = .builtin
|
||||
private var didSetCurrentAudioOutputValue: Bool = false
|
||||
var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> {
|
||||
return self.audioOutputStatePromise.get()
|
||||
}
|
||||
|
||||
private let audioSessionShouldBeActive = Promise<Bool>(true)
|
||||
|
||||
init(audioSession: ManagedAudioSession, callKitIntegration: CallKitIntegration?) {
|
||||
self.callKitIntegration = callKitIntegration
|
||||
self.audioDevice = OngoingCallContext.AudioDevice.create(enableSystemMute: false)
|
||||
|
||||
var didReceiveAudioOutputs = false
|
||||
self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] control in
|
||||
Queue.mainQueue().async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let previousControl = self.audioSessionControl
|
||||
self.audioSessionControl = control
|
||||
|
||||
if previousControl == nil, let audioSessionControl = self.audioSessionControl {
|
||||
if let callKitIntegration = self.callKitIntegration {
|
||||
if self.didSetCurrentAudioOutputValue {
|
||||
callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue))
|
||||
}
|
||||
} else {
|
||||
audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue))
|
||||
audioSessionControl.setup(synchronous: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, deactivate: { [weak self] _ in
|
||||
return Signal { subscriber in
|
||||
Queue.mainQueue().async {
|
||||
if let self {
|
||||
self.isAudioSessionActivePromise.set(.single(false))
|
||||
self.audioSessionControl = nil
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}, availableOutputsChanged: { [weak self] availableOutputs, currentOutput in
|
||||
Queue.mainQueue().async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.audioOutputStateValue = (availableOutputs, currentOutput)
|
||||
if let currentOutput = currentOutput {
|
||||
self.currentAudioOutputValue = currentOutput
|
||||
self.didSetCurrentAudioOutputValue = true
|
||||
}
|
||||
|
||||
var signal: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> = .single((availableOutputs, currentOutput))
|
||||
if !didReceiveAudioOutputs {
|
||||
didReceiveAudioOutputs = true
|
||||
if currentOutput == .speaker {
|
||||
signal = .single((availableOutputs, .builtin))
|
||||
|> then(
|
||||
signal
|
||||
|> delay(1.0, queue: Queue.mainQueue())
|
||||
)
|
||||
}
|
||||
}
|
||||
self.audioOutputStatePromise.set(signal)
|
||||
}
|
||||
})
|
||||
|
||||
self.audioSessionShouldBeActive.set(.single(true))
|
||||
self.audioSessionShouldBeActiveDisposable = (self.audioSessionShouldBeActive.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
if let audioSessionControl = self.audioSessionControl {
|
||||
let audioSessionActive: Signal<Bool, NoError>
|
||||
if let callKitIntegration = self.callKitIntegration {
|
||||
audioSessionActive = callKitIntegration.audioSessionActive
|
||||
} else {
|
||||
audioSessionControl.activate({ _ in })
|
||||
audioSessionActive = .single(true)
|
||||
}
|
||||
self.isAudioSessionActivePromise.set(audioSessionActive)
|
||||
} else {
|
||||
self.isAudioSessionActivePromise.set(.single(false))
|
||||
}
|
||||
} else {
|
||||
self.isAudioSessionActivePromise.set(.single(false))
|
||||
}
|
||||
})
|
||||
|
||||
self.isAudioSessionActiveDisposable = (self.isAudioSessionActive
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.audioSessionDisposable?.dispose()
|
||||
self.audioSessionShouldBeActiveDisposable?.dispose()
|
||||
self.isAudioSessionActiveDisposable?.dispose()
|
||||
}
|
||||
|
||||
func setCurrentAudioOutput(_ output: AudioSessionOutput) {
|
||||
guard self.currentAudioOutputValue != output else {
|
||||
return
|
||||
}
|
||||
self.currentAudioOutputValue = output
|
||||
self.didSetCurrentAudioOutputValue = true
|
||||
|
||||
self.audioOutputStatePromise.set(.single((self.audioOutputStateValue.0, output))
|
||||
|> then(
|
||||
.single(self.audioOutputStateValue)
|
||||
|> delay(1.0, queue: Queue.mainQueue())
|
||||
))
|
||||
|
||||
if let audioSessionControl = self.audioSessionControl {
|
||||
if let callKitIntegration = self.callKitIntegration {
|
||||
callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue))
|
||||
} else {
|
||||
audioSessionControl.setOutputMode(.custom(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class PresentationCallImpl: PresentationCall {
|
||||
public let context: AccountContext
|
||||
private let audioSession: ManagedAudioSession
|
||||
@ -43,6 +191,8 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
private let currentNetworkType: NetworkType
|
||||
private let updatedNetworkType: Signal<NetworkType, NoError>
|
||||
|
||||
private var sharedAudioContext: SharedCallAudioContext?
|
||||
|
||||
private var sessionState: CallSession?
|
||||
private var callContextState: OngoingCallContextState?
|
||||
private var ongoingContext: OngoingCallContext?
|
||||
@ -50,7 +200,6 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
private var ongoingContextIsFailedDisposable: Disposable?
|
||||
private var ongoingContextIsDroppedDisposable: Disposable?
|
||||
private var didDropCall = false
|
||||
private var sharedAudioDevice: OngoingCallContext.AudioDevice?
|
||||
private var requestedVideoAspect: Float?
|
||||
private var reception: Int32?
|
||||
private var receptionDisposable: Disposable?
|
||||
@ -90,6 +239,10 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
private var currentAudioOutputValue: AudioSessionOutput = .builtin
|
||||
private var didSetCurrentAudioOutputValue: Bool = false
|
||||
public var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> {
|
||||
if let sharedAudioContext = self.sharedAudioContext {
|
||||
return sharedAudioContext.audioOutputState
|
||||
}
|
||||
|
||||
return self.audioOutputStatePromise.get()
|
||||
}
|
||||
|
||||
@ -157,6 +310,8 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
return self.conferenceStatePromise.get()
|
||||
}
|
||||
|
||||
public private(set) var pendingInviteToConferencePeerIds: [EnginePeer.Id] = []
|
||||
|
||||
private var localVideoEndpointId: String?
|
||||
private var remoteVideoEndpointId: String?
|
||||
|
||||
@ -242,6 +397,14 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
})
|
||||
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_call_device"] {
|
||||
self.sharedAudioContext = nil
|
||||
} else {
|
||||
self.sharedAudioContext = SharedCallAudioContext(audioSession: audioSession, callKitIntegration: callKitIntegration)
|
||||
}
|
||||
|
||||
if let _ = self.sharedAudioContext {
|
||||
} else {
|
||||
self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] control in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
@ -315,18 +478,13 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
})
|
||||
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_call_device"] {
|
||||
self.sharedAudioDevice = nil
|
||||
} else {
|
||||
self.sharedAudioDevice = OngoingCallContext.AudioDevice.create(enableSystemMute: false)
|
||||
}
|
||||
|
||||
self.audioSessionActiveDisposable = (self.audioSessionActive.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateIsAudioSessionActive(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let screencastCapturer = OngoingCallVideoCapturer(isCustom: true)
|
||||
self.screencastCapturer = screencastCapturer
|
||||
@ -414,9 +572,9 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
|
||||
let reception = self.reception
|
||||
|
||||
if previousControl != nil && audioSessionControl == nil {
|
||||
/*if previousControl != nil && audioSessionControl == nil {
|
||||
print("updateSessionState \(sessionState.state) \(audioSessionControl != nil)")
|
||||
}
|
||||
}*/
|
||||
|
||||
var presentationState: PresentationCallState?
|
||||
|
||||
@ -433,6 +591,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
}
|
||||
|
||||
if self.sharedAudioContext == nil {
|
||||
if let audioSessionControl = audioSessionControl, previous == nil || previousControl == nil {
|
||||
if let callKitIntegration = self.callKitIntegration {
|
||||
if self.didSetCurrentAudioOutputValue {
|
||||
@ -443,6 +602,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
audioSessionControl.setup(synchronous: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mappedVideoState: PresentationCallState.VideoState
|
||||
let mappedRemoteVideoState: PresentationCallState.RemoteVideoState
|
||||
@ -637,11 +797,13 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
encryptionKey: (key, 1),
|
||||
conferenceFromCallId: conferenceFromCallId,
|
||||
isConference: true,
|
||||
sharedAudioDevice: self.sharedAudioDevice
|
||||
sharedAudioContext: self.sharedAudioContext
|
||||
)
|
||||
self.conferenceCallImpl = conferenceCall
|
||||
conferenceCall.upgradedConferenceCall = self
|
||||
|
||||
conferenceCall.setInvitedPeers(self.pendingInviteToConferencePeerIds)
|
||||
|
||||
conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted)
|
||||
if let videoCapturer = self.videoCapturer {
|
||||
conferenceCall.requestVideo(capturer: videoCapturer)
|
||||
@ -746,12 +908,19 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.callKitIntegration?.reportOutgoingCallConnected(uuid: sessionState.id, at: Date())
|
||||
}
|
||||
} else {
|
||||
if let _ = audioSessionControl, !wasActive || previousControl == nil {
|
||||
if (self.sharedAudioContext != nil || audioSessionControl != nil), !wasActive || (self.sharedAudioContext == nil && previousControl == nil) {
|
||||
let logName = "\(id.id)_\(id.accessHash)"
|
||||
|
||||
let updatedConnections = connections
|
||||
|
||||
let ongoingContext = OngoingCallContext(account: self.context.account, callSessionManager: self.callSessionManager, callId: id, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: updatedConnections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowP2P: allowsP2P, enableTCP: self.enableTCP, enableStunMarking: self.enableStunMarking, audioSessionActive: self.audioSessionActive.get(), logName: logName, preferredVideoCodec: self.preferredVideoCodec, audioDevice: self.sharedAudioDevice)
|
||||
let contextAudioSessionActive: Signal<Bool, NoError>
|
||||
if self.sharedAudioContext != nil {
|
||||
contextAudioSessionActive = .single(true)
|
||||
} else {
|
||||
contextAudioSessionActive = self.audioSessionActive.get()
|
||||
}
|
||||
|
||||
let ongoingContext = OngoingCallContext(account: self.context.account, callSessionManager: self.callSessionManager, callId: id, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: updatedConnections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowP2P: allowsP2P, enableTCP: self.enableTCP, enableStunMarking: self.enableStunMarking, audioSessionActive: contextAudioSessionActive, logName: logName, preferredVideoCodec: self.preferredVideoCodec, audioDevice: self.sharedAudioContext?.audioDevice)
|
||||
self.ongoingContext = ongoingContext
|
||||
ongoingContext.setIsMuted(self.isMutedValue)
|
||||
if let requestedVideoAspect = self.requestedVideoAspect {
|
||||
@ -957,7 +1126,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
if tone != self.currentTone {
|
||||
self.currentTone = tone
|
||||
self.sharedAudioDevice?.setTone(tone: tone.flatMap(presentationCallToneData).flatMap { data in
|
||||
self.sharedAudioContext?.audioDevice?.setTone(tone: tone.flatMap(presentationCallToneData).flatMap { data in
|
||||
return OngoingCallContext.Tone(samples: data, sampleRate: 48000, loopCount: tone?.loopCount ?? 1000000)
|
||||
})
|
||||
}
|
||||
@ -967,7 +1136,6 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
if self.isAudioSessionActive != value {
|
||||
self.isAudioSessionActive = value
|
||||
}
|
||||
self.sharedAudioDevice?.setIsAudioSessionActive(value)
|
||||
}
|
||||
|
||||
public func answer() {
|
||||
@ -1143,13 +1311,29 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.videoCapturer?.setIsVideoEnabled(!isPaused)
|
||||
}
|
||||
|
||||
public func upgradeToConference(completion: @escaping (PresentationGroupCall) -> Void) -> Disposable {
|
||||
public func upgradeToConference(invitePeerIds: [EnginePeer.Id], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable {
|
||||
if let conferenceCall = self.conferenceCall {
|
||||
completion(conferenceCall)
|
||||
|
||||
for peerId in invitePeerIds {
|
||||
let _ = self.requestAddToConference(peerId: peerId)
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let index = self.upgradedToConferenceCompletions.add(completion)
|
||||
self.pendingInviteToConferencePeerIds = invitePeerIds
|
||||
let index = self.upgradedToConferenceCompletions.add({ [weak self] call in
|
||||
completion(call)
|
||||
|
||||
if let self {
|
||||
for peerId in invitePeerIds {
|
||||
let _ = self.requestAddToConference(peerId: peerId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.conferenceStateValue = .preparing
|
||||
self.callSessionManager.createConferenceIfNecessary(internalId: self.internalId)
|
||||
|
||||
return ActionDisposable { [weak self] in
|
||||
@ -1162,7 +1346,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
}
|
||||
|
||||
public func requestAddToConference(peerId: EnginePeer.Id) -> Disposable {
|
||||
private func requestAddToConference(peerId: EnginePeer.Id) -> Disposable {
|
||||
var conferenceCall: (conference: GroupCallReference, encryptionKey: Data)?
|
||||
if let sessionState = self.sessionState {
|
||||
switch sessionState.state {
|
||||
@ -1189,6 +1373,11 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
|
||||
public func setCurrentAudioOutput(_ output: AudioSessionOutput) {
|
||||
if let sharedAudioContext = self.sharedAudioContext {
|
||||
sharedAudioContext.setCurrentAudioOutput(output)
|
||||
return
|
||||
}
|
||||
|
||||
guard self.currentAudioOutputValue != output else {
|
||||
return
|
||||
}
|
||||
|
@ -706,7 +706,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
encryptionKey: nil,
|
||||
conferenceFromCallId: nil,
|
||||
isConference: false,
|
||||
sharedAudioDevice: nil
|
||||
sharedAudioContext: nil
|
||||
)
|
||||
call.schedule(timestamp: timestamp)
|
||||
|
||||
@ -749,7 +749,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
encryptionKey: nil,
|
||||
conferenceFromCallId: nil,
|
||||
isConference: false,
|
||||
sharedAudioDevice: nil
|
||||
sharedAudioContext: nil
|
||||
)
|
||||
strongSelf.updateCurrentGroupCall(call)
|
||||
strongSelf.currentGroupCallPromise.set(.single(call))
|
||||
@ -937,7 +937,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
encryptionKey: nil,
|
||||
conferenceFromCallId: nil,
|
||||
isConference: false,
|
||||
sharedAudioDevice: nil
|
||||
sharedAudioContext: nil
|
||||
)
|
||||
strongSelf.updateCurrentGroupCall(call)
|
||||
strongSelf.currentGroupCallPromise.set(.single(call))
|
||||
|
@ -16,6 +16,7 @@ import AccountContext
|
||||
import DeviceProximity
|
||||
import UndoUI
|
||||
import TemporaryCachedPeerDataManager
|
||||
import CallsEmoji
|
||||
|
||||
private extension GroupCallParticipantsContext.Participant {
|
||||
var allSsrcs: Set<UInt32> {
|
||||
@ -818,6 +819,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
private var audioOutputStateValue: ([AudioSessionOutput], AudioSessionOutput?) = ([], nil)
|
||||
private var currentSelectedAudioOutputValue: AudioSessionOutput = .builtin
|
||||
public var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> {
|
||||
if let sharedAudioContext = self.sharedAudioContext {
|
||||
return sharedAudioContext.audioOutputState
|
||||
}
|
||||
return self.audioOutputStatePromise.get()
|
||||
}
|
||||
|
||||
@ -995,10 +999,17 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|
||||
public let isStream: Bool
|
||||
private let encryptionKey: (key: Data, fingerprint: Int64)?
|
||||
private let sharedAudioDevice: OngoingCallContext.AudioDevice?
|
||||
private let sharedAudioContext: SharedCallAudioContext?
|
||||
|
||||
private let conferenceFromCallId: CallId?
|
||||
private let isConference: Bool
|
||||
public let isConference: Bool
|
||||
public var encryptionKeyValue: Data? {
|
||||
if let key = self.encryptionKey?.key {
|
||||
return dataForEmojiRawKey(key)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var internal_isRemoteConnected = Promise<Bool>()
|
||||
private var internal_isRemoteConnectedDisposable: Disposable?
|
||||
@ -1024,7 +1035,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
encryptionKey: (key: Data, fingerprint: Int64)?,
|
||||
conferenceFromCallId: CallId?,
|
||||
isConference: Bool,
|
||||
sharedAudioDevice: OngoingCallContext.AudioDevice?
|
||||
sharedAudioContext: SharedCallAudioContext?
|
||||
) {
|
||||
self.account = accountContext.account
|
||||
self.accountContext = accountContext
|
||||
@ -1053,9 +1064,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.conferenceFromCallId = conferenceFromCallId
|
||||
self.isConference = isConference
|
||||
self.encryptionKey = encryptionKey
|
||||
self.sharedAudioDevice = sharedAudioDevice
|
||||
self.sharedAudioContext = sharedAudioContext
|
||||
|
||||
if self.sharedAudioDevice == nil && !accountContext.sharedContext.immediateExperimentalUISettings.liveStreamV2 {
|
||||
if self.sharedAudioContext == nil && !accountContext.sharedContext.immediateExperimentalUISettings.liveStreamV2 {
|
||||
var didReceiveAudioOutputs = false
|
||||
|
||||
if !audioSession.getIsHeadsetPluggedIn() {
|
||||
@ -1139,6 +1150,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
})
|
||||
|
||||
if self.sharedAudioContext == nil {
|
||||
self.audioSessionActiveDisposable = (self.audioSessionActive.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
@ -1154,6 +1166,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
strongSelf.updateAudioOutputs(availableOutputs: availableOutputs, currentOutput: currentOutput)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.groupCallParticipantUpdatesDisposable = (self.account.stateManager.groupCallParticipantUpdates
|
||||
|> deliverOnMainQueue).start(next: { [weak self] updates in
|
||||
@ -1768,7 +1781,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.internalState = internalState
|
||||
self.internalStatePromise.set(.single(internalState))
|
||||
|
||||
if !self.accountContext.sharedContext.immediateExperimentalUISettings.liveStreamV2, let audioSessionControl = audioSessionControl, previousControl == nil {
|
||||
if self.sharedAudioContext == nil, !self.accountContext.sharedContext.immediateExperimentalUISettings.liveStreamV2, let audioSessionControl = audioSessionControl, previousControl == nil {
|
||||
if self.isStream {
|
||||
audioSessionControl.setOutputMode(.system)
|
||||
} else {
|
||||
@ -1847,7 +1860,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
var encryptionKey: Data?
|
||||
encryptionKey = self.encryptionKey?.key
|
||||
|
||||
genericCallContext = .call(OngoingGroupCallContext(audioSessionActive: self.audioSessionActive.get(), video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in
|
||||
let contextAudioSessionActive: Signal<Bool, NoError>
|
||||
if self.sharedAudioContext != nil {
|
||||
contextAudioSessionActive = .single(true)
|
||||
} else {
|
||||
contextAudioSessionActive = self.audioSessionActive.get()
|
||||
}
|
||||
|
||||
genericCallContext = .call(OngoingGroupCallContext(audioSessionActive: contextAudioSessionActive, video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in
|
||||
let disposable = MetaDisposable()
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
@ -1872,7 +1892,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
strongSelf.onMutedSpeechActivityDetected?(value)
|
||||
}
|
||||
}, encryptionKey: encryptionKey, isConference: self.isConference, isStream: self.isStream, sharedAudioDevice: self.sharedAudioDevice))
|
||||
}, encryptionKey: encryptionKey, isConference: self.isConference, isStream: self.isStream, sharedAudioDevice: self.sharedAudioContext?.audioDevice))
|
||||
}
|
||||
|
||||
self.genericCallContext = genericCallContext
|
||||
@ -3349,7 +3369,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
|
||||
public func setCurrentAudioOutput(_ output: AudioSessionOutput) {
|
||||
if self.sharedAudioDevice != nil {
|
||||
if let sharedAudioContext = self.sharedAudioContext {
|
||||
sharedAudioContext.setCurrentAudioOutput(output)
|
||||
return
|
||||
}
|
||||
guard self.currentSelectedAudioOutputValue != output else {
|
||||
@ -3567,6 +3588,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
return true
|
||||
}
|
||||
|
||||
func setInvitedPeers(_ peerIds: [PeerId]) {
|
||||
self.invitedPeersValue = peerIds
|
||||
}
|
||||
|
||||
public func removedPeer(_ peerId: PeerId) {
|
||||
var updatedInvitedPeers = self.invitedPeersValue
|
||||
updatedInvitedPeers.removeAll(where: { $0 == peerId})
|
||||
|
@ -128,6 +128,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
|
||||
let call: VideoChatCall
|
||||
let participants: Participants?
|
||||
let invitedPeers: [EnginePeer]
|
||||
let speakingParticipants: Set<EnginePeer.Id>
|
||||
let expandedVideoState: ExpandedVideoState?
|
||||
let maxVideoQuality: Int
|
||||
@ -147,6 +148,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
init(
|
||||
call: VideoChatCall,
|
||||
participants: Participants?,
|
||||
invitedPeers: [EnginePeer],
|
||||
speakingParticipants: Set<EnginePeer.Id>,
|
||||
expandedVideoState: ExpandedVideoState?,
|
||||
maxVideoQuality: Int,
|
||||
@ -165,6 +167,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
) {
|
||||
self.call = call
|
||||
self.participants = participants
|
||||
self.invitedPeers = invitedPeers
|
||||
self.speakingParticipants = speakingParticipants
|
||||
self.expandedVideoState = expandedVideoState
|
||||
self.maxVideoQuality = maxVideoQuality
|
||||
@ -189,6 +192,9 @@ final class VideoChatParticipantsComponent: Component {
|
||||
if lhs.participants != rhs.participants {
|
||||
return false
|
||||
}
|
||||
if lhs.invitedPeers != rhs.invitedPeers {
|
||||
return false
|
||||
}
|
||||
if lhs.speakingParticipants != rhs.speakingParticipants {
|
||||
return false
|
||||
}
|
||||
@ -1183,25 +1189,14 @@ final class VideoChatParticipantsComponent: Component {
|
||||
let clippedVisibleListItemRange = itemLayout.visibleListItemRange(for: clippedScrollViewBounds)
|
||||
if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex {
|
||||
for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex {
|
||||
let participant = self.listParticipants[i]
|
||||
validListItemIds.append(participant.peer.id)
|
||||
|
||||
if i >= clippedVisibleListItemRange.minIndex && i <= clippedVisibleListItemRange.maxIndex {
|
||||
visibleParticipants.append(participant.peer.id)
|
||||
}
|
||||
|
||||
var itemTransition = transition
|
||||
let itemView: ListItem
|
||||
if let current = self.listItemViews[participant.peer.id] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
itemView = ListItem()
|
||||
self.listItemViews[participant.peer.id] = itemView
|
||||
}
|
||||
|
||||
let itemFrame = itemLayout.listItemFrame(at: i)
|
||||
|
||||
let participantPeerId: EnginePeer.Id
|
||||
let peerItemComponent: PeerListItemComponent
|
||||
if i < self.listParticipants.count {
|
||||
let participant = self.listParticipants[i]
|
||||
participantPeerId = participant.peer.id
|
||||
|
||||
let subtitle: PeerListItemComponent.Subtitle
|
||||
if participant.peer.id == component.call.accountContext.account.peerId {
|
||||
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_You, color: .accent)
|
||||
@ -1224,9 +1219,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
theme: component.theme
|
||||
))
|
||||
|
||||
let _ = itemView.view.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(PeerListItemComponent(
|
||||
peerItemComponent = PeerListItemComponent(
|
||||
context: component.call.accountContext,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
@ -1263,7 +1256,63 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
component.openParticipantContextMenu(peer.id, sourceView, gesture)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
let invitedPeer = component.invitedPeers[i - self.listParticipants.count]
|
||||
participantPeerId = invitedPeer.id
|
||||
|
||||
let subtitle: PeerListItemComponent.Subtitle
|
||||
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusInvited, color: .neutral)
|
||||
|
||||
peerItemComponent = PeerListItemComponent(
|
||||
context: component.call.accountContext,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
style: .generic,
|
||||
sideInset: 0.0,
|
||||
title: invitedPeer.displayTitle(strings: component.strings, displayOrder: .firstLast),
|
||||
avatarComponent: AnyComponent(VideoChatParticipantAvatarComponent(
|
||||
call: component.call,
|
||||
peer: invitedPeer,
|
||||
myPeerId: component.participants?.myPeerId ?? component.call.accountContext.account.peerId,
|
||||
isSpeaking: false,
|
||||
theme: component.theme
|
||||
)),
|
||||
peer: invitedPeer,
|
||||
subtitle: subtitle,
|
||||
subtitleAccessory: .none,
|
||||
presence: nil,
|
||||
rightAccessoryComponent: nil,
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
extractedTheme: PeerListItemComponent.ExtractedTheme(
|
||||
inset: 2.0,
|
||||
background: UIColor(white: 0.1, alpha: 1.0)
|
||||
),
|
||||
action: nil,
|
||||
contextAction: nil
|
||||
)
|
||||
}
|
||||
|
||||
validListItemIds.append(participantPeerId)
|
||||
|
||||
if i >= clippedVisibleListItemRange.minIndex && i <= clippedVisibleListItemRange.maxIndex {
|
||||
visibleParticipants.append(participantPeerId)
|
||||
}
|
||||
|
||||
var itemTransition = transition
|
||||
let itemView: ListItem
|
||||
if let current = self.listItemViews[participantPeerId] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
itemView = ListItem()
|
||||
self.listItemViews[participantPeerId] = itemView
|
||||
}
|
||||
|
||||
let _ = itemView.view.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(peerItemComponent),
|
||||
environment: {},
|
||||
containerSize: itemFrame.size
|
||||
)
|
||||
@ -1363,12 +1412,6 @@ final class VideoChatParticipantsComponent: Component {
|
||||
isPresentation: participant.isPresentation
|
||||
))
|
||||
}
|
||||
/*for participant in self.listParticipants {
|
||||
thumbnailParticipants.append(VideoChatExpandedParticipantThumbnailsComponent.Participant(
|
||||
participant: participant,
|
||||
isPresentation: false
|
||||
))
|
||||
}*/
|
||||
|
||||
let expandedControlsAlpha: CGFloat = (expandedVideoState.isUIHidden || self.isPinchToZoomActive) ? 0.0 : 1.0
|
||||
let expandedThumbnailsAlpha: CGFloat = expandedControlsAlpha
|
||||
@ -1770,7 +1813,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
expandedInsets: component.expandedInsets,
|
||||
safeInsets: component.safeInsets,
|
||||
gridItemCount: gridParticipants.count,
|
||||
listItemCount: listParticipants.count,
|
||||
listItemCount: listParticipants.count + component.invitedPeers.count,
|
||||
listItemHeight: measureListItemSize.height,
|
||||
listTrailingItemHeight: inviteListItemSize.height
|
||||
)
|
||||
|
@ -241,6 +241,9 @@ final class VideoChatScreenComponent: Component {
|
||||
var members: PresentationGroupCallMembers?
|
||||
var membersDisposable: Disposable?
|
||||
|
||||
var invitedPeers: [EnginePeer] = []
|
||||
var invitedPeersDisposable: Disposable?
|
||||
|
||||
var speakingParticipantPeers: [EnginePeer] = []
|
||||
var visibleParticipants: Set<EnginePeer.Id> = Set()
|
||||
|
||||
@ -285,6 +288,7 @@ final class VideoChatScreenComponent: Component {
|
||||
deinit {
|
||||
self.stateDisposable?.dispose()
|
||||
self.membersDisposable?.dispose()
|
||||
self.invitedPeersDisposable?.dispose()
|
||||
self.applicationStateDisposable?.dispose()
|
||||
self.reconnectedAsEventsDisposable?.dispose()
|
||||
self.memberEventsDisposable?.dispose()
|
||||
@ -305,12 +309,17 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
|
||||
func animateIn(sourceCallController: CallController) {
|
||||
let sourceCallControllerView = sourceCallController.view
|
||||
var isAnimationFinished = false
|
||||
let animateOutData = sourceCallController.animateOutToGroupChat(completion: { [weak sourceCallControllerView] in
|
||||
var sourceCallControllerAnimatedOut: (() -> Void)?
|
||||
let animateOutData = sourceCallController.animateOutToGroupChat(completion: {
|
||||
isAnimationFinished = true
|
||||
sourceCallControllerView?.removeFromSuperview()
|
||||
sourceCallControllerAnimatedOut?()
|
||||
})
|
||||
let sourceCallControllerView = animateOutData?.containerView
|
||||
sourceCallControllerView?.isUserInteractionEnabled = false
|
||||
sourceCallControllerAnimatedOut = { [weak sourceCallControllerView] in
|
||||
sourceCallControllerView?.removeFromSuperview()
|
||||
}
|
||||
|
||||
var expandedPeer: (id: EnginePeer.Id, isPresentation: Bool)?
|
||||
if let animateOutData, animateOutData.incomingVideoLayer != nil {
|
||||
@ -327,11 +336,11 @@ final class VideoChatScreenComponent: Component {
|
||||
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
if !isAnimationFinished {
|
||||
if !isAnimationFinished, let sourceCallControllerView {
|
||||
if let participantsView = self.participants.view {
|
||||
self.containerView.insertSubview(sourceCallController.view, belowSubview: participantsView)
|
||||
self.containerView.insertSubview(sourceCallControllerView, belowSubview: participantsView)
|
||||
} else {
|
||||
self.containerView.addSubview(sourceCallController.view)
|
||||
self.containerView.addSubview(sourceCallControllerView)
|
||||
}
|
||||
}
|
||||
|
||||
@ -380,7 +389,15 @@ final class VideoChatScreenComponent: Component {
|
||||
self.state?.updated(transition: .spring(duration: 0.5))
|
||||
}
|
||||
|
||||
@objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
guard let result = super.hitTest(point, with: event) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer is UITapGestureRecognizer {
|
||||
if otherGestureRecognizer is UIPanGestureRecognizer {
|
||||
return true
|
||||
@ -409,7 +426,6 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began, .changed:
|
||||
@ -952,6 +968,103 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
static func groupCallStateForConferenceSource(conferenceSource: PresentationCall) -> Signal<(state: PresentationGroupCallState, invitedPeers: [EnginePeer]), NoError> {
|
||||
let invitedPeers = conferenceSource.context.engine.data.subscribe(
|
||||
EngineDataList((conferenceSource as! PresentationCallImpl).pendingInviteToConferencePeerIds.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0) })
|
||||
)
|
||||
|
||||
let accountPeerId = conferenceSource.context.account.peerId
|
||||
let conferenceSourcePeerId = conferenceSource.peerId
|
||||
|
||||
return combineLatest(queue: .mainQueue(),
|
||||
conferenceSource.state,
|
||||
conferenceSource.isMuted,
|
||||
invitedPeers
|
||||
)
|
||||
|> mapToSignal { state, isMuted, invitedPeers -> Signal<(state: PresentationGroupCallState, invitedPeers: [EnginePeer]), NoError> in
|
||||
let mappedNetworkState: PresentationGroupCallState.NetworkState
|
||||
switch state.state {
|
||||
case .active:
|
||||
mappedNetworkState = .connected
|
||||
default:
|
||||
mappedNetworkState = .connecting
|
||||
}
|
||||
|
||||
let callState = PresentationGroupCallState(
|
||||
myPeerId: accountPeerId,
|
||||
networkState: mappedNetworkState,
|
||||
canManageCall: false,
|
||||
adminIds: Set([accountPeerId, conferenceSourcePeerId]),
|
||||
muteState: isMuted ? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: true) : nil,
|
||||
defaultParticipantMuteState: nil,
|
||||
recordingStartTimestamp: nil,
|
||||
title: nil,
|
||||
raisedHand: false,
|
||||
scheduleTimestamp: nil,
|
||||
subscribedToScheduled: false,
|
||||
isVideoEnabled: true,
|
||||
isVideoWatchersLimitReached: false
|
||||
)
|
||||
|
||||
return .single((callState, invitedPeers.compactMap({ $0 })))
|
||||
}
|
||||
}
|
||||
|
||||
static func groupCallMembersForConferenceSource(conferenceSource: PresentationCall) -> Signal<PresentationGroupCallMembers, NoError> {
|
||||
return combineLatest(queue: .mainQueue(),
|
||||
conferenceSource.context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: conferenceSource.context.account.peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: conferenceSource.peerId)
|
||||
),
|
||||
conferenceSource.state
|
||||
)
|
||||
|> map { peers, state in
|
||||
var participants: [GroupCallParticipantsContext.Participant] = []
|
||||
let (myPeer, remotePeer) = peers
|
||||
if let myPeer {
|
||||
participants.append(GroupCallParticipantsContext.Participant(
|
||||
peer: myPeer._asPeer(),
|
||||
ssrc: nil,
|
||||
videoDescription: nil,
|
||||
presentationDescription: nil,
|
||||
joinTimestamp: 0,
|
||||
raiseHandRating: nil,
|
||||
hasRaiseHand: false,
|
||||
activityTimestamp: nil,
|
||||
activityRank: nil,
|
||||
muteState: nil,
|
||||
volume: nil,
|
||||
about: nil,
|
||||
joinedVideo: false
|
||||
))
|
||||
}
|
||||
if let remotePeer {
|
||||
participants.append(GroupCallParticipantsContext.Participant(
|
||||
peer: remotePeer._asPeer(),
|
||||
ssrc: nil,
|
||||
videoDescription: nil,
|
||||
presentationDescription: nil,
|
||||
joinTimestamp: 0,
|
||||
raiseHandRating: nil,
|
||||
hasRaiseHand: false,
|
||||
activityTimestamp: nil,
|
||||
activityRank: nil,
|
||||
muteState: nil,
|
||||
volume: nil,
|
||||
about: nil,
|
||||
joinedVideo: false
|
||||
))
|
||||
}
|
||||
let members = PresentationGroupCallMembers(
|
||||
participants: participants,
|
||||
speakingParticipants: Set(),
|
||||
totalCount: 2,
|
||||
loadMoreToken: nil
|
||||
)
|
||||
return members
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
@ -971,6 +1084,10 @@ final class VideoChatScreenComponent: Component {
|
||||
if self.component == nil {
|
||||
self.peer = component.initialData.peer
|
||||
self.members = component.initialData.members
|
||||
self.invitedPeers = component.initialData.invitedPeers
|
||||
if let members = self.members {
|
||||
self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.id }) })
|
||||
}
|
||||
self.callState = component.initialData.callState
|
||||
}
|
||||
|
||||
@ -1004,6 +1121,9 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
|
||||
self.members = members
|
||||
if let members {
|
||||
self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.id }) })
|
||||
}
|
||||
|
||||
if let members, let expandedParticipantsVideoState = self.expandedParticipantsVideoState, !expandedParticipantsVideoState.isUIHidden {
|
||||
var videoCount = 0
|
||||
@ -1103,6 +1223,35 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
})
|
||||
|
||||
self.invitedPeersDisposable?.dispose()
|
||||
let accountContext = groupCall.accountContext
|
||||
self.invitedPeersDisposable = (groupCall.invitedPeers
|
||||
|> mapToSignal { invitedPeers in
|
||||
return accountContext.engine.data.get(
|
||||
EngineDataList(invitedPeers.map({ TelegramEngine.EngineData.Item.Peer.Peer(id: $0) }))
|
||||
)
|
||||
|> map { peers in
|
||||
return peers.compactMap { $0 }
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] invitedPeers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var invitedPeers = invitedPeers
|
||||
if let members {
|
||||
invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.id }) })
|
||||
}
|
||||
|
||||
if self.invitedPeers != invitedPeers {
|
||||
self.invitedPeers = invitedPeers
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.stateDisposable?.dispose()
|
||||
self.stateDisposable = (groupCall.state
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] callState in
|
||||
@ -1243,64 +1392,14 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
case let .conferenceSource(conferenceSource):
|
||||
self.membersDisposable?.dispose()
|
||||
self.membersDisposable = (combineLatest(queue: .mainQueue(),
|
||||
conferenceSource.context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: conferenceSource.context.account.peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: conferenceSource.peerId)
|
||||
),
|
||||
conferenceSource.state
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] peers, state in
|
||||
self.membersDisposable = (View.groupCallMembersForConferenceSource(conferenceSource: conferenceSource)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] members in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var participants: [GroupCallParticipantsContext.Participant] = []
|
||||
let (myPeer, remotePeer) = peers
|
||||
if let myPeer {
|
||||
participants.append(GroupCallParticipantsContext.Participant(
|
||||
peer: myPeer._asPeer(),
|
||||
ssrc: nil,
|
||||
videoDescription: nil,
|
||||
presentationDescription: nil,
|
||||
joinTimestamp: 0,
|
||||
raiseHandRating: nil,
|
||||
hasRaiseHand: false,
|
||||
activityTimestamp: nil,
|
||||
activityRank: nil,
|
||||
muteState: nil,
|
||||
volume: nil,
|
||||
about: nil,
|
||||
joinedVideo: false
|
||||
))
|
||||
}
|
||||
if let remotePeer {
|
||||
participants.append(GroupCallParticipantsContext.Participant(
|
||||
peer: remotePeer._asPeer(),
|
||||
ssrc: nil,
|
||||
videoDescription: nil,
|
||||
presentationDescription: nil,
|
||||
joinTimestamp: 0,
|
||||
raiseHandRating: nil,
|
||||
hasRaiseHand: false,
|
||||
activityTimestamp: nil,
|
||||
activityRank: nil,
|
||||
muteState: nil,
|
||||
volume: nil,
|
||||
about: nil,
|
||||
joinedVideo: false
|
||||
))
|
||||
}
|
||||
let members: PresentationGroupCallMembers? = PresentationGroupCallMembers(
|
||||
participants: participants,
|
||||
speakingParticipants: Set(),
|
||||
totalCount: 2,
|
||||
loadMoreToken: nil
|
||||
)
|
||||
|
||||
if self.members != members {
|
||||
var members = members
|
||||
if let membersValue = members {
|
||||
let membersValue = members
|
||||
let participants = membersValue.participants
|
||||
members = PresentationGroupCallMembers(
|
||||
participants: participants,
|
||||
@ -1308,11 +1407,10 @@ final class VideoChatScreenComponent: Component {
|
||||
totalCount: membersValue.totalCount,
|
||||
loadMoreToken: membersValue.loadMoreToken
|
||||
)
|
||||
}
|
||||
|
||||
self.members = members
|
||||
|
||||
if let members, let expandedParticipantsVideoState = self.expandedParticipantsVideoState, !expandedParticipantsVideoState.isUIHidden {
|
||||
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, !expandedParticipantsVideoState.isUIHidden {
|
||||
var videoCount = 0
|
||||
for participant in members.participants {
|
||||
if participant.presentationDescription != nil {
|
||||
@ -1330,7 +1428,7 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, let members {
|
||||
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState {
|
||||
if CFAbsoluteTimeGetCurrent() > self.focusedSpeakerAutoSwitchDeadline, !expandedParticipantsVideoState.isMainParticipantPinned, let participant = members.participants.first(where: { participant in
|
||||
if let callState = self.callState, participant.peer.id == callState.myPeerId {
|
||||
return false
|
||||
@ -1396,7 +1494,7 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
|
||||
var speakingParticipantPeers: [EnginePeer] = []
|
||||
if let members, !members.speakingParticipants.isEmpty {
|
||||
if !members.speakingParticipants.isEmpty {
|
||||
for participant in members.participants {
|
||||
if members.speakingParticipants.contains(participant.peer.id) {
|
||||
speakingParticipantPeers.append(EnginePeer(participant.peer))
|
||||
@ -1410,43 +1508,27 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
})
|
||||
|
||||
self.invitedPeersDisposable?.dispose()
|
||||
self.invitedPeersDisposable = nil
|
||||
|
||||
self.stateDisposable?.dispose()
|
||||
self.stateDisposable = (combineLatest(queue: .mainQueue(),
|
||||
conferenceSource.state,
|
||||
conferenceSource.isMuted
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] state, isMuted in
|
||||
guard let self, case let .conferenceSource(conferenceSource) = self.currentCall else {
|
||||
self.stateDisposable = (View.groupCallStateForConferenceSource(conferenceSource: conferenceSource)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] callState, invitedPeers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let mappedNetworkState: PresentationGroupCallState.NetworkState
|
||||
switch state.state {
|
||||
case .active:
|
||||
mappedNetworkState = .connected
|
||||
default:
|
||||
mappedNetworkState = .connecting
|
||||
}
|
||||
|
||||
let callState = PresentationGroupCallState(
|
||||
myPeerId: conferenceSource.context.account.peerId,
|
||||
networkState: mappedNetworkState,
|
||||
canManageCall: false,
|
||||
adminIds: Set([conferenceSource.context.account.peerId, conferenceSource.peerId]),
|
||||
muteState: isMuted ? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: true) : nil,
|
||||
defaultParticipantMuteState: nil,
|
||||
recordingStartTimestamp: nil,
|
||||
title: nil,
|
||||
raisedHand: false,
|
||||
scheduleTimestamp: nil,
|
||||
subscribedToScheduled: false,
|
||||
isVideoEnabled: true,
|
||||
isVideoWatchersLimitReached: false
|
||||
)
|
||||
|
||||
var isUpdated = false
|
||||
if self.callState != callState {
|
||||
self.callState = callState
|
||||
isUpdated = true
|
||||
}
|
||||
if self.invitedPeers != invitedPeers {
|
||||
self.invitedPeers = invitedPeers
|
||||
isUpdated = true
|
||||
}
|
||||
|
||||
if isUpdated {
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
@ -1987,6 +2069,7 @@ final class VideoChatScreenComponent: Component {
|
||||
component: AnyComponent(VideoChatParticipantsComponent(
|
||||
call: call,
|
||||
participants: mappedParticipants,
|
||||
invitedPeers: self.invitedPeers,
|
||||
speakingParticipants: self.members?.speakingParticipants ?? Set(),
|
||||
expandedVideoState: self.expandedParticipantsVideoState,
|
||||
maxVideoQuality: self.maxVideoQuality,
|
||||
@ -2368,15 +2451,18 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
|
||||
let peer: EnginePeer?
|
||||
let members: PresentationGroupCallMembers?
|
||||
let callState: PresentationGroupCallState
|
||||
let invitedPeers: [EnginePeer]
|
||||
|
||||
init(
|
||||
peer: EnginePeer?,
|
||||
members: PresentationGroupCallMembers?,
|
||||
callState: PresentationGroupCallState
|
||||
callState: PresentationGroupCallState,
|
||||
invitedPeers: [EnginePeer]
|
||||
) {
|
||||
self.peer = peer
|
||||
self.members = members
|
||||
self.callState = callState
|
||||
self.invitedPeers = invitedPeers
|
||||
}
|
||||
}
|
||||
|
||||
@ -2424,6 +2510,8 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
|
||||
presentationMode: .default,
|
||||
theme: .custom(theme)
|
||||
)
|
||||
|
||||
self.flatReceivesModalTransition = true
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
@ -2521,39 +2609,40 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
|
||||
} else {
|
||||
callPeer = .single(nil)
|
||||
}
|
||||
let accountContext = groupCall.accountContext
|
||||
let invitedPeers = groupCall.invitedPeers |> take(1) |> mapToSignal { invitedPeers in
|
||||
return accountContext.engine.data.get(
|
||||
EngineDataList(invitedPeers.map({ TelegramEngine.EngineData.Item.Peer.Peer(id: $0) }))
|
||||
)
|
||||
}
|
||||
return combineLatest(
|
||||
callPeer,
|
||||
groupCall.members |> take(1),
|
||||
groupCall.state |> take(1)
|
||||
groupCall.state |> take(1),
|
||||
invitedPeers
|
||||
)
|
||||
|> map { peer, members, callState -> InitialData in
|
||||
|> map { peer, members, callState, invitedPeers -> InitialData in
|
||||
return InitialData(
|
||||
peer: peer,
|
||||
members: members,
|
||||
callState: callState
|
||||
callState: callState,
|
||||
invitedPeers: invitedPeers.compactMap { $0 }
|
||||
)
|
||||
}
|
||||
case let .conferenceSource(conferenceSource):
|
||||
//TODO:release move initialization from component
|
||||
return .single(InitialData(
|
||||
peer: nil,
|
||||
members: nil,
|
||||
callState: PresentationGroupCallState(
|
||||
myPeerId: conferenceSource.context.account.peerId,
|
||||
networkState: .connected,
|
||||
canManageCall: false,
|
||||
adminIds: Set(),
|
||||
muteState: nil,
|
||||
defaultParticipantMuteState: nil,
|
||||
recordingStartTimestamp: nil,
|
||||
title: nil,
|
||||
raisedHand: false,
|
||||
scheduleTimestamp: nil,
|
||||
subscribedToScheduled: false,
|
||||
isVideoEnabled: true,
|
||||
isVideoWatchersLimitReached: false
|
||||
return combineLatest(
|
||||
VideoChatScreenComponent.View.groupCallStateForConferenceSource(conferenceSource: conferenceSource) |> take(1),
|
||||
VideoChatScreenComponent.View.groupCallMembersForConferenceSource(conferenceSource: conferenceSource) |> take(1)
|
||||
)
|
||||
))
|
||||
|> map { stateAndInvitedPeers, members in
|
||||
let (state, invitedPeers) = stateAndInvitedPeers
|
||||
return InitialData(
|
||||
peer: nil,
|
||||
members: members,
|
||||
callState: state,
|
||||
invitedPeers: invitedPeers
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,131 @@ import LegacyMediaPickerUI
|
||||
import AvatarNode
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import CallsEmoji
|
||||
import AlertComponent
|
||||
import TelegramPresentationData
|
||||
import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
|
||||
private func resolvedEmojiKey(data: Data) -> [String] {
|
||||
let resolvedKey = stringForEmojiHashOfData(data, 4) ?? []
|
||||
return resolvedKey
|
||||
}
|
||||
|
||||
private final class EmojiKeyAlertComponet: CombinedComponent {
|
||||
let theme: PresentationTheme
|
||||
let emojiKey: [String]
|
||||
let title: String
|
||||
let text: String
|
||||
|
||||
init(theme: PresentationTheme, emojiKey: [String], title: String, text: String) {
|
||||
self.theme = theme
|
||||
self.emojiKey = emojiKey
|
||||
self.title = title
|
||||
self.text = text
|
||||
}
|
||||
|
||||
static func ==(lhs: EmojiKeyAlertComponet, rhs: EmojiKeyAlertComponet) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.emojiKey != rhs.emojiKey {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public static var body: Body {
|
||||
//let emojiKeyItems = ChildMap(environment: MultilineTextComponent.self, keyedBy: Int.self)
|
||||
let emojiKey = Child(MultilineTextComponent.self)
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
|
||||
return { context in
|
||||
/*let emojiKeyItems = context.component.emojiKey.map { item in
|
||||
return emojiKeyItems[item].update(
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: context.component.emojiKey.joined(separator: ""), font: Font.semibold(40.0), textColor: context.component.theme.actionSheet.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
availableSize: CGSize(width: 100.0, height: 100.0),
|
||||
transition: .immediate
|
||||
)
|
||||
}*/
|
||||
|
||||
let emojiKey = emojiKey.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: context.component.emojiKey.joined(separator: ""), font: Font.semibold(40.0), textColor: context.component.theme.actionSheet.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 10000.0),
|
||||
transition: .immediate
|
||||
)
|
||||
let title = title.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: context.component.title, font: Font.semibold(16.0), textColor: context.component.theme.actionSheet.primaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 10000.0),
|
||||
transition: .immediate
|
||||
)
|
||||
let text = text.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: context.component.text, font: Font.regular(13.0), textColor: context.component.theme.actionSheet.primaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 10000.0),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
var size = CGSize(width: 0.0, height: 0.0)
|
||||
|
||||
size.width = max(size.width, emojiKey.size.width)
|
||||
size.width = max(size.width, title.size.width)
|
||||
size.width = max(size.width, text.size.width)
|
||||
|
||||
let titleSpacing: CGFloat = 10.0
|
||||
let textSpacing: CGFloat = 10.0
|
||||
|
||||
size.height += emojiKey.size.height
|
||||
size.height += titleSpacing
|
||||
size.height += title.size.height
|
||||
size.height += textSpacing
|
||||
size.height += text.size.height
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
let emojiKeyFrame = CGRect(origin: CGPoint(x: floor((size.width - emojiKey.size.width) * 0.5), y: contentHeight), size: emojiKey.size)
|
||||
contentHeight += emojiKey.size.height + titleSpacing
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - title.size.width) * 0.5), y: contentHeight), size: title.size)
|
||||
contentHeight += title.size.height + textSpacing
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - text.size.width) * 0.5), y: contentHeight), size: text.size)
|
||||
contentHeight += text.size.height + 5.0
|
||||
|
||||
context.add(emojiKey
|
||||
.position(emojiKeyFrame.center)
|
||||
)
|
||||
context.add(title
|
||||
.position(titleFrame.center)
|
||||
)
|
||||
context.add(text
|
||||
.position(textFrame.center)
|
||||
)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoChatScreenComponent.View {
|
||||
func openMoreMenu() {
|
||||
@ -50,6 +175,35 @@ extension VideoChatScreenComponent.View {
|
||||
}
|
||||
}
|
||||
|
||||
if case let .group(groupCall) = currentCall, let encryptionKey = groupCall.encryptionKeyValue {
|
||||
//TODO:localize
|
||||
let emojiKey = resolvedEmojiKey(data: encryptionKey)
|
||||
items.append(.action(ContextMenuActionItem(text: "Encryption Key", textLayout: .secondLineWithValue(emojiKey.joined(separator: "")), icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Lock"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
c?.dismiss(completion: nil)
|
||||
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
|
||||
let alertController = componentAlertController(
|
||||
theme: AlertControllerTheme(presentationTheme: defaultDarkPresentationTheme, fontSize: .regular),
|
||||
content: AnyComponent(EmojiKeyAlertComponet(
|
||||
theme: defaultDarkPresentationTheme,
|
||||
emojiKey: emojiKey,
|
||||
title: "This call is end-to-end encrypted",
|
||||
text: "If the emojis on everyone's screens are the same, this call is 100% secure."
|
||||
)),
|
||||
actions: [ComponentAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})],
|
||||
actionLayout: .horizontal
|
||||
)
|
||||
|
||||
environment.controller()?.present(alertController, in: .window(.root))
|
||||
})))
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
if let (availableOutputs, currentOutput) = self.audioOutputState, availableOutputs.count > 1 {
|
||||
var currentOutputTitle = ""
|
||||
for output in availableOutputs {
|
||||
|
@ -751,7 +751,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
}
|
||||
self.backgroundLayer.update(stateIndex: backgroundStateIndex, isEnergySavingEnabled: params.state.isEnergySavingEnabled, transition: transition)
|
||||
|
||||
genericAlphaTransition.setAlpha(layer: self.backgroundLayer, alpha: self.isAnimatedOutToGroupCall ? 0.0 : 1.0, completion: { [weak self] _ in
|
||||
let backgroundAlpha = self.isAnimatedOutToGroupCall ? 0.0 : 1.0
|
||||
if CGFloat(self.backgroundLayer.opacity) != backgroundAlpha {
|
||||
genericAlphaTransition.setAlpha(layer: self.backgroundLayer, alpha: backgroundAlpha, completion: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -760,6 +762,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
animateOutToGroupCallCompletion()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.buttonGroupView, frame: CGRect(origin: CGPoint(), size: params.size))
|
||||
|
||||
@ -914,7 +917,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
transition.setFrame(view: self.backButtonView, frame: backButtonFrame)
|
||||
genericAlphaTransition.setAlpha(view: self.backButtonView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0)
|
||||
|
||||
|
||||
var isConferencePossible = false
|
||||
if case .active = params.state.lifecycleState, params.state.isConferencePossible {
|
||||
isConferencePossible = true
|
||||
@ -952,7 +954,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
let conferenceButtonFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - 10.0 - conferenceButtonSize.width, y: conferenceButtonY), size: conferenceButtonSize)
|
||||
|
||||
conferenceButtonTransition.setFrame(view: conferenceButtonView, frame: conferenceButtonFrame)
|
||||
genericAlphaTransition.setAlpha(view: conferenceButtonView, alpha: 1.0)
|
||||
genericAlphaTransition.setAlpha(view: conferenceButtonView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0)
|
||||
} else {
|
||||
if let conferenceButtonView = self.conferenceButtonView {
|
||||
self.conferenceButtonView = nil
|
||||
@ -1291,8 +1293,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
}
|
||||
|
||||
self.avatarLayer.update(size: collapsedAvatarFrame.size, isExpanded: havePrimaryVideo, cornerRadius: avatarCornerRadius, transition: transition)
|
||||
transition.setAlpha(layer: self.avatarLayer, alpha: (expandedEmojiKeyOverlapsAvatar && !havePrimaryVideo) ? 0.0 : 1.0)
|
||||
transition.setScale(layer: self.avatarLayer, scale: expandedEmojiKeyOverlapsAvatar ? 0.001 : 1.0)
|
||||
transition.setAlpha(layer: self.avatarLayer, alpha: (self.isAnimatedOutToGroupCall || (expandedEmojiKeyOverlapsAvatar && !havePrimaryVideo)) ? 0.0 : 1.0)
|
||||
transition.setScale(layer: self.avatarLayer, scale: (self.isAnimatedOutToGroupCall || expandedEmojiKeyOverlapsAvatar) ? 0.001 : 1.0)
|
||||
|
||||
transition.setPosition(view: self.videoContainerBackgroundView, position: avatarFrame.center)
|
||||
transition.setBounds(view: self.videoContainerBackgroundView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
@ -1347,8 +1349,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
transition.setScale(layer: self.avatarTransformLayer, scale: 1.0)
|
||||
transition.setScale(layer: self.blobTransformLayer, scale: 1.0)
|
||||
} else {
|
||||
genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: (expandedEmojiKeyOverlapsAvatar && !havePrimaryVideo) ? 0.0 : 1.0)
|
||||
transition.setScale(layer: self.blobLayer, scale: expandedEmojiKeyOverlapsAvatar ? 0.001 : 1.0)
|
||||
genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: (self.isAnimatedOutToGroupCall || (expandedEmojiKeyOverlapsAvatar && !havePrimaryVideo)) ? 0.0 : 1.0)
|
||||
transition.setScale(layer: self.blobLayer, scale: (self.isAnimatedOutToGroupCall || expandedEmojiKeyOverlapsAvatar) ? 0.001 : 1.0)
|
||||
if !havePrimaryVideo {
|
||||
self.canAnimateAudioLevel = true
|
||||
}
|
||||
|
@ -1864,6 +1864,21 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
statusNode.frame = statusFrame
|
||||
}
|
||||
|
||||
var videoTimestamp: Int32?
|
||||
var storedVideoTimestamp: Int32?
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? ForwardVideoTimestampAttribute {
|
||||
videoTimestamp = attribute.timestamp
|
||||
} else if let attribute = attribute as? DerivedDataMessageAttribute {
|
||||
if let value = attribute.data["mps"]?.get(MediaPlaybackStoredState.self) {
|
||||
storedVideoTimestamp = Int32(value.timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let storedVideoTimestamp {
|
||||
videoTimestamp = storedVideoTimestamp
|
||||
}
|
||||
|
||||
var updatedVideoNodeReadySignal: Signal<Void, NoError>?
|
||||
var updatedPlayerStatusSignal: Signal<MediaPlayerStatus?, NoError>?
|
||||
if let currentReplaceVideoNode = replaceVideoNode {
|
||||
@ -1920,8 +1935,15 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
}
|
||||
let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
|
||||
videoNode.isUserInteractionEnabled = false
|
||||
var firstTime = true
|
||||
videoNode.ownsContentNodeUpdated = { [weak self] owns in
|
||||
if let strongSelf = self {
|
||||
if firstTime {
|
||||
firstTime = false
|
||||
if let videoTimestamp {
|
||||
videoNode.seek(Double(videoTimestamp))
|
||||
}
|
||||
}
|
||||
strongSelf.videoNode?.isHidden = !owns
|
||||
if owns {
|
||||
strongSelf.videoNode?.setBaseRate(1.0)
|
||||
@ -2006,21 +2028,6 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
}
|
||||
}
|
||||
|
||||
var videoTimestamp: Int32?
|
||||
var storedVideoTimestamp: Int32?
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? ForwardVideoTimestampAttribute {
|
||||
videoTimestamp = attribute.timestamp
|
||||
} else if let attribute = attribute as? DerivedDataMessageAttribute {
|
||||
if let value = attribute.data["mps"]?.get(MediaPlaybackStoredState.self) {
|
||||
storedVideoTimestamp = Int32(value.timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let storedVideoTimestamp {
|
||||
videoTimestamp = storedVideoTimestamp
|
||||
}
|
||||
|
||||
if let videoTimestamp, let file = media as? TelegramMediaFile, let duration = file.duration, duration > 1.0 {
|
||||
let timestampContainerView: UIView
|
||||
if let current = strongSelf.timestampContainerView {
|
||||
@ -2062,9 +2069,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
}
|
||||
|
||||
videoTimestampBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.5).cgColor
|
||||
videoTimestampForegroundLayer.backgroundColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.accentControlColor.cgColor : presentationData.theme.theme.chat.message.outgoing.accentControlColor.cgColor
|
||||
videoTimestampForegroundLayer.backgroundColor = UIColor(rgb: 0x0A84FF).cgColor
|
||||
|
||||
timestampContainerView.frame = imageFrame
|
||||
timestampContainerView.frame = imageFrame.offsetBy(dx: arguments.corners.extendedEdges.left, dy: 0.0)
|
||||
timestampMaskView.frame = imageFrame
|
||||
|
||||
let videoTimestampBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: imageFrame.height - 3.0), size: CGSize(width: imageFrame.width, height: 3.0))
|
||||
@ -3093,7 +3100,6 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
}
|
||||
|
||||
public func scrubberTransition() -> GalleryItemScrubberTransition? {
|
||||
if let timestampContainerView = self.timestampContainerView, let timestampMaskView = self.timestampMaskView, let videoTimestampBackgroundLayer = self.videoTimestampBackgroundLayer, let videoTimestampForegroundLayer = self.videoTimestampForegroundLayer {
|
||||
final class TimestampContainerTransitionView: UIView {
|
||||
let containerView: UIView
|
||||
let containerMaskView: UIImageView
|
||||
@ -3131,7 +3137,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(state: GalleryItemScrubberTransition.TransitionState, transition: ContainedViewLayoutTransition) {
|
||||
func update(state: GalleryItemScrubberTransition.Scrubber.TransitionState, transition: ContainedViewLayoutTransition) {
|
||||
let containerFrame = CGRect(origin: CGPoint(), size: state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress))
|
||||
transition.updateFrame(view: self.containerView, frame: containerFrame)
|
||||
transition.updateFrame(view: self.containerMaskView, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
|
||||
@ -3141,7 +3147,128 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
}
|
||||
}
|
||||
|
||||
return GalleryItemScrubberTransition(
|
||||
final class MediaContentTransitionView: UIView {
|
||||
let backgroundLayer: SimpleLayer
|
||||
let backgroundMaskLayer: SimpleShapeLayer
|
||||
let sourceCorners: ImageCorners
|
||||
|
||||
init(imageNode: TransformImageNode) {
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
self.backgroundLayer.backgroundColor = UIColor.black.cgColor
|
||||
|
||||
self.backgroundMaskLayer = SimpleShapeLayer()
|
||||
self.backgroundMaskLayer.fillColor = UIColor.white.cgColor
|
||||
self.backgroundLayer.mask = self.backgroundMaskLayer
|
||||
|
||||
self.sourceCorners = imageNode.currentArguments?.corners ?? ImageCorners()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(state: GalleryItemScrubberTransition.Content.TransitionState, transition: ContainedViewLayoutTransition) {
|
||||
let sourceCorners: (topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) = (max(0.1, self.sourceCorners.topLeft.radius), max(0.1, self.sourceCorners.topRight.radius), max(0.1, self.sourceCorners.bottomLeft.radius), max(0.1, self.sourceCorners.bottomRight.radius))
|
||||
let destinationCorners: (topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) = (max(0.1, state.destinationCornerRadius), max(0.1, state.destinationCornerRadius), max(0.1, state.destinationCornerRadius), max(0.1, state.destinationCornerRadius))
|
||||
|
||||
let currentCornersData = CGRect(x: sourceCorners.topLeft, y: sourceCorners.topRight, width: sourceCorners.bottomLeft, height: sourceCorners.bottomRight).interpolate(to: CGRect(x: destinationCorners.topLeft, y: destinationCorners.topRight, width: destinationCorners.bottomLeft, height: destinationCorners.bottomRight), amount: state.progress)
|
||||
let currentCorners: (topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) = (currentCornersData.minX, currentCornersData.minY, currentCornersData.width, currentCornersData.height)
|
||||
|
||||
func makeRoundedRectPath(
|
||||
in rect: CGRect,
|
||||
topLeft: CGFloat,
|
||||
topRight: CGFloat,
|
||||
bottomRight: CGFloat,
|
||||
bottomLeft: CGFloat
|
||||
) -> CGPath {
|
||||
let path = CGMutablePath()
|
||||
|
||||
// Move to top-left, offset by its corner radius
|
||||
path.move(to: CGPoint(x: rect.minX + topLeft, y: rect.minY))
|
||||
|
||||
// Top edge (straight line)
|
||||
path.addLine(to: CGPoint(x: rect.maxX - topRight, y: rect.minY))
|
||||
// Top-right corner arc
|
||||
if topRight > 0 {
|
||||
path.addArc(
|
||||
center: CGPoint(x: rect.maxX - topRight, y: rect.minY + topRight),
|
||||
radius: topRight,
|
||||
startAngle: -CGFloat.pi / 2,
|
||||
endAngle: 0,
|
||||
clockwise: false
|
||||
)
|
||||
}
|
||||
|
||||
// Right edge (straight line)
|
||||
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - bottomRight))
|
||||
// Bottom-right corner arc
|
||||
if bottomRight > 0 {
|
||||
path.addArc(
|
||||
center: CGPoint(x: rect.maxX - bottomRight, y: rect.maxY - bottomRight),
|
||||
radius: bottomRight,
|
||||
startAngle: 0,
|
||||
endAngle: CGFloat.pi / 2,
|
||||
clockwise: false
|
||||
)
|
||||
}
|
||||
|
||||
// Bottom edge (straight line)
|
||||
path.addLine(to: CGPoint(x: rect.minX + bottomLeft, y: rect.maxY))
|
||||
// Bottom-left corner arc
|
||||
if bottomLeft > 0 {
|
||||
path.addArc(
|
||||
center: CGPoint(x: rect.minX + bottomLeft, y: rect.maxY - bottomLeft),
|
||||
radius: bottomLeft,
|
||||
startAngle: CGFloat.pi / 2,
|
||||
endAngle: CGFloat.pi,
|
||||
clockwise: false
|
||||
)
|
||||
}
|
||||
|
||||
// Left edge (straight line)
|
||||
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + topLeft))
|
||||
// Top-left corner arc
|
||||
if topLeft > 0 {
|
||||
path.addArc(
|
||||
center: CGPoint(x: rect.minX + topLeft, y: rect.minY + topLeft),
|
||||
radius: topLeft,
|
||||
startAngle: CGFloat.pi,
|
||||
endAngle: 3 * CGFloat.pi / 2,
|
||||
clockwise: false
|
||||
)
|
||||
}
|
||||
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress))
|
||||
|
||||
transition.updatePath(layer: self.backgroundMaskLayer, path: makeRoundedRectPath(in: CGRect(origin: CGPoint(), size: backgroundFrame.size), topLeft: currentCorners.topLeft, topRight: currentCorners.topRight, bottomRight: currentCorners.bottomLeft, bottomLeft: currentCorners.bottomRight))
|
||||
|
||||
transition.updateFrame(layer: self.backgroundLayer, frame: backgroundFrame)
|
||||
|
||||
transition.updateFrame(layer: self.backgroundMaskLayer, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
}
|
||||
}
|
||||
|
||||
guard let currentImageArguments = self.currentImageArguments else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sourceContentRect = self.imageNode.bounds
|
||||
sourceContentRect.origin.x += currentImageArguments.insets.left
|
||||
sourceContentRect.origin.y += currentImageArguments.insets.top
|
||||
sourceContentRect.size.width -= currentImageArguments.insets.left + currentImageArguments.insets.right
|
||||
sourceContentRect.size.height -= currentImageArguments.insets.top + currentImageArguments.insets.bottom
|
||||
|
||||
var scrubber: GalleryItemScrubberTransition.Scrubber?
|
||||
if let timestampContainerView = self.timestampContainerView, let timestampMaskView = self.timestampMaskView, let videoTimestampBackgroundLayer = self.videoTimestampBackgroundLayer, let videoTimestampForegroundLayer = self.videoTimestampForegroundLayer {
|
||||
scrubber = GalleryItemScrubberTransition.Scrubber(
|
||||
view: timestampContainerView,
|
||||
makeView: { [weak timestampContainerView, weak timestampMaskView, weak videoTimestampBackgroundLayer, weak videoTimestampForegroundLayer] in
|
||||
return TimestampContainerTransitionView(timestampContainerView: timestampContainerView, timestampMaskView: timestampMaskView, videoTimestampBackgroundLayer: videoTimestampBackgroundLayer, videoTimestampForegroundLayer: videoTimestampForegroundLayer)
|
||||
@ -3150,12 +3277,33 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
if let view = view as? TimestampContainerTransitionView {
|
||||
view.update(state: state, transition: transition)
|
||||
}
|
||||
},
|
||||
insertCloneTransitionView: nil
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var content: GalleryItemScrubberTransition.Content?
|
||||
content = GalleryItemScrubberTransition.Content(
|
||||
sourceView: self.imageNode.view,
|
||||
sourceRect: sourceContentRect,
|
||||
makeView: { [weak self] in
|
||||
guard let self else {
|
||||
return UIView()
|
||||
}
|
||||
|
||||
return MediaContentTransitionView(imageNode: self.imageNode)
|
||||
},
|
||||
updateView: { view, state, transition in
|
||||
guard let view = view as? MediaContentTransitionView else {
|
||||
return
|
||||
}
|
||||
view.update(state: state, transition: transition)
|
||||
}
|
||||
)
|
||||
|
||||
return GalleryItemScrubberTransition(
|
||||
scrubber: scrubber,
|
||||
content: content
|
||||
)
|
||||
}
|
||||
|
||||
public func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? {
|
||||
|
@ -1439,7 +1439,8 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
})
|
||||
}
|
||||
|
||||
self.channelsForPublicReactionDisposable = (component.context.engine.peers.channelsForPublicReaction(useLocalCache: false)
|
||||
//TODO:wip-release
|
||||
/*self.channelsForPublicReactionDisposable = (component.context.engine.peers.channelsForPublicReaction(useLocalCache: false)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] peers in
|
||||
guard let self else {
|
||||
return
|
||||
@ -1448,7 +1449,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
self.channelsForPublicReaction = peers
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
})*/
|
||||
}
|
||||
|
||||
self.component = component
|
||||
|
@ -245,7 +245,7 @@ public final class PeerListItemComponent: Component {
|
||||
let hasNext: Bool
|
||||
let extractedTheme: ExtractedTheme?
|
||||
let insets: UIEdgeInsets?
|
||||
let action: (EnginePeer, EngineMessage.Id?, PeerListItemComponent.View) -> Void
|
||||
let action: ((EnginePeer, EngineMessage.Id?, PeerListItemComponent.View) -> Void)?
|
||||
let inlineActions: InlineActionsState?
|
||||
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
|
||||
let openStories: ((EnginePeer, AvatarNode) -> Void)?
|
||||
@ -276,7 +276,7 @@ public final class PeerListItemComponent: Component {
|
||||
hasNext: Bool,
|
||||
extractedTheme: ExtractedTheme? = nil,
|
||||
insets: UIEdgeInsets? = nil,
|
||||
action: @escaping (EnginePeer, EngineMessage.Id?, PeerListItemComponent.View) -> Void,
|
||||
action: ((EnginePeer, EngineMessage.Id?, PeerListItemComponent.View) -> Void)?,
|
||||
inlineActions: InlineActionsState? = nil,
|
||||
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil,
|
||||
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
|
||||
@ -391,6 +391,12 @@ public final class PeerListItemComponent: Component {
|
||||
if lhs.inlineActions != rhs.inlineActions {
|
||||
return false
|
||||
}
|
||||
if (lhs.action == nil) != (rhs.action == nil) {
|
||||
return false
|
||||
}
|
||||
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -568,7 +574,7 @@ public final class PeerListItemComponent: Component {
|
||||
guard let component = self.component, let peer = component.peer else {
|
||||
return
|
||||
}
|
||||
component.action(peer, component.message?.id, self)
|
||||
component.action?(peer, component.message?.id, self)
|
||||
}
|
||||
|
||||
@objc private func avatarButtonPressed() {
|
||||
@ -673,6 +679,7 @@ public final class PeerListItemComponent: Component {
|
||||
self.state = state
|
||||
|
||||
self.containerButton.alpha = component.isEnabled ? 1.0 : 0.3
|
||||
self.containerButton.isEnabled = component.action != nil
|
||||
|
||||
self.avatarButtonView.isUserInteractionEnabled = component.storyStats != nil && component.openStories != nil
|
||||
|
||||
|
@ -51,7 +51,33 @@ extension ChatControllerImpl {
|
||||
return
|
||||
}
|
||||
|
||||
if message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, let addressName = channel.addressName {
|
||||
var timestampSuffix = ""
|
||||
let startAtTimestamp = parseTimeString(timecode)
|
||||
|
||||
var startAtTimestampString = ""
|
||||
let hours = startAtTimestamp / 3600
|
||||
let minutes = startAtTimestamp / 60 % 60
|
||||
let seconds = startAtTimestamp % 60
|
||||
if hours == 0 && minutes == 0 {
|
||||
startAtTimestampString = "\(startAtTimestamp)"
|
||||
} else {
|
||||
if hours != 0 {
|
||||
startAtTimestampString += "\(hours)h"
|
||||
}
|
||||
if minutes != 0 {
|
||||
startAtTimestampString += "\(minutes)m"
|
||||
}
|
||||
if seconds != 0 {
|
||||
startAtTimestampString += "\(seconds)s"
|
||||
}
|
||||
}
|
||||
timestampSuffix = "?t=\(startAtTimestampString)"
|
||||
let inputCopyText = "https://t.me/\(addressName)/\(message.id.id)\(timestampSuffix)"
|
||||
UIPasteboard.general.string = inputCopyText
|
||||
} else {
|
||||
UIPasteboard.general.string = timecode
|
||||
}
|
||||
|
||||
self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
}))
|
||||
@ -67,3 +93,30 @@ extension ChatControllerImpl {
|
||||
self.window?.presentInGlobalOverlay(controller)
|
||||
}
|
||||
}
|
||||
|
||||
private func parseTimeString(_ timeString: String) -> Int {
|
||||
let parts = timeString.split(separator: ":").map(String.init)
|
||||
|
||||
switch parts.count {
|
||||
case 1:
|
||||
// Single component (e.g. "1", "10") => seconds
|
||||
return Int(parts[0]) ?? 0
|
||||
|
||||
case 2:
|
||||
// Two components (e.g. "1:01", "10:30") => minutes:seconds
|
||||
let minutes = Int(parts[0]) ?? 0
|
||||
let seconds = Int(parts[1]) ?? 0
|
||||
return minutes * 60 + seconds
|
||||
|
||||
case 3:
|
||||
// Three components (e.g. "1:01:01", "10:00:00") => hours:minutes:seconds
|
||||
let hours = Int(parts[0]) ?? 0
|
||||
let minutes = Int(parts[1]) ?? 0
|
||||
let seconds = Int(parts[2]) ?? 0
|
||||
return hours * 3600 + minutes * 60 + seconds
|
||||
|
||||
default:
|
||||
// Fallback to 0 or handle invalid format
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
@ -10974,10 +10974,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
public func updatePushedTransition(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
if !transition.isAnimated {
|
||||
self.chatDisplayNode.historyNodeContainer.layer.removeAllAnimations()
|
||||
self.chatDisplayNode.historyNode.layer.removeAnimation(forKey: "sublayerTransform")
|
||||
}
|
||||
let scale: CGFloat = 1.0 - 0.06 * fraction
|
||||
transition.updateTransformScale(node: self.chatDisplayNode.historyNodeContainer, scale: scale)
|
||||
transition.updateSublayerTransformScale(node: self.chatDisplayNode.historyNode, scale: scale)
|
||||
}
|
||||
|
||||
func restrictedSendingContentsText() -> String {
|
||||
|
@ -250,7 +250,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
|
||||
case let .chats(chatsNode):
|
||||
count = chatsNode.currentState.selectedPeerIds.count
|
||||
}
|
||||
self.titleView.title = CounterControllerTitle(title: self.presentationData.strings.Compose_NewGroupTitle, counter: "\(count)/\(maxCount)")
|
||||
self.titleView.title = CounterControllerTitle(title: self.params.title ?? self.presentationData.strings.Compose_NewGroupTitle, counter: "\(count)/\(maxCount)")
|
||||
if self.rightNavigationButton == nil {
|
||||
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
|
||||
self.rightNavigationButton = rightNavigationButton
|
||||
@ -262,23 +262,23 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
|
||||
if case let .contacts(contactsNode) = self.contactsNode.contentNode {
|
||||
count = contactsNode.selectionState?.selectedPeerIndices.count ?? 0
|
||||
}
|
||||
self.titleView.title = CounterControllerTitle(title: hasActions ? self.presentationData.strings.Premium_Gift_ContactSelection_Title : self.presentationData.strings.Stars_Purchase_GiftStars, counter: "\(count)/\(maxCount)")
|
||||
self.titleView.title = CounterControllerTitle(title: self.params.title ?? (hasActions ? self.presentationData.strings.Premium_Gift_ContactSelection_Title : self.presentationData.strings.Stars_Purchase_GiftStars), counter: "\(count)/\(maxCount)")
|
||||
case .requestedUsersSelection:
|
||||
let maxCount: Int32 = self.limit ?? 10
|
||||
var count = 0
|
||||
if case let .contacts(contactsNode) = self.contactsNode.contentNode {
|
||||
count = contactsNode.selectionState?.selectedPeerIndices.count ?? 0
|
||||
}
|
||||
self.titleView.title = CounterControllerTitle(title: self.presentationData.strings.RequestPeer_SelectUsers, counter: "\(count)/\(maxCount)")
|
||||
self.titleView.title = CounterControllerTitle(title: self.params.title ?? self.presentationData.strings.RequestPeer_SelectUsers, counter: "\(count)/\(maxCount)")
|
||||
case .channelCreation:
|
||||
self.titleView.title = CounterControllerTitle(title: self.presentationData.strings.GroupInfo_AddParticipantTitle, counter: "")
|
||||
self.titleView.title = CounterControllerTitle(title: self.params.title ?? self.presentationData.strings.GroupInfo_AddParticipantTitle, counter: "")
|
||||
if self.rightNavigationButton == nil {
|
||||
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
|
||||
self.rightNavigationButton = rightNavigationButton
|
||||
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
|
||||
}
|
||||
case .peerSelection:
|
||||
self.titleView.title = CounterControllerTitle(title: self.presentationData.strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder, counter: "")
|
||||
self.titleView.title = CounterControllerTitle(title: self.params.title ?? self.presentationData.strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder, counter: "")
|
||||
if self.rightNavigationButton == nil {
|
||||
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
|
||||
self.rightNavigationButton = rightNavigationButton
|
||||
@ -286,7 +286,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
|
||||
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
|
||||
}
|
||||
case let .chatSelection(chatSelection):
|
||||
self.titleView.title = CounterControllerTitle(title: chatSelection.title, counter: "")
|
||||
self.titleView.title = CounterControllerTitle(title: self.params.title ?? chatSelection.title, counter: "")
|
||||
if self.rightNavigationButton == nil {
|
||||
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
|
||||
self.rightNavigationButton = rightNavigationButton
|
||||
|
@ -241,7 +241,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
|
||||
return .natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers)
|
||||
}
|
||||
|
||||
let contactListNode = ContactListNode(context: context, updatedPresentationData: updatedPresentationData, presentation: presentation, filters: filters, onlyWriteable: onlyWriteable, isGroupInvitation: isGroupInvitation, selectionState: ContactListNodeGroupSelectionState())
|
||||
let contactListNode = ContactListNode(context: context, updatedPresentationData: updatedPresentationData, presentation: presentation, filters: filters, onlyWriteable: onlyWriteable, isGroupInvitation: isGroupInvitation, isPeerEnabled: isPeerEnabled, selectionState: ContactListNodeGroupSelectionState())
|
||||
self.contentNode = .contacts(contactListNode)
|
||||
|
||||
if !selectedPeers.isEmpty {
|
||||
|
@ -843,7 +843,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
guard let callController = self.callController, callController.call === call else {
|
||||
return
|
||||
}
|
||||
if call.conferenceCall != nil {
|
||||
if call.conferenceStateValue != nil {
|
||||
self.callState.set(.single(nil))
|
||||
self.presentControllerWithCurrentCall()
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"app": "12",
|
||||
"app": "11.7.1",
|
||||
"xcode": "16.0",
|
||||
"bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff",
|
||||
"macos": "15.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user