Conference updates

This commit is contained in:
Isaac 2025-04-04 14:04:52 +04:00
parent e03a9d818e
commit e8b7f53c84
6 changed files with 297 additions and 70 deletions

View File

@ -15,17 +15,20 @@ final class VideoChatListInviteComponent: Component {
let title: String let title: String
let icon: Icon let icon: Icon
let theme: PresentationTheme let theme: PresentationTheme
let hasNext: Bool
let action: () -> Void let action: () -> Void
init( init(
title: String, title: String,
icon: Icon, icon: Icon,
theme: PresentationTheme, theme: PresentationTheme,
hasNext: Bool,
action: @escaping () -> Void action: @escaping () -> Void
) { ) {
self.title = title self.title = title
self.icon = icon self.icon = icon
self.theme = theme self.theme = theme
self.hasNext = hasNext
self.action = action self.action = action
} }
@ -39,6 +42,9 @@ final class VideoChatListInviteComponent: Component {
if lhs.theme !== rhs.theme { if lhs.theme !== rhs.theme {
return false return false
} }
if lhs.hasNext != rhs.hasNext {
return false
}
return true return true
} }
@ -52,7 +58,11 @@ final class VideoChatListInviteComponent: Component {
private var highlightBackgroundLayer: SimpleLayer? private var highlightBackgroundLayer: SimpleLayer?
private var highlightBackgroundFrame: CGRect? private var highlightBackgroundFrame: CGRect?
private let separatorLayer: SimpleLayer
override init(frame: CGRect) { override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
super.init(frame: frame) super.init(frame: frame)
self.highligthedChanged = { [weak self] isHighlighted in self.highligthedChanged = { [weak self] isHighlighted in
@ -74,6 +84,15 @@ final class VideoChatListInviteComponent: Component {
} }
highlightBackgroundLayer.frame = highlightBackgroundFrame highlightBackgroundLayer.frame = highlightBackgroundFrame
highlightBackgroundLayer.opacity = 1.0 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 { } else {
if let highlightBackgroundLayer = self.highlightBackgroundLayer { if let highlightBackgroundLayer = self.highlightBackgroundLayer {
self.highlightBackgroundLayer = nil self.highlightBackgroundLayer = nil
@ -152,7 +171,14 @@ final class VideoChatListInviteComponent: Component {
transition.setFrame(view: iconView, frame: iconFrame) 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 return size
} }

View File

@ -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<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)
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
))
}
var gridParticipants: [VideoParticipant] = [] var gridParticipants: [VideoParticipant] = []
var listParticipants: [GroupCallParticipantsContext.Participant] = [] var listParticipants: [GroupCallParticipantsContext.Participant] = []
if let participants = component.participants { if let participants = component.participants {
@ -1868,6 +1800,90 @@ final class VideoChatParticipantsComponent: Component {
self.gridParticipants = gridParticipants self.gridParticipants = gridParticipants
self.listParticipants = listParticipants 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<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,
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( let itemLayout = ItemLayout(
containerSize: availableSize, containerSize: availableSize,
layout: component.layout, layout: component.layout,

View File

@ -1301,6 +1301,70 @@ public final class AccountViewTracker {
} }
} }
public func refreshInlineGroupCallsForMessageIds(messageIds: Set<MessageId>) {
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<Never, NoError> in
guard !ids.isEmpty else {
return .complete()
}
var requests: [Signal<Never, NoError>] = []
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]) { public func refreshStoryStatsForPeerIds(peerIds: [PeerId]) {
self.queue.async { self.queue.async {
self.pendingRefreshStoriesForPeerIds.append(contentsOf: peerIds) self.pendingRefreshStoriesForPeerIds.append(contentsOf: peerIds)

View File

@ -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<Api.phone.GroupCall?, NoError> 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 { public enum CreateGroupCallError {
case generic case generic
case anonymousNotAllowed case anonymousNotAllowed
@ -3050,3 +3097,59 @@ func _internal_sendConferenceCallBroadcast(account: Account, callId: Int64, acce
return .complete() return .complete()
} }
} }
func _internal_refreshInlineGroupCall(account: Account, messageId: MessageId) -> Signal<Never, NoError> {
return _internal_getCurrentGroupCallInfo(account: account, reference: .message(id: messageId))
|> mapToSignal { result -> Signal<Never, NoError> 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
}
}

View File

@ -142,6 +142,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
#else #else
missedTimeout = 30 missedTimeout = 30
#endif #endif
if conferenceCall.duration != nil {
hasCallButton = false
}
let currentTime = Int32(Date().timeIntervalSince1970) let currentTime = Int32(Date().timeIntervalSince1970)
if conferenceCall.flags.contains(.isMissed) { if conferenceCall.flags.contains(.isMissed) {
titleString = "Declined Group Call" titleString = "Declined Group Call"
@ -149,7 +152,6 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
titleString = "Missed Group Call" titleString = "Missed Group Call"
} else if conferenceCall.duration != nil { } else if conferenceCall.duration != nil {
titleString = "Cancelled Group Call" titleString = "Cancelled Group Call"
hasCallButton = true
} else { } else {
if incoming { if incoming {
titleString = "Incoming Group Call" titleString = "Incoming Group Call"

View File

@ -586,6 +586,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
private let translationProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0) private let translationProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0)
private let refreshStoriesProcessingManager = ChatMessageThrottledProcessingManager() private let refreshStoriesProcessingManager = ChatMessageThrottledProcessingManager()
private let factCheckProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0) private let factCheckProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0)
private let inlineGroupCallsProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0)
let prefetchManager: InChatPrefetchManager let prefetchManager: InChatPrefetchManager
private var currentEarlierPrefetchMessages: [(Message, Media)] = [] 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))) 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.preloadPages = false
self.beginChatHistoryTransitions(resetScrolling: false) self.beginChatHistoryTransitions(resetScrolling: false)
@ -2728,6 +2733,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
var messageIdsWithUnseenPersonalMention: [MessageId] = [] var messageIdsWithUnseenPersonalMention: [MessageId] = []
var messageIdsWithUnseenReactions: [MessageId] = [] var messageIdsWithUnseenReactions: [MessageId] = []
var messageIdsWithInactiveExtendedMedia = Set<MessageId>() var messageIdsWithInactiveExtendedMedia = Set<MessageId>()
var messageIdsWithGroupCalls: [MessageId] = []
var downloadableResourceIds: [(messageId: MessageId, resourceId: String)] = [] var downloadableResourceIds: [(messageId: MessageId, resourceId: String)] = []
var allVisibleAnchorMessageIds: [(MessageId, Int)] = [] var allVisibleAnchorMessageIds: [(MessageId, Int)] = []
var visibleAdOpaqueIds: [Data] = [] var visibleAdOpaqueIds: [Data] = []
@ -2826,6 +2832,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
storiesRequiredValidation = true storiesRequiredValidation = true
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let _ = content.story { } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let _ = content.story {
storiesRequiredValidation = true 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 { if contentRequiredValidation {
@ -3083,6 +3096,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
if !peerIdsWithRefreshStories.isEmpty { if !peerIdsWithRefreshStories.isEmpty {
self.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: peerIdsWithRefreshStories) self.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: peerIdsWithRefreshStories)
} }
if !messageIdsWithGroupCalls.isEmpty {
self.inlineGroupCallsProcessingManager.add(messageIdsWithGroupCalls.map { MessageAndThreadId(messageId: $0, threadId: nil) })
}
self.currentEarlierPrefetchMessages = toEarlierMediaMessages self.currentEarlierPrefetchMessages = toEarlierMediaMessages
self.currentLaterPrefetchMessages = toLaterMediaMessages self.currentLaterPrefetchMessages = toLaterMediaMessages