mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
0a67b254f8
@ -22,6 +22,7 @@ internal:
|
||||
- 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/appcenter-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=adhoc --configuration=release_arm64
|
||||
- python3 -u build-system/Make/DeployToFirebase.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/firebase-configurations/firebase-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
|
||||
- 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"
|
||||
environment:
|
||||
name: internal
|
||||
|
72
build-system/Make/DeployToFirebase.py
Normal file
72
build-system/Make/DeployToFirebase.py
Normal file
@ -0,0 +1,72 @@
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import json
|
||||
|
||||
from BuildEnvironment import check_run_system
|
||||
|
||||
def deploy_to_firebase(args):
|
||||
if not os.path.exists(args.configuration):
|
||||
print('{} does not exist'.format(args.configuration))
|
||||
sys.exit(1)
|
||||
if not os.path.exists(args.ipa):
|
||||
print('{} does not exist'.format(args.ipa))
|
||||
sys.exit(1)
|
||||
if args.dsyms is not None and not os.path.exists(args.dsyms):
|
||||
print('{} does not exist'.format(args.dsyms))
|
||||
sys.exit(1)
|
||||
|
||||
with open(args.configuration) as file:
|
||||
configuration_dict = json.load(file)
|
||||
required_keys = [
|
||||
'app_id',
|
||||
'group',
|
||||
]
|
||||
for key in required_keys:
|
||||
if key not in configuration_dict:
|
||||
print('Configuration at {} does not contain {}'.format(args.configuration, key))
|
||||
sys.exit(1)
|
||||
|
||||
debug_flag = "--debug" if args.debug else ""
|
||||
command = 'firebase appdistribution:distribute --app {app_id} --groups "{group}" {debug_flag}'.format(
|
||||
app_id=configuration_dict['app_id'],
|
||||
group=configuration_dict['group'],
|
||||
debug_flag=debug_flag
|
||||
)
|
||||
|
||||
command += ' "{ipa_path}"'.format(ipa_path=args.ipa)
|
||||
|
||||
check_run_system(command)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(prog='deploy-firebase')
|
||||
|
||||
parser.add_argument(
|
||||
'--configuration',
|
||||
required=True,
|
||||
help='Path to configuration json.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--ipa',
|
||||
required=True,
|
||||
help='Path to IPA.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dsyms',
|
||||
required=False,
|
||||
help='Path to DSYMs.zip.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
help='Enable debug output for firebase deploy.'
|
||||
)
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
deploy_to_firebase(args)
|
@ -111,6 +111,7 @@ public final class ContactSelectionControllerParams {
|
||||
public let requirePhoneNumbers: Bool
|
||||
public let allowChannelsInSearch: Bool
|
||||
public let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
|
||||
public let isPeerEnabled: (ContactListPeer) -> Bool
|
||||
public let openProfile: ((EnginePeer) -> Void)?
|
||||
public let sendMessage: ((EnginePeer) -> Void)?
|
||||
|
||||
@ -127,6 +128,7 @@ public final class ContactSelectionControllerParams {
|
||||
requirePhoneNumbers: Bool = false,
|
||||
allowChannelsInSearch: Bool = false,
|
||||
confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) },
|
||||
isPeerEnabled: @escaping (ContactListPeer) -> Bool = { _ in true },
|
||||
openProfile: ((EnginePeer) -> Void)? = nil,
|
||||
sendMessage: ((EnginePeer) -> Void)? = nil
|
||||
) {
|
||||
@ -142,6 +144,7 @@ public final class ContactSelectionControllerParams {
|
||||
self.requirePhoneNumbers = requirePhoneNumbers
|
||||
self.allowChannelsInSearch = allowChannelsInSearch
|
||||
self.confirmation = confirmation
|
||||
self.isPeerEnabled = isPeerEnabled
|
||||
self.openProfile = openProfile
|
||||
self.sendMessage = sendMessage
|
||||
}
|
||||
|
@ -550,10 +550,6 @@ public enum PresentationCurrentCall: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum JoinConferenceCallMode {
|
||||
case joining
|
||||
}
|
||||
|
||||
public protocol PresentationCallManager: AnyObject {
|
||||
var currentCallSignal: Signal<PresentationCall?, NoError> { get }
|
||||
var currentGroupCallSignal: Signal<VideoChatCall?, NoError> { get }
|
||||
@ -568,6 +564,6 @@ public protocol PresentationCallManager: AnyObject {
|
||||
accountContext: AccountContext,
|
||||
initialCall: EngineGroupCallDescription,
|
||||
reference: InternalGroupCallReference,
|
||||
mode: JoinConferenceCallMode
|
||||
beginWithVideo: Bool
|
||||
)
|
||||
}
|
||||
|
@ -231,7 +231,7 @@ public final class CallListController: TelegramBaseController {
|
||||
isStream: false
|
||||
),
|
||||
reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash),
|
||||
mode: .joining
|
||||
beginWithVideo: false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -692,6 +692,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
public func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
|
||||
var reallyHighlighted = self.isHighlighted
|
||||
if let item = self.item, !item.enabled {
|
||||
reallyHighlighted = false
|
||||
}
|
||||
let highlightProgress: CGFloat = self.item?.itemHighlighting?.progress ?? 1.0
|
||||
if let item = self.item {
|
||||
switch item.peer {
|
||||
@ -1649,6 +1652,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
actionButtonNode.setImage(actionButton.image, for: .normal)
|
||||
transition.updateFrame(node: actionButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 12.0 - actionButtonImage.size.width - offset, y: floor((nodeLayout.contentSize.height - actionButtonImage.size.height) / 2.0)), size: actionButtonImage.size))
|
||||
|
||||
actionButtonNode.isEnabled = item.enabled
|
||||
actionButtonNode.alpha = item.enabled ? 1.0 : 0.4
|
||||
|
||||
offset += actionButtonImage.size.width + 12.0
|
||||
}
|
||||
}
|
||||
|
@ -486,7 +486,7 @@ public final class CallController: ViewController {
|
||||
var disablePeerIds: [EnginePeer.Id] = []
|
||||
disablePeerIds.append(self.call.context.account.peerId)
|
||||
disablePeerIds.append(self.call.peerId)
|
||||
let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, completion: { [weak self] peers in
|
||||
let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, shareLink: nil, completion: { [weak self] peers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -497,15 +497,18 @@ public final class CallController: ViewController {
|
||||
self.push(controller)
|
||||
}
|
||||
|
||||
static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], completion: @escaping ([(id: EnginePeer.Id, isVideo: Bool)]) -> Void) -> ViewController {
|
||||
static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], shareLink: (() -> Void)?, completion: @escaping ([(id: EnginePeer.Id, isVideo: Bool)]) -> Void) -> ViewController {
|
||||
//TODO:localize
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
|
||||
var options: [ContactListAdditionalOption] = []
|
||||
//TODO:localize
|
||||
options.append(ContactListAdditionalOption(title: "Share Call Link", icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: {
|
||||
//TODO:release
|
||||
}, clearHighlightAutomatically: false))
|
||||
var openShareLinkImpl: (() -> Void)?
|
||||
if shareLink != nil {
|
||||
//TODO:localize
|
||||
options.append(ContactListAdditionalOption(title: "Share Call Link", icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: {
|
||||
openShareLinkImpl?()
|
||||
}, clearHighlightAutomatically: false))
|
||||
}
|
||||
|
||||
let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
|
||||
context: context,
|
||||
@ -534,9 +537,32 @@ public final class CallController: ViewController {
|
||||
default:
|
||||
return .single(false)
|
||||
}
|
||||
},
|
||||
isPeerEnabled: { peer in
|
||||
switch peer {
|
||||
case let .peer(peer, _, _):
|
||||
let peer = EnginePeer(peer)
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
if disablePeerIds.contains(user.id) {
|
||||
return false
|
||||
}
|
||||
if user.botInfo != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
))
|
||||
|
||||
openShareLinkImpl = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
shareLink?()
|
||||
}
|
||||
|
||||
controller.navigationPresentation = .modal
|
||||
let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in
|
||||
guard let result, let peer = result.0.first, case let .peer(peer, _, _) = peer else {
|
||||
|
@ -1026,6 +1026,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
keyPair: keyPair,
|
||||
conferenceSourceId: self.internalId,
|
||||
isConference: true,
|
||||
beginWithVideo: false,
|
||||
sharedAudioContext: self.sharedAudioContext
|
||||
)
|
||||
self.conferenceCallImpl = conferenceCall
|
||||
|
@ -850,6 +850,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
keyPair: nil,
|
||||
conferenceSourceId: nil,
|
||||
isConference: false,
|
||||
beginWithVideo: false,
|
||||
sharedAudioContext: nil
|
||||
)
|
||||
call.schedule(timestamp: timestamp)
|
||||
@ -1076,6 +1077,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
keyPair: nil,
|
||||
conferenceSourceId: nil,
|
||||
isConference: false,
|
||||
beginWithVideo: false,
|
||||
sharedAudioContext: nil
|
||||
)
|
||||
self.updateCurrentGroupCall(.group(call))
|
||||
@ -1085,16 +1087,13 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
accountContext: AccountContext,
|
||||
initialCall: EngineGroupCallDescription,
|
||||
reference: InternalGroupCallReference,
|
||||
mode: JoinConferenceCallMode
|
||||
beginWithVideo: Bool
|
||||
) {
|
||||
let keyPair: TelegramKeyPair
|
||||
switch mode {
|
||||
case .joining:
|
||||
guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else {
|
||||
return
|
||||
}
|
||||
keyPair = keyPairValue
|
||||
guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else {
|
||||
return
|
||||
}
|
||||
keyPair = keyPairValue
|
||||
|
||||
let call = PresentationGroupCallImpl(
|
||||
accountContext: accountContext,
|
||||
@ -1111,6 +1110,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
keyPair: keyPair,
|
||||
conferenceSourceId: nil,
|
||||
isConference: true,
|
||||
beginWithVideo: beginWithVideo,
|
||||
sharedAudioContext: nil
|
||||
)
|
||||
self.updateCurrentGroupCall(.group(call))
|
||||
|
@ -621,54 +621,98 @@ private final class PendingConferenceInvitationContext {
|
||||
case ringing
|
||||
}
|
||||
|
||||
private let callSessionManager: CallSessionManager
|
||||
private let engine: TelegramEngine
|
||||
private var requestDisposable: Disposable?
|
||||
private var stateDisposable: Disposable?
|
||||
private var internalId: CallSessionInternalId?
|
||||
private(set) var messageId: EngineMessage.Id?
|
||||
|
||||
private var hadMessage: Bool = false
|
||||
private var didNotifyEnded: Bool = false
|
||||
|
||||
init(callSessionManager: CallSessionManager, groupCall: GroupCallReference, peerId: PeerId, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) {
|
||||
self.callSessionManager = callSessionManager
|
||||
|
||||
preconditionFailure()
|
||||
|
||||
/*self.requestDisposable = (callSessionManager.request(peerId: peerId, isVideo: false, enableVideo: true, conferenceCall: (groupCall, encryptionKey))
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] internalId in
|
||||
init(engine: TelegramEngine, reference: InternalGroupCallReference, peerId: PeerId, isVideo: Bool, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) {
|
||||
self.engine = engine
|
||||
self.requestDisposable = (engine.calls.inviteConferenceCallParticipant(reference: reference, peerId: peerId, isVideo: isVideo).startStrict(next: { [weak self] messageId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.internalId = internalId
|
||||
guard let messageId else {
|
||||
if !self.didNotifyEnded {
|
||||
self.didNotifyEnded = true
|
||||
onEnded(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.messageId = messageId
|
||||
|
||||
self.stateDisposable = (self.callSessionManager.callState(internalId: internalId)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
|
||||
onStateUpdated(.ringing)
|
||||
|
||||
let timeout: Double = 30.0
|
||||
let timerSignal = Signal<Void, NoError>.single(Void()) |> then(
|
||||
Signal<Void, NoError>.single(Void())
|
||||
|> delay(1.0, queue: .mainQueue())
|
||||
) |> restart
|
||||
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
self.stateDisposable = (combineLatest(queue: .mainQueue(),
|
||||
engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Messages.Message(id: messageId)
|
||||
),
|
||||
timerSignal
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] message, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
switch state.state {
|
||||
case let .requesting(ringing, _):
|
||||
if ringing {
|
||||
onStateUpdated(.ringing)
|
||||
if let message {
|
||||
self.hadMessage = true
|
||||
if message.timestamp + Int32(timeout) <= Int32(Date().timeIntervalSince1970) {
|
||||
if !self.didNotifyEnded {
|
||||
self.didNotifyEnded = true
|
||||
onEnded(false)
|
||||
}
|
||||
} else {
|
||||
var isActive = false
|
||||
var isAccepted = false
|
||||
var foundAction: TelegramMediaAction?
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
foundAction = action
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action {
|
||||
if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil {
|
||||
} else {
|
||||
if conferenceCall.flags.contains(.isActive) {
|
||||
isAccepted = true
|
||||
} else {
|
||||
isActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isActive {
|
||||
if !self.didNotifyEnded {
|
||||
self.didNotifyEnded = true
|
||||
onEnded(isAccepted)
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .dropping(reason), let .terminated(_, reason, _):
|
||||
if !self.didNotifyEnded {
|
||||
self.didNotifyEnded = true
|
||||
onEnded(reason == .ended(.switchedToConference))
|
||||
} else {
|
||||
if self.hadMessage || CFAbsoluteTimeGetCurrent() > startTime + 1.0 {
|
||||
if !self.didNotifyEnded {
|
||||
self.didNotifyEnded = true
|
||||
onEnded(false)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
})*/
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.requestDisposable?.dispose()
|
||||
self.stateDisposable?.dispose()
|
||||
|
||||
if let internalId = self.internalId {
|
||||
self.callSessionManager.drop(internalId: internalId, reason: .hangUp, debugLog: .single(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1121,6 +1165,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
private let sharedAudioContext: SharedCallAudioContext?
|
||||
|
||||
public let isConference: Bool
|
||||
private let beginWithVideo: Bool
|
||||
|
||||
private let conferenceSourceId: CallSessionInternalId?
|
||||
public var conferenceSource: CallSessionInternalId? {
|
||||
@ -1153,6 +1198,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
keyPair: TelegramKeyPair?,
|
||||
conferenceSourceId: CallSessionInternalId?,
|
||||
isConference: Bool,
|
||||
beginWithVideo: Bool,
|
||||
sharedAudioContext: SharedCallAudioContext?
|
||||
) {
|
||||
self.account = accountContext.account
|
||||
@ -1183,6 +1229,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.isStream = isStream
|
||||
self.conferenceSourceId = conferenceSourceId
|
||||
self.isConference = isConference
|
||||
self.beginWithVideo = beginWithVideo
|
||||
self.keyPair = keyPair
|
||||
|
||||
if let keyPair, let initialCall {
|
||||
@ -1490,6 +1537,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
strongSelf.screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: basePath)
|
||||
})*/
|
||||
|
||||
if beginWithVideo {
|
||||
self.requestVideo()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -3844,22 +3895,17 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
return false
|
||||
}
|
||||
|
||||
//TODO:release
|
||||
let _ = self.accountContext.engine.calls.inviteConferenceCallParticipant(callId: initialCall.description.id, accessHash: initialCall.description.accessHash, peerId: peerId, isVideo: isVideo).start()
|
||||
return false
|
||||
/*guard let initialCall = self.initialCall else {
|
||||
return false
|
||||
}
|
||||
if conferenceInvitationContexts[peerId] != nil {
|
||||
if self.conferenceInvitationContexts[peerId] != nil {
|
||||
return false
|
||||
}
|
||||
var onStateUpdated: ((PendingConferenceInvitationContext.State) -> Void)?
|
||||
var onEnded: ((Bool) -> Void)?
|
||||
var didEndAlready = false
|
||||
let invitationContext = PendingConferenceInvitationContext(
|
||||
callSessionManager: self.accountContext.account.callSessionManager,
|
||||
groupCall: GroupCallReference(id: initialCall.id, accessHash: initialCall.accessHash),
|
||||
engine: self.accountContext.engine,
|
||||
reference: initialCall.reference,
|
||||
peerId: peerId,
|
||||
isVideo: isVideo,
|
||||
onStateUpdated: { state in
|
||||
onStateUpdated?(state)
|
||||
},
|
||||
@ -3906,7 +3952,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
}
|
||||
|
||||
return false*/
|
||||
return false
|
||||
} else {
|
||||
guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(where: { $0.id == peerId }) else {
|
||||
return false
|
||||
@ -3933,6 +3979,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
var updatedInvitedPeers = self.invitedPeersValue
|
||||
updatedInvitedPeers.removeAll(where: { $0.id == peerId})
|
||||
self.invitedPeersValue = updatedInvitedPeers
|
||||
|
||||
if let conferenceInvitationContext = self.conferenceInvitationContexts[peerId] {
|
||||
self.conferenceInvitationContexts.removeValue(forKey: peerId)
|
||||
if let messageId = conferenceInvitationContext.messageId {
|
||||
self.accountContext.engine.account.callSessionManager.dropOutgoingConferenceRequest(messageId: messageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateTitle(_ title: String) {
|
||||
|
@ -5,6 +5,179 @@ import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import TelegramPresentationData
|
||||
import CallsEmoji
|
||||
|
||||
private final class EmojiItemComponent: Component {
|
||||
let emoji: String?
|
||||
|
||||
init(emoji: String?) {
|
||||
self.emoji = emoji
|
||||
}
|
||||
|
||||
static func ==(lhs: EmojiItemComponent, rhs: EmojiItemComponent) -> Bool {
|
||||
if lhs.emoji != rhs.emoji {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let measureEmojiView = ComponentView<Empty>()
|
||||
private var pendingContainerView: UIView?
|
||||
private var pendingEmojiViews: [ComponentView<Empty>] = []
|
||||
private var emojiView: ComponentView<Empty>?
|
||||
|
||||
private var component: EmojiItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var pendingEmojiValues: [String]?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
func update(component: EmojiItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = self.measureEmojiView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "👍", font: Font.regular(40.0), textColor: .white))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 200.0)
|
||||
)
|
||||
|
||||
let borderEmoji = 2
|
||||
let numEmoji = borderEmoji * 2 + 3
|
||||
|
||||
if let emoji = component.emoji {
|
||||
let emojiView: ComponentView<Empty>
|
||||
var emojiViewTransition = transition
|
||||
if let current = self.emojiView {
|
||||
emojiView = current
|
||||
} else {
|
||||
emojiViewTransition = .immediate
|
||||
emojiView = ComponentView()
|
||||
self.emojiView = emojiView
|
||||
}
|
||||
let emojiSize = emojiView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: emoji, font: Font.regular(40.0), textColor: .white))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 200.0)
|
||||
)
|
||||
let emojiFrame = CGRect(origin: CGPoint(x: floor((size.width - emojiSize.width) * 0.5), y: floor((size.height - emojiSize.height) * 0.5)), size: emojiSize)
|
||||
if let emojiComponentView = emojiView.view {
|
||||
if emojiComponentView.superview == nil {
|
||||
self.addSubview(emojiComponentView)
|
||||
}
|
||||
emojiViewTransition.setFrame(view: emojiComponentView, frame: emojiFrame)
|
||||
}
|
||||
|
||||
self.pendingEmojiValues = nil
|
||||
} else {
|
||||
if let emojiView = self.emojiView {
|
||||
self.emojiView = nil
|
||||
emojiView.view?.removeFromSuperview()
|
||||
}
|
||||
|
||||
if self.pendingEmojiValues?.count != numEmoji {
|
||||
var pendingEmojiValuesValue: [String] = []
|
||||
for _ in 0 ..< numEmoji - borderEmoji - 1 {
|
||||
pendingEmojiValuesValue.append(randomCallsEmoji() ?? "👍")
|
||||
}
|
||||
for i in 0 ..< borderEmoji + 1 {
|
||||
pendingEmojiValuesValue.append(pendingEmojiValuesValue[i])
|
||||
}
|
||||
self.pendingEmojiValues = pendingEmojiValuesValue
|
||||
}
|
||||
}
|
||||
|
||||
if let pendingEmojiValues, pendingEmojiValues.count == numEmoji {
|
||||
let pendingContainerView: UIView
|
||||
if let current = self.pendingContainerView {
|
||||
pendingContainerView = current
|
||||
} else {
|
||||
pendingContainerView = UIView()
|
||||
self.pendingContainerView = pendingContainerView
|
||||
}
|
||||
|
||||
for i in 0 ..< numEmoji {
|
||||
let pendingEmojiView: ComponentView<Empty>
|
||||
if self.pendingEmojiViews.count > i {
|
||||
pendingEmojiView = self.pendingEmojiViews[i]
|
||||
} else {
|
||||
pendingEmojiView = ComponentView()
|
||||
self.pendingEmojiViews.append(pendingEmojiView)
|
||||
}
|
||||
let pendingEmojiViewSize = pendingEmojiView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: pendingEmojiValues[i], font: Font.regular(40.0), textColor: .white))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 200.0)
|
||||
)
|
||||
if let pendingEmojiComponentView = pendingEmojiView.view {
|
||||
if pendingEmojiComponentView.superview == nil {
|
||||
pendingContainerView.addSubview(pendingEmojiComponentView)
|
||||
}
|
||||
pendingEmojiComponentView.frame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(i) * size.height), size: pendingEmojiViewSize)
|
||||
}
|
||||
}
|
||||
|
||||
pendingContainerView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
if pendingContainerView.superview == nil {
|
||||
self.addSubview(pendingContainerView)
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "sublayerTransform.translation.y")
|
||||
//animation.duration = 4.2
|
||||
animation.duration = 0.2
|
||||
animation.fromValue = -CGFloat(numEmoji - borderEmoji) * size.height
|
||||
animation.toValue = CGFloat(borderEmoji - 3) * size.height
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||
animation.autoreverses = false
|
||||
animation.repeatCount = .infinity
|
||||
|
||||
pendingContainerView.layer.add(animation, forKey: "offsetCycle")
|
||||
}
|
||||
} else if let pendingContainerView = self.pendingContainerView {
|
||||
self.pendingContainerView = nil
|
||||
pendingContainerView.removeFromSuperview()
|
||||
|
||||
for emojiView in self.pendingEmojiViews {
|
||||
emojiView.view?.removeFromSuperview()
|
||||
}
|
||||
self.pendingEmojiViews.removeAll()
|
||||
}
|
||||
|
||||
//self.layer.borderColor = UIColor.red.cgColor
|
||||
//self.layer.borderWidth = 4.0
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class VideoChatEncryptionKeyComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
@ -119,7 +292,7 @@ final class VideoChatEncryptionKeyComponent: Component {
|
||||
let expandedButtonTopInset: CGFloat = 12.0
|
||||
let expandedButtonBottomInset: CGFloat = 13.0
|
||||
|
||||
let emojiItemSizes = (0 ..< component.emoji.count).map { i -> CGSize in
|
||||
let emojiItemSizes = (0 ..< 4).map { i -> CGSize in
|
||||
let emojiItem: ComponentView<Empty>
|
||||
if self.emojiItems.count > i {
|
||||
emojiItem = self.emojiItems[i]
|
||||
@ -128,9 +301,9 @@ final class VideoChatEncryptionKeyComponent: Component {
|
||||
self.emojiItems.append(emojiItem)
|
||||
}
|
||||
return emojiItem.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.emoji[i], font: Font.regular(40.0), textColor: .white))
|
||||
transition: transition,
|
||||
component: AnyComponent(EmojiItemComponent(
|
||||
emoji: i < component.emoji.count ? component.emoji[i] : nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 200.0)
|
||||
|
@ -7,16 +7,24 @@ import TelegramPresentationData
|
||||
import BundleIconComponent
|
||||
|
||||
final class VideoChatListInviteComponent: Component {
|
||||
enum Icon {
|
||||
case addUser
|
||||
case link
|
||||
}
|
||||
|
||||
let title: String
|
||||
let icon: Icon
|
||||
let theme: PresentationTheme
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
title: String,
|
||||
icon: Icon,
|
||||
theme: PresentationTheme,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
self.theme = theme
|
||||
self.action = action
|
||||
}
|
||||
@ -25,6 +33,9 @@ final class VideoChatListInviteComponent: Component {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.icon != rhs.icon {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
@ -116,10 +127,17 @@ final class VideoChatListInviteComponent: Component {
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
}
|
||||
|
||||
let iconName: String
|
||||
switch component.icon {
|
||||
case .addUser:
|
||||
iconName = "Chat/Context Menu/AddUser"
|
||||
case .link:
|
||||
iconName = "Chat/Context Menu/Link"
|
||||
}
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Context Menu/AddUser",
|
||||
name: iconName,
|
||||
tintColor: component.theme.list.itemAccentColor
|
||||
)),
|
||||
environment: {},
|
||||
|
@ -39,23 +39,33 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
|
||||
final class Participants: Equatable {
|
||||
enum InviteType {
|
||||
case invite
|
||||
enum InviteType: Equatable {
|
||||
case invite(isMultipleUsers: Bool)
|
||||
case shareLink
|
||||
}
|
||||
|
||||
struct InviteOption: Equatable {
|
||||
let id: Int
|
||||
let type: InviteType
|
||||
|
||||
init(id: Int, type: InviteType) {
|
||||
self.id = id
|
||||
self.type = type
|
||||
}
|
||||
}
|
||||
|
||||
let myPeerId: EnginePeer.Id
|
||||
let participants: [GroupCallParticipantsContext.Participant]
|
||||
let totalCount: Int
|
||||
let loadMoreToken: String?
|
||||
let inviteType: InviteType?
|
||||
let inviteOptions: [InviteOption]
|
||||
|
||||
init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteType: InviteType?) {
|
||||
init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteOptions: [InviteOption]) {
|
||||
self.myPeerId = myPeerId
|
||||
self.participants = participants
|
||||
self.totalCount = totalCount
|
||||
self.loadMoreToken = loadMoreToken
|
||||
self.inviteType = inviteType
|
||||
self.inviteOptions = inviteOptions
|
||||
}
|
||||
|
||||
static func ==(lhs: Participants, rhs: Participants) -> Bool {
|
||||
@ -74,7 +84,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
if lhs.loadMoreToken != rhs.loadMoreToken {
|
||||
return false
|
||||
}
|
||||
if lhs.inviteType != rhs.inviteType {
|
||||
if lhs.inviteOptions != rhs.inviteOptions {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -142,7 +152,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void
|
||||
let updateIsMainParticipantPinned: (Bool) -> Void
|
||||
let updateIsExpandedUIHidden: (Bool) -> Void
|
||||
let openInviteMembers: () -> Void
|
||||
let openInviteMembers: (Participants.InviteType) -> Void
|
||||
let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void
|
||||
|
||||
init(
|
||||
@ -162,7 +172,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
|
||||
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
|
||||
updateIsExpandedUIHidden: @escaping (Bool) -> Void,
|
||||
openInviteMembers: @escaping () -> Void,
|
||||
openInviteMembers: @escaping (Participants.InviteType) -> Void,
|
||||
visibleParticipantsUpdated: @escaping (Set<EnginePeer.Id>) -> Void
|
||||
) {
|
||||
self.call = call
|
||||
@ -379,14 +389,14 @@ final class VideoChatParticipantsComponent: Component {
|
||||
let sideInset: CGFloat
|
||||
let itemCount: Int
|
||||
let itemHeight: CGFloat
|
||||
let trailingItemHeight: CGFloat
|
||||
let trailingItemHeights: [CGFloat]
|
||||
|
||||
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeight: CGFloat) {
|
||||
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeights: [CGFloat]) {
|
||||
self.containerSize = containerSize
|
||||
self.sideInset = sideInset
|
||||
self.itemCount = itemCount
|
||||
self.itemHeight = itemHeight
|
||||
self.trailingItemHeight = trailingItemHeight
|
||||
self.trailingItemHeights = trailingItemHeights
|
||||
}
|
||||
|
||||
func frame(at index: Int) -> CGRect {
|
||||
@ -394,8 +404,15 @@ final class VideoChatParticipantsComponent: Component {
|
||||
return frame
|
||||
}
|
||||
|
||||
func trailingItemFrame() -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeight))
|
||||
func trailingItemFrame(index: Int) -> CGRect {
|
||||
if index < 0 || index >= self.trailingItemHeights.count {
|
||||
return CGRect()
|
||||
}
|
||||
var prefixHeight: CGFloat = 0.0
|
||||
for i in 0 ..< index {
|
||||
prefixHeight += self.trailingItemHeights[i]
|
||||
}
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight + prefixHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeights[index]))
|
||||
}
|
||||
|
||||
func contentHeight() -> CGFloat {
|
||||
@ -403,7 +420,9 @@ final class VideoChatParticipantsComponent: Component {
|
||||
if self.itemCount != 0 {
|
||||
result = self.frame(at: self.itemCount - 1).maxY
|
||||
}
|
||||
result += self.trailingItemHeight
|
||||
for height in self.trailingItemHeights {
|
||||
result += height
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -439,7 +458,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
let scrollClippingFrame: CGRect
|
||||
let separateVideoScrollClippingFrame: CGRect
|
||||
|
||||
init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
|
||||
init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeights: [CGFloat]) {
|
||||
self.containerSize = containerSize
|
||||
self.layout = layout
|
||||
self.isUIHidden = isUIHidden
|
||||
@ -465,7 +484,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
|
||||
self.grid = Grid(containerSize: CGSize(width: gridWidth, height: gridContainerHeight), sideInset: gridSideInset, itemCount: gridItemCount, isDedicatedColumn: layout.videoColumn != nil)
|
||||
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
|
||||
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeights: listTrailingItemHeights)
|
||||
self.spacing = 4.0
|
||||
|
||||
if let videoColumn = layout.videoColumn, !isUIHidden && !layout.isMainColumnHidden {
|
||||
@ -568,8 +587,8 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func listTrailingItemFrame() -> CGRect {
|
||||
return self.list.trailingItemFrame()
|
||||
func listTrailingItemFrame(index: Int) -> CGRect {
|
||||
return self.list.trailingItemFrame(index: index)
|
||||
}
|
||||
}
|
||||
|
||||
@ -641,7 +660,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
private var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||
|
||||
private let measureListItemView = ComponentView<Empty>()
|
||||
private let inviteListItemView = ComponentView<Empty>()
|
||||
private var inviteListItemViews: [Int: ComponentView<Empty>] = [:]
|
||||
|
||||
private var gridItemViews: [VideoParticipantKey: GridItem] = [:]
|
||||
private let gridItemViewContainer: UIView
|
||||
@ -1270,7 +1289,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
case .requesting:
|
||||
subtitle = PeerListItemComponent.Subtitle(text: "requesting...", color: .neutral)
|
||||
case .ringing:
|
||||
subtitle = PeerListItemComponent.Subtitle(text: "ringing...", color: .neutral)
|
||||
subtitle = PeerListItemComponent.Subtitle(text: "invited", color: .neutral)
|
||||
}
|
||||
|
||||
peerItemComponent = PeerListItemComponent(
|
||||
@ -1381,11 +1400,15 @@ final class VideoChatParticipantsComponent: Component {
|
||||
self.listItemViews.removeValue(forKey: itemId)
|
||||
}
|
||||
|
||||
do {
|
||||
var trailingItemIndex = 0
|
||||
for inviteOption in component.participants?.inviteOptions ?? [] {
|
||||
guard let itemView = self.inviteListItemViews[inviteOption.id] else {
|
||||
continue
|
||||
}
|
||||
var itemTransition = transition
|
||||
let itemView = self.inviteListItemView
|
||||
|
||||
let itemFrame = itemLayout.listTrailingItemFrame()
|
||||
let itemFrame = itemLayout.listTrailingItemFrame(index: trailingItemIndex)
|
||||
trailingItemIndex += 1
|
||||
|
||||
if let itemComponentView = itemView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
@ -1395,6 +1418,17 @@ final class VideoChatParticipantsComponent: Component {
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
var removeInviteListItemIds: [Int] = []
|
||||
for (id, itemView) in self.inviteListItemViews {
|
||||
if let participants = component.participants, participants.inviteOptions.contains(where: { $0.id == id }) {
|
||||
} else {
|
||||
removeInviteListItemIds.append(id)
|
||||
itemView.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
for id in removeInviteListItemIds {
|
||||
self.inviteListItemViews.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
transition.setScale(view: self.gridItemViewContainer, scale: gridIsEmpty ? 0.001 : 1.0)
|
||||
transition.setPosition(view: self.gridItemViewContainer, position: CGPoint(x: itemLayout.gridItemContainerFrame().midX, y: itemLayout.gridItemContainerFrame().minY))
|
||||
@ -1748,32 +1782,51 @@ final class VideoChatParticipantsComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
|
||||
let inviteText: String
|
||||
if let participants = component.participants, let inviteType = participants.inviteType {
|
||||
switch inviteType {
|
||||
case .invite:
|
||||
inviteText = component.strings.VoiceChat_InviteMember
|
||||
var inviteListItemSizes: [CGSize] = []
|
||||
for (inviteOption) in component.participants?.inviteOptions ?? [] {
|
||||
let inviteText: String
|
||||
let iconType: VideoChatListInviteComponent.Icon
|
||||
switch inviteOption.type {
|
||||
case let .invite(isMultiple):
|
||||
//TODO:localize
|
||||
if isMultiple {
|
||||
inviteText = component.strings.VoiceChat_InviteMember
|
||||
} else {
|
||||
inviteText = "Add Member"
|
||||
}
|
||||
iconType = .addUser
|
||||
case .shareLink:
|
||||
inviteText = component.strings.VoiceChat_Share
|
||||
iconType = .link
|
||||
}
|
||||
} else {
|
||||
inviteText = component.strings.VoiceChat_InviteMember
|
||||
}
|
||||
let inviteListItemSize = self.inviteListItemView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(VideoChatListInviteComponent(
|
||||
title: inviteText,
|
||||
theme: component.theme,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
|
||||
let inviteListItemView: ComponentView<Empty>
|
||||
var inviteListItemTransition = transition
|
||||
if let current = self.inviteListItemViews[inviteOption.id] {
|
||||
inviteListItemView = current
|
||||
} else {
|
||||
inviteListItemView = ComponentView()
|
||||
self.inviteListItemViews[inviteOption.id] = inviteListItemView
|
||||
inviteListItemTransition = inviteListItemTransition.withAnimation(.none)
|
||||
}
|
||||
|
||||
inviteListItemSizes.append(inviteListItemView.update(
|
||||
transition: inviteListItemTransition,
|
||||
component: AnyComponent(VideoChatListInviteComponent(
|
||||
title: inviteText,
|
||||
icon: iconType,
|
||||
theme: component.theme,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.openInviteMembers(inviteOption.type)
|
||||
}
|
||||
component.openInviteMembers()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
))
|
||||
}
|
||||
|
||||
var gridParticipants: [VideoParticipant] = []
|
||||
var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||
@ -1824,7 +1877,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
gridItemCount: gridParticipants.count,
|
||||
listItemCount: listParticipants.count + component.invitedPeers.count,
|
||||
listItemHeight: measureListItemSize.height,
|
||||
listTrailingItemHeight: inviteListItemSize.height
|
||||
listTrailingItemHeights: inviteListItemSizes.map(\.height)
|
||||
)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
|
@ -1759,12 +1759,19 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
var inviteType: VideoChatParticipantsComponent.Participants.InviteType?
|
||||
if canInvite {
|
||||
if inviteIsLink {
|
||||
inviteType = .shareLink
|
||||
} else {
|
||||
inviteType = .invite
|
||||
var inviteOptions: [VideoChatParticipantsComponent.Participants.InviteOption] = []
|
||||
if case let .group(groupCall) = self.currentCall, groupCall.isConference {
|
||||
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: .invite(isMultipleUsers: false)))
|
||||
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 1, type: .shareLink))
|
||||
} else {
|
||||
if canInvite {
|
||||
let inviteType: VideoChatParticipantsComponent.Participants.InviteType
|
||||
if inviteIsLink {
|
||||
inviteType = .shareLink
|
||||
} else {
|
||||
inviteType = .invite(isMultipleUsers: false)
|
||||
}
|
||||
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: inviteType))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1773,7 +1780,7 @@ final class VideoChatScreenComponent: Component {
|
||||
participants: members.participants,
|
||||
totalCount: members.totalCount,
|
||||
loadMoreToken: members.loadMoreToken,
|
||||
inviteType: inviteType
|
||||
inviteOptions: inviteOptions
|
||||
)
|
||||
}
|
||||
|
||||
@ -2038,7 +2045,13 @@ final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
|
||||
var encryptionKeyFrame: CGRect?
|
||||
if let encryptionKeyEmoji = self.encryptionKeyEmoji {
|
||||
var isConference = false
|
||||
if case let .group(groupCall) = self.currentCall {
|
||||
isConference = groupCall.isConference
|
||||
} else if case .conferenceSource = self.currentCall {
|
||||
isConference = true
|
||||
}
|
||||
if isConference {
|
||||
navigationHeight -= 2.0
|
||||
let encryptionKey: ComponentView<Empty>
|
||||
var encryptionKeyTransition = transition
|
||||
@ -2055,7 +2068,7 @@ final class VideoChatScreenComponent: Component {
|
||||
component: AnyComponent(VideoChatEncryptionKeyComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
emoji: encryptionKeyEmoji,
|
||||
emoji: self.encryptionKeyEmoji ?? [],
|
||||
isExpanded: self.isEncryptionKeyExpanded,
|
||||
tapAction: { [weak self] in
|
||||
guard let self else {
|
||||
@ -2326,11 +2339,18 @@ final class VideoChatScreenComponent: Component {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
},
|
||||
openInviteMembers: { [weak self] in
|
||||
openInviteMembers: { [weak self] type in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openInviteMembers()
|
||||
if case .shareLink = type {
|
||||
guard let inviteLinks = self.inviteLinks else {
|
||||
return
|
||||
}
|
||||
self.presentShare(inviteLinks)
|
||||
} else {
|
||||
self.openInviteMembers()
|
||||
}
|
||||
},
|
||||
visibleParticipantsUpdated: { [weak self] visibleParticipants in
|
||||
guard let self else {
|
||||
|
@ -51,7 +51,15 @@ extension VideoChatScreenComponent.View {
|
||||
}
|
||||
}
|
||||
}
|
||||
let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in
|
||||
let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, shareLink: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let inviteLinks = self.inviteLinks else {
|
||||
return
|
||||
}
|
||||
self.presentShare(inviteLinks)
|
||||
}, completion: { [weak self] peerIds in
|
||||
guard let self, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
@ -80,7 +88,7 @@ extension VideoChatScreenComponent.View {
|
||||
if inviteIsLink {
|
||||
inviteType = .shareLink
|
||||
} else {
|
||||
inviteType = .invite
|
||||
inviteType = .invite(isMultipleUsers: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -718,6 +718,23 @@ private final class CallSessionManagerContext {
|
||||
}
|
||||
}
|
||||
|
||||
func dropOutgoingConferenceRequest(messageId: MessageId) {
|
||||
let addUpdates = self.addUpdates
|
||||
let rejectSignal = self.network.request(Api.functions.phone.declineConferenceCallInvite(msgId: messageId.id))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<Never, NoError> in
|
||||
if let updates {
|
||||
addUpdates(updates)
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
||||
self.rejectConferenceInvitationDisposables.add(rejectSignal.startStrict())
|
||||
}
|
||||
|
||||
func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal<String?, NoError>) {
|
||||
for (id, context) in self.incomingConferenceInvitationContexts {
|
||||
if context.internalId == internalId {
|
||||
@ -1383,6 +1400,12 @@ public final class CallSessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
public func dropOutgoingConferenceRequest(messageId: MessageId) {
|
||||
self.withContext { context in
|
||||
context.dropOutgoingConferenceRequest(messageId: messageId)
|
||||
}
|
||||
}
|
||||
|
||||
func drop(stableId: CallSessionStableId, reason: DropCallReason) {
|
||||
self.withContext { context in
|
||||
context.drop(stableId: stableId, reason: reason)
|
||||
|
@ -831,11 +831,11 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?,
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64, accessHash: Int64, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<Never, NoError> {
|
||||
func _internal_inviteConferenceCallParticipant(account: Account, reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<MessageId?, NoError> {
|
||||
return account.postbox.transaction { transaction -> Api.InputUser? in
|
||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
}
|
||||
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
|
||||
|> mapToSignal { inputPeer -> Signal<MessageId?, NoError> in
|
||||
guard let inputPeer else {
|
||||
return .complete()
|
||||
}
|
||||
@ -844,16 +844,19 @@ func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64,
|
||||
if isVideo {
|
||||
flags |= 1 << 0
|
||||
}
|
||||
return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(flags: flags, call: .inputGroupCall(id: callId, accessHash: accessHash), userId: inputPeer))
|
||||
return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(flags: flags, call: reference.apiInputGroupCall, userId: inputPeer))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
||||
|> mapToSignal { result -> Signal<MessageId?, NoError> in
|
||||
if let result {
|
||||
account.stateManager.addUpdates(result)
|
||||
if let message = result.messageIds.first {
|
||||
return .single(message)
|
||||
}
|
||||
}
|
||||
return .complete()
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -105,8 +105,8 @@ public extension TelegramEngine {
|
||||
return _internal_sendConferenceCallBroadcast(account: self.account, callId: callId, accessHash: accessHash, block: block)
|
||||
}
|
||||
|
||||
public func inviteConferenceCallParticipant(callId: Int64, accessHash: Int64, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<Never, NoError> {
|
||||
return _internal_inviteConferenceCallParticipant(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId, isVideo: isVideo)
|
||||
public func inviteConferenceCallParticipant(reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<EngineMessage.Id?, NoError> {
|
||||
return _internal_inviteConferenceCallParticipant(account: self.account, reference: reference, peerId: peerId, isVideo: isVideo)
|
||||
}
|
||||
|
||||
public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal<RemoveGroupCallBlockchainParticipantsResult, NoError> {
|
||||
|
@ -10,6 +10,7 @@ swift_library(
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
|
@ -9,6 +9,7 @@ import AppBundle
|
||||
import ChatMessageBubbleContentNode
|
||||
import ChatMessageItemCommon
|
||||
import ChatMessageDateAndStatusNode
|
||||
import SwiftSignalKit
|
||||
|
||||
private let titleFont: UIFont = Font.medium(16.0)
|
||||
private let labelFont: UIFont = Font.regular(13.0)
|
||||
@ -25,6 +26,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let iconNode: ASImageNode
|
||||
private let buttonNode: HighlightableButtonNode
|
||||
|
||||
private var activeConferenceUpdateTimer: SwiftSignalKit.Timer?
|
||||
|
||||
required public init() {
|
||||
self.titleNode = TextNode()
|
||||
self.labelNode = TextNode()
|
||||
@ -57,6 +60,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.activeConferenceUpdateTimer?.invalidate()
|
||||
}
|
||||
|
||||
override public func accessibilityActivate() -> Bool {
|
||||
self.callButtonPressed()
|
||||
return true
|
||||
@ -90,6 +97,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var callDuration: Int32?
|
||||
var callSuccessful = true
|
||||
var isVideo = false
|
||||
var hasCallButton = true
|
||||
var updateConferenceTimerEndTimeout: Int32?
|
||||
for media in item.message.media {
|
||||
if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action {
|
||||
isVideo = isVideoValue
|
||||
@ -124,10 +133,31 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
break
|
||||
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
|
||||
isVideo = false
|
||||
isVideo = conferenceCall.flags.contains(.isVideo)
|
||||
callDuration = conferenceCall.duration
|
||||
//TODO:localize
|
||||
titleString = "Group Call"
|
||||
let missedTimeout: Int32
|
||||
#if DEBUG
|
||||
missedTimeout = 5
|
||||
#else
|
||||
missedTimeout = 30
|
||||
#endif
|
||||
let currentTime = Int32(Date().timeIntervalSince1970)
|
||||
if conferenceCall.flags.contains(.isMissed) {
|
||||
titleString = "Declined Group Call"
|
||||
} else if item.message.timestamp < currentTime - missedTimeout {
|
||||
titleString = "Missed Group Call"
|
||||
} else if conferenceCall.duration != nil {
|
||||
titleString = "Cancelled Group Call"
|
||||
hasCallButton = true
|
||||
} else {
|
||||
if incoming {
|
||||
titleString = "Incoming Group Call"
|
||||
} else {
|
||||
titleString = "Outgoing Group Call"
|
||||
}
|
||||
updateConferenceTimerEndTimeout = (item.message.timestamp + missedTimeout) - currentTime
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -211,7 +241,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
||||
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
|
||||
|
||||
boundingSize.width += 54.0
|
||||
if hasCallButton {
|
||||
boundingSize.width += 54.0
|
||||
}
|
||||
|
||||
return (boundingSize.width, { boundingWidth in
|
||||
return (boundingSize, { [weak self] animation, _, _ in
|
||||
@ -234,6 +266,22 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if let buttonImage = buttonImage {
|
||||
strongSelf.buttonNode.setImage(buttonImage, for: [])
|
||||
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: boundingWidth - buttonImage.size.width - 8.0, y: 15.0), size: buttonImage.size)
|
||||
strongSelf.buttonNode.isHidden = !hasCallButton
|
||||
}
|
||||
|
||||
if let activeConferenceUpdateTimer = strongSelf.activeConferenceUpdateTimer {
|
||||
activeConferenceUpdateTimer.invalidate()
|
||||
strongSelf.activeConferenceUpdateTimer = nil
|
||||
}
|
||||
if let updateConferenceTimerEndTimeout, updateConferenceTimerEndTimeout >= 0 {
|
||||
strongSelf.activeConferenceUpdateTimer?.invalidate()
|
||||
strongSelf.activeConferenceUpdateTimer = SwiftSignalKit.Timer(timeout: Double(updateConferenceTimerEndTimeout) + 0.5, repeat: false, completion: { [weak strongSelf] in
|
||||
guard let strongSelf else {
|
||||
return
|
||||
}
|
||||
strongSelf.requestInlineUpdate?()
|
||||
}, queue: .mainQueue())
|
||||
strongSelf.activeConferenceUpdateTimer?.start()
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -270,6 +318,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||
if self.buttonNode.isHidden {
|
||||
return ChatMessageBubbleContentTapAction(content: .none)
|
||||
}
|
||||
|
||||
if self.buttonNode.frame.contains(point) {
|
||||
return ChatMessageBubbleContentTapAction(content: .ignore)
|
||||
} else if self.bounds.contains(point), let item = self.item {
|
||||
|
@ -405,7 +405,7 @@ private final class JoinSubjectScreenComponent: Component {
|
||||
isStream: false
|
||||
),
|
||||
reference: .link(slug: groupCall.slug),
|
||||
mode: .joining
|
||||
beginWithVideo: false
|
||||
)
|
||||
|
||||
self.environment?.controller()?.dismiss()
|
||||
|
@ -2908,7 +2908,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
isStream: false
|
||||
),
|
||||
reference: .message(id: message.id),
|
||||
mode: .joining
|
||||
beginWithVideo: conferenceCall.flags.contains(.isVideo)
|
||||
)
|
||||
})
|
||||
}, longTap: { [weak self] action, params in
|
||||
|
@ -60,6 +60,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
||||
}
|
||||
|
||||
private let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
|
||||
private let isPeerEnabled: (ContactListPeer) -> Bool
|
||||
var dismissed: (() -> Void)?
|
||||
|
||||
var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void = { _ in }
|
||||
@ -107,6 +108,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
||||
self.displayDeviceContacts = params.displayDeviceContacts
|
||||
self.displayCallIcons = params.displayCallIcons
|
||||
self.confirmation = params.confirmation
|
||||
self.isPeerEnabled = params.isPeerEnabled
|
||||
self.multipleSelection = params.multipleSelection
|
||||
self.requirePhoneNumbers = params.requirePhoneNumbers
|
||||
self.allowChannelsInSearch = params.allowChannelsInSearch
|
||||
@ -218,7 +220,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers, allowChannelsInSearch: self.allowChannelsInSearch)
|
||||
self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers, allowChannelsInSearch: self.allowChannelsInSearch, isPeerEnabled: self.isPeerEnabled)
|
||||
self._ready.set(self.contactsNode.contactListNode.ready)
|
||||
|
||||
self.contactsNode.navigationBar = self.navigationBar
|
||||
|
@ -44,6 +44,8 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
||||
var cancelSearch: (() -> Void)?
|
||||
var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)?
|
||||
|
||||
let isPeerEnabled: (ContactListPeer) -> Bool
|
||||
|
||||
var presentationData: PresentationData {
|
||||
didSet {
|
||||
self.presentationDataPromise.set(.single(self.presentationData))
|
||||
@ -57,12 +59,13 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
||||
|
||||
var searchContainerNode: ContactsSearchContainerNode?
|
||||
|
||||
init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool) {
|
||||
init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool, isPeerEnabled: @escaping (ContactListPeer) -> Bool) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.displayDeviceContacts = displayDeviceContacts
|
||||
self.displayCallIcons = displayCallIcons
|
||||
self.allowChannelsInSearch = allowChannelsInSearch
|
||||
self.isPeerEnabled = isPeerEnabled
|
||||
|
||||
var excludeSelf = true
|
||||
|
||||
@ -124,7 +127,9 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
|
||||
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
|
||||
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, isPeerEnabled: { peer in
|
||||
return isPeerEnabled(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil))
|
||||
}, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
|
||||
contextActionImpl?(peer, node, gesture, nil)
|
||||
} : nil, multipleSelection: multipleSelection)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user