diff --git a/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift index 8e089cec0f..e84a5aac38 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift @@ -15,17 +15,20 @@ final class VideoChatListInviteComponent: Component { let title: String let icon: Icon let theme: PresentationTheme + let hasNext: Bool let action: () -> Void init( title: String, icon: Icon, theme: PresentationTheme, + hasNext: Bool, action: @escaping () -> Void ) { self.title = title self.icon = icon self.theme = theme + self.hasNext = hasNext self.action = action } @@ -39,6 +42,9 @@ final class VideoChatListInviteComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.hasNext != rhs.hasNext { + return false + } return true } @@ -52,7 +58,11 @@ final class VideoChatListInviteComponent: Component { private var highlightBackgroundLayer: SimpleLayer? private var highlightBackgroundFrame: CGRect? + private let separatorLayer: SimpleLayer + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + super.init(frame: frame) self.highligthedChanged = { [weak self] isHighlighted in @@ -74,6 +84,15 @@ final class VideoChatListInviteComponent: Component { } highlightBackgroundLayer.frame = highlightBackgroundFrame highlightBackgroundLayer.opacity = 1.0 + if component.hasNext { + highlightBackgroundLayer.maskedCorners = [] + highlightBackgroundLayer.masksToBounds = false + highlightBackgroundLayer.cornerRadius = 0.0 + } else { + highlightBackgroundLayer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + highlightBackgroundLayer.masksToBounds = true + highlightBackgroundLayer.cornerRadius = 10.0 + } } else { if let highlightBackgroundLayer = self.highlightBackgroundLayer { self.highlightBackgroundLayer = nil @@ -152,7 +171,14 @@ final class VideoChatListInviteComponent: Component { transition.setFrame(view: iconView, frame: iconFrame) } - //self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: size) + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: size) + + if self.separatorLayer.superlayer == nil { + self.layer.addSublayer(self.separatorLayer) + } + self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 62.0, y: size.height), size: CGSize(width: size.width - 62.0, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext return size } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index a8ce722e88..242d67f9ea 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -1760,74 +1760,6 @@ final class VideoChatParticipantsComponent: Component { } } - let measureListItemSize = self.measureListItemView.update( - transition: .immediate, - component: AnyComponent(PeerListItemComponent( - context: component.call.accountContext, - theme: component.theme, - strings: component.strings, - style: .generic, - sideInset: 0.0, - title: "AAA", - peer: nil, - subtitle: PeerListItemComponent.Subtitle(text: "bbb", color: .neutral), - subtitleAccessory: .none, - presence: nil, - selectionState: .none, - hasNext: true, - action: { _, _, _ in - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 1000.0) - ) - - 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 - } - - let inviteListItemView: ComponentView - 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) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 1000.0) - )) - } - var gridParticipants: [VideoParticipant] = [] var listParticipants: [GroupCallParticipantsContext.Participant] = [] if let participants = component.participants { @@ -1868,6 +1800,90 @@ final class VideoChatParticipantsComponent: Component { self.gridParticipants = gridParticipants self.listParticipants = listParticipants + let measureListItemSize = self.measureListItemView.update( + transition: .immediate, + component: AnyComponent(PeerListItemComponent( + context: component.call.accountContext, + theme: component.theme, + strings: component.strings, + style: .generic, + sideInset: 0.0, + title: "AAA", + peer: nil, + subtitle: PeerListItemComponent.Subtitle(text: "bbb", color: .neutral), + subtitleAccessory: .none, + presence: nil, + selectionState: .none, + hasNext: true, + action: { _, _, _ in + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + + var inviteListItemSizes: [CGSize] = [] + if let participants = component.participants { + let tempItemLayout = ItemLayout( + containerSize: availableSize, + layout: component.layout, + isUIHidden: component.expandedVideoState?.isUIHidden ?? false, + expandedInsets: component.expandedInsets, + safeInsets: component.safeInsets, + gridItemCount: gridParticipants.count, + listItemCount: listParticipants.count + component.invitedPeers.count, + listItemHeight: measureListItemSize.height, + listTrailingItemHeights: [] + ) + + for i in 0 ..< participants.inviteOptions.count { + let inviteOption = participants.inviteOptions[i] + 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 + } + + let inviteListItemView: ComponentView + 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, + hasNext: i != participants.inviteOptions.count - 1, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.openInviteMembers(inviteOption.type) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - tempItemLayout.list.sideInset * 2.0, height: 1000.0) + )) + } + } + let itemLayout = ItemLayout( containerSize: availableSize, layout: component.layout, diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 8d2bcdfb15..9923e728e1 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -1301,6 +1301,70 @@ public final class AccountViewTracker { } } + public func refreshInlineGroupCallsForMessageIds(messageIds: Set) { + self.queue.async { + var addedMessageIds: [MessageId] = [] + let timestamp = Int32(CFAbsoluteTimeGetCurrent()) + for messageId in messageIds { + let messageTimestamp = self.updatedUnsupportedMediaMessageIdsAndTimestamps[MessageAndThreadId(messageId: messageId, threadId: nil)] + var refresh = false + if let messageTimestamp = messageTimestamp { + refresh = messageTimestamp < timestamp - 60 + } else { + refresh = true + } + + if refresh { + self.updatedUnsupportedMediaMessageIdsAndTimestamps[MessageAndThreadId(messageId: messageId, threadId: nil)] = timestamp + addedMessageIds.append(messageId) + } + } + if !addedMessageIds.isEmpty { + for (_, messageIds) in messagesIdsGroupedByPeerId(Set(addedMessageIds)) { + let disposableId = self.nextUpdatedUnsupportedMediaDisposableId + self.nextUpdatedUnsupportedMediaDisposableId += 1 + + if let account = self.account { + let signal = account.postbox.transaction { transaction -> [MessageId] in + var result: [MessageId] = [] + for id in messageIds { + if let message = transaction.getMessage(id) { + for media in message.media { + if let _ = media as? TelegramMediaAction { + result.append(id) + break + } + } + } + } + return result + } + |> mapToSignal { ids -> Signal in + guard !ids.isEmpty else { + return .complete() + } + + var requests: [Signal] = [] + + for id in ids { + requests.append(_internal_refreshInlineGroupCall(account: account, messageId: id)) + } + + return combineLatest(requests) + |> ignoreValues + } + |> afterDisposed { [weak self] in + self?.queue.async { + self?.updatedUnsupportedMediaDisposables.set(nil, forKey: disposableId) + } + } + self.updatedUnsupportedMediaDisposables.set(signal.start(), forKey: disposableId) + } + } + } + } + } + public func refreshStoryStatsForPeerIds(peerIds: [PeerId]) { self.queue.async { self.pendingRefreshStoriesForPeerIds.append(contentsOf: peerIds) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index b4df402821..af3a013edb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -241,6 +241,53 @@ func _internal_getCurrentGroupCall(account: Account, reference: InternalGroupCal } } +func _internal_getCurrentGroupCallInfo(account: Account, reference: InternalGroupCallReference) -> Signal<(participants: [PeerId], duration: Int32?)?, NoError> { + let accountPeerId = account.peerId + let inputCall: Api.InputGroupCall + switch reference { + case let .id(id, accessHash): + inputCall = .inputGroupCall(id: id, accessHash: accessHash) + case let .link(slug): + inputCall = .inputGroupCallSlug(slug: slug) + case let .message(id): + if id.peerId.namespace != Namespaces.Peer.CloudUser { + return .single(nil) + } + if id.namespace != Namespaces.Message.Cloud { + return .single(nil) + } + inputCall = .inputGroupCallInviteMessage(msgId: id.id) + } + return account.network.request(Api.functions.phone.getGroupCall(call: inputCall, limit: 4)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<(participants: [PeerId], duration: Int32?)?, NoError> in + guard let result else { + return .single(nil) + } + switch result { + case let .groupCall(call, participants, _, chats, users): + return account.postbox.transaction { transaction -> (participants: [PeerId], duration: Int32?)? in + if case let .groupCallDiscarded(_, _, duration) = call { + return ([], duration) + } + + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) + + let parsedParticipants = participants.compactMap { GroupCallParticipantsContext.Participant($0, transaction: transaction) } + return ( + parsedParticipants.map(\.peer.id), + nil + ) + } + } + } +} + public enum CreateGroupCallError { case generic case anonymousNotAllowed @@ -3050,3 +3097,59 @@ func _internal_sendConferenceCallBroadcast(account: Account, callId: Int64, acce return .complete() } } + +func _internal_refreshInlineGroupCall(account: Account, messageId: MessageId) -> Signal { + return _internal_getCurrentGroupCallInfo(account: account, reference: .message(id: messageId)) + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> Void in + transaction.updateMessage(messageId, update: { currentMessage in + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) + } + var updatedMedia = currentMessage.media + + for i in 0 ..< updatedMedia.count { + if let action = updatedMedia[i] as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action { + var otherParticipants: [PeerId] = [] + var duration: Int32? = conferenceCall.duration + if let result { + for id in result.participants { + if id != account.peerId { + otherParticipants.append(id) + } + } + duration = result.duration + } else { + duration = nil + } + + updatedMedia[i] = TelegramMediaAction(action: .conferenceCall(TelegramMediaActionType.ConferenceCall( + callId: conferenceCall.callId, + duration: duration, + flags: conferenceCall.flags, + otherParticipants: otherParticipants + ))) + } + } + return .update(StoreMessage( + id: currentMessage.id, + globallyUniqueId: currentMessage.globallyUniqueId, + groupingKey: currentMessage.groupingKey, + threadId: currentMessage.threadId, + timestamp: currentMessage.timestamp, + flags: StoreMessageFlags(currentMessage.flags), + tags: currentMessage.tags, + globalTags: currentMessage.globalTags, + localTags: currentMessage.localTags, + forwardInfo: storeForwardInfo, + authorId: currentMessage.author?.id, + text: currentMessage.text, + attributes: currentMessage.attributes, + media: updatedMedia + )) + }) + } + |> ignoreValues + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift index 75987f4d84..38d85daf9c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift @@ -142,6 +142,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { #else missedTimeout = 30 #endif + if conferenceCall.duration != nil { + hasCallButton = false + } let currentTime = Int32(Date().timeIntervalSince1970) if conferenceCall.flags.contains(.isMissed) { titleString = "Declined Group Call" @@ -149,7 +152,6 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { titleString = "Missed Group Call" } else if conferenceCall.duration != nil { titleString = "Cancelled Group Call" - hasCallButton = true } else { if incoming { titleString = "Incoming Group Call" diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index cd88b36aa2..1a45674174 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -586,6 +586,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto private let translationProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0) private let refreshStoriesProcessingManager = ChatMessageThrottledProcessingManager() private let factCheckProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0) + private let inlineGroupCallsProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0) let prefetchManager: InChatPrefetchManager private var currentEarlierPrefetchMessages: [(Message, Media)] = [] @@ -978,6 +979,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto strongSelf.context.account.viewTracker.updatedExtendedMediaForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } + self.inlineGroupCallsProcessingManager.process = { [weak context] messageIds in + context?.account.viewTracker.refreshInlineGroupCallsForMessageIds(messageIds: Set(messageIds.map(\.messageId))) + } + self.preloadPages = false self.beginChatHistoryTransitions(resetScrolling: false) @@ -2728,6 +2733,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var messageIdsWithUnseenPersonalMention: [MessageId] = [] var messageIdsWithUnseenReactions: [MessageId] = [] var messageIdsWithInactiveExtendedMedia = Set() + var messageIdsWithGroupCalls: [MessageId] = [] var downloadableResourceIds: [(messageId: MessageId, resourceId: String)] = [] var allVisibleAnchorMessageIds: [(MessageId, Int)] = [] var visibleAdOpaqueIds: [Data] = [] @@ -2826,6 +2832,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto storiesRequiredValidation = true } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let _ = content.story { storiesRequiredValidation = true + } else if let media = media as? TelegramMediaAction { + if case let .conferenceCall(conferenceCall) = media.action { + if conferenceCall.duration != nil { + } else { + messageIdsWithGroupCalls.append(message.id) + } + } } } if contentRequiredValidation { @@ -3083,6 +3096,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if !peerIdsWithRefreshStories.isEmpty { self.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: peerIdsWithRefreshStories) } + if !messageIdsWithGroupCalls.isEmpty { + self.inlineGroupCallsProcessingManager.add(messageIdsWithGroupCalls.map { MessageAndThreadId(messageId: $0, threadId: nil) }) + } self.currentEarlierPrefetchMessages = toEarlierMediaMessages self.currentLaterPrefetchMessages = toLaterMediaMessages