diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index e2a77eca8a..0321b30b7a 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -240,6 +240,7 @@ public struct PresentationGroupCallState: Equatable { public var isVideoWatchersLimitReached: Bool public var isMyVideoActive: Bool public var isUnifiedStream: Bool + public var defaultSendAs: EnginePeer.Id? public init( myPeerId: EnginePeer.Id, @@ -260,7 +261,8 @@ public struct PresentationGroupCallState: Equatable { isVideoEnabled: Bool, isVideoWatchersLimitReached: Bool, isMyVideoActive: Bool, - isUnifiedStream: Bool + isUnifiedStream: Bool, + defaultSendAs: EnginePeer.Id? ) { self.myPeerId = myPeerId self.networkState = networkState @@ -281,6 +283,7 @@ public struct PresentationGroupCallState: Equatable { self.isVideoWatchersLimitReached = isVideoWatchersLimitReached self.isMyVideoActive = isMyVideoActive self.isUnifiedStream = isUnifiedStream + self.defaultSendAs = defaultSendAs } } diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index d08f1942de..2409fe8d8d 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -40,7 +40,8 @@ private extension PresentationGroupCallState { isVideoEnabled: false, isVideoWatchersLimitReached: false, isMyVideoActive: false, - isUnifiedStream: false + isUnifiedStream: false, + defaultSendAs: nil ) } } @@ -1565,6 +1566,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { isVideoEnabled: callInfo.isVideoEnabled, unmutedVideoLimit: callInfo.unmutedVideoLimit, isStream: callInfo.isStream, + sendPaidMessagesStars: self.stateValue.sendPaidMessageStars, + defaultSendAs: self.stateValue.defaultSendAs, version: 0 ), previousServiceState: nil, @@ -2282,19 +2285,23 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if case let .established(callInfo, _, _, _, initialState) = internalState { self.summaryInfoState.set(.single(SummaryInfoState(info: callInfo))) - self.stateValue.canManageCall = initialState.isCreator || initialState.adminIds.contains(self.accountContext.account.peerId) - if self.stateValue.canManageCall && initialState.defaultParticipantsAreMuted.canChange { - self.stateValue.defaultParticipantMuteState = initialState.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted + var stateValue = self.stateValue + + stateValue.canManageCall = initialState.isCreator || initialState.adminIds.contains(self.accountContext.account.peerId) + if stateValue.canManageCall && initialState.defaultParticipantsAreMuted.canChange { + stateValue.defaultParticipantMuteState = initialState.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted } - if self.stateValue.recordingStartTimestamp != initialState.recordingStartTimestamp { - self.stateValue.recordingStartTimestamp = initialState.recordingStartTimestamp + if stateValue.recordingStartTimestamp != initialState.recordingStartTimestamp { + stateValue.recordingStartTimestamp = initialState.recordingStartTimestamp } - if self.stateValue.title != initialState.title { - self.stateValue.title = initialState.title + if stateValue.title != initialState.title { + stateValue.title = initialState.title } - if self.stateValue.scheduleTimestamp != initialState.scheduleTimestamp { - self.stateValue.scheduleTimestamp = initialState.scheduleTimestamp + if stateValue.scheduleTimestamp != initialState.scheduleTimestamp { + stateValue.scheduleTimestamp = initialState.scheduleTimestamp } + stateValue.defaultSendAs = initialState.defaultSendAs + self.stateValue = stateValue let accountContext = self.accountContext let peerId = self.peerId @@ -2667,21 +2674,24 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.membersValue = members - self.stateValue.adminIds = adminIds + var stateValue = self.stateValue + stateValue.adminIds = adminIds - self.stateValue.canManageCall = state.isCreator || adminIds.contains(self.accountContext.account.peerId) - if (state.isCreator || self.stateValue.adminIds.contains(self.accountContext.account.peerId)) && state.defaultParticipantsAreMuted.canChange { - self.stateValue.defaultParticipantMuteState = state.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted + stateValue.canManageCall = state.isCreator || adminIds.contains(self.accountContext.account.peerId) + if (state.isCreator || stateValue.adminIds.contains(self.accountContext.account.peerId)) && state.defaultParticipantsAreMuted.canChange { + stateValue.defaultParticipantMuteState = state.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted } - self.stateValue.messagesAreEnabled = state.messagesAreEnabled.isEnabled - self.stateValue.canEnableMessages = state.messagesAreEnabled.canChange - self.stateValue.sendPaidMessageStars = state.messagesAreEnabled.sendPaidMessagesStars - self.stateValue.recordingStartTimestamp = state.recordingStartTimestamp - self.stateValue.title = state.title - self.stateValue.scheduleTimestamp = state.scheduleTimestamp - self.stateValue.isVideoEnabled = state.isVideoEnabled && otherParticipantsWithVideo < state.unmutedVideoLimit - self.stateValue.isVideoWatchersLimitReached = videoWatchingParticipants >= configuration.videoParticipantsMaxCount - self.stateValue.isUnifiedStream = state.isStream + stateValue.messagesAreEnabled = state.messagesAreEnabled.isEnabled + stateValue.canEnableMessages = state.messagesAreEnabled.canChange + stateValue.sendPaidMessageStars = state.messagesAreEnabled.sendPaidMessagesStars + stateValue.recordingStartTimestamp = state.recordingStartTimestamp + stateValue.title = state.title + stateValue.scheduleTimestamp = state.scheduleTimestamp + stateValue.isVideoEnabled = state.isVideoEnabled && otherParticipantsWithVideo < state.unmutedVideoLimit + stateValue.isVideoWatchersLimitReached = videoWatchingParticipants >= configuration.videoParticipantsMaxCount + stateValue.isUnifiedStream = state.isStream + stateValue.defaultSendAs = state.defaultSendAs + self.stateValue = stateValue self.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo( id: callInfo.id, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index e44ff50623..e63f68e541 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1220,7 +1220,8 @@ final class VideoChatScreenComponent: Component { isVideoEnabled: true, isVideoWatchersLimitReached: false, isMyVideoActive: false, - isUnifiedStream: false + isUnifiedStream: false, + defaultSendAs: nil ) return .single((callState, invitedPeers.compactMap({ peer -> VideoChatScreenComponent.InvitedPeer? in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 5a7d07b036..925ed0ab8c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -595,6 +595,8 @@ func _internal_getGroupCallParticipants(account: Account, reference: InternalGro isVideoEnabled: isVideoEnabled, unmutedVideoLimit: unmutedVideoLimit, isStream: isStream, + sendPaidMessagesStars: nil, + defaultSendAs: nil, version: version ) } @@ -1479,6 +1481,8 @@ public final class GroupCallParticipantsContext { isVideoEnabled: Bool, unmutedVideoLimit: Int, isStream: Bool, + sendPaidMessagesStars: Int64?, + defaultSendAs: PeerId?, version: Int32 ) { self.participants = participants @@ -1496,6 +1500,8 @@ public final class GroupCallParticipantsContext { self.isVideoEnabled = isVideoEnabled self.unmutedVideoLimit = unmutedVideoLimit self.isStream = isStream + self.sendPaidMessagesStars = sendPaidMessagesStars + self.defaultSendAs = defaultSendAs self.version = version } } @@ -1871,6 +1877,8 @@ public final class GroupCallParticipantsContext { isVideoEnabled: strongSelf.stateValue.state.isVideoEnabled, unmutedVideoLimit: strongSelf.stateValue.state.unmutedVideoLimit, isStream: strongSelf.stateValue.state.isStream, + sendPaidMessagesStars: strongSelf.stateValue.state.sendPaidMessagesStars, + defaultSendAs: strongSelf.stateValue.state.defaultSendAs, version: strongSelf.stateValue.state.version ), overlayState: strongSelf.stateValue.overlayState, @@ -2102,6 +2110,8 @@ public final class GroupCallParticipantsContext { isVideoEnabled: strongSelf.stateValue.state.isVideoEnabled, unmutedVideoLimit: strongSelf.stateValue.state.unmutedVideoLimit, isStream: strongSelf.stateValue.state.isStream, + sendPaidMessagesStars: strongSelf.stateValue.state.sendPaidMessagesStars, + defaultSendAs: strongSelf.stateValue.state.defaultSendAs, version: strongSelf.stateValue.state.version ), overlayState: strongSelf.stateValue.overlayState, @@ -2328,6 +2338,8 @@ public final class GroupCallParticipantsContext { let isVideoEnabled = strongSelf.stateValue.state.isVideoEnabled let isStream = strongSelf.stateValue.state.isStream let unmutedVideoLimit = strongSelf.stateValue.state.unmutedVideoLimit + let sendPaidMessagesStars = strongSelf.stateValue.state.sendPaidMessagesStars + let defaultSendAs = strongSelf.stateValue.state.defaultSendAs updatedParticipants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: strongSelf.stateValue.state.sortAscending) }) @@ -2348,6 +2360,8 @@ public final class GroupCallParticipantsContext { isVideoEnabled: isVideoEnabled, unmutedVideoLimit: unmutedVideoLimit, isStream: isStream, + sendPaidMessagesStars: sendPaidMessagesStars, + defaultSendAs: defaultSendAs, version: update.version ), overlayState: updatedOverlayState, @@ -4300,7 +4314,7 @@ public final class GroupCallMessagesContext { flags |= 1 << 0 } var sendAs: Api.InputPeer? - if fromId != self.account.peerId { + if fromId != self.account.peerId || self.isLiveStream { guard let fromPeer else { return } @@ -4364,7 +4378,7 @@ public final class GroupCallMessagesContext { var flags: Int32 = 0 flags |= 1 << 0 var sendAs: Api.InputPeer? - if pendingSendStars.fromPeer.id != self.account.peerId { + if pendingSendStars.fromPeer.id != self.account.peerId || self.isLiveStream { sendAs = apiInputPeer(pendingSendStars.fromPeer) } if sendAs != nil { diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 9dbdda3121..4141089a0a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -1533,10 +1533,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg shouldDisplayMenuButton = true } + var displaySendAsAvatarButton = false let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty && interfaceState.editMessageState == nil { menuButtonExpanded = false - self.sendAsAvatarButtonNode.isHidden = false + displaySendAsAvatarButton = true var currentPeer = sendAsPeers.first(where: { $0.peer.id == interfaceState.currentSendAsPeerId})?.peer if currentPeer == nil { @@ -1556,9 +1557,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg break } } - self.sendAsAvatarButtonNode.isHidden = true - } else { - self.sendAsAvatarButtonNode.isHidden = true } if mediaRecordingState != nil || interfaceState.interfaceState.mediaDraftState != nil { hasMenuButton = false @@ -2978,11 +2976,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } transition.updateFrame(node: self.textPlaceholderNode, frame: textPlaceholderFrame) + let sendAsButtonFrame = CGRect(origin: CGPoint(x: 3.0, y: textInputContainerBackgroundFrame.height - 3.0 - 34.0), size: CGSize(width: 34.0, height: 34.0)) + let sendAsAvatarButtonAlpha: CGFloat = audioRecordingItemsAlpha * (displaySendAsAvatarButton ? 1.0 : 0.0) transition.updatePosition(node: self.sendAsAvatarButtonNode, position: sendAsButtonFrame.center) transition.updateBounds(node: self.sendAsAvatarButtonNode, bounds: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size)) - transition.updateAlpha(layer: self.sendAsAvatarButtonNode.layer, alpha: audioRecordingItemsAlpha) - transition.updateTransformScale(layer: self.sendAsAvatarButtonNode.layer, scale: audioRecordingItemsAlpha == 0.0 ? 0.001 : 1.0) + transition.updateAlpha(layer: self.sendAsAvatarButtonNode.layer, alpha: sendAsAvatarButtonAlpha) + transition.updateTransformScale(layer: self.sendAsAvatarButtonNode.layer, scale: sendAsAvatarButtonAlpha == 0.0 ? 0.001 : 1.0) transition.updateFrame(node: self.sendAsAvatarContainerNode, frame: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size)) transition.updateFrame(node: self.sendAsAvatarReferenceNode, frame: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size)) transition.updatePosition(node: self.sendAsAvatarNode, position: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size).center) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 63ac495965..5d6e89e962 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -118,8 +118,9 @@ final class StoryItemContentComponent: Component { var minMessagePrice: Int64? var starStats: StarStats? var isAdmin: Bool + var defaultSendAs: EnginePeer.Id? - init(isExpanded: Bool, isEmpty: Bool, hasUnseenMessages: Bool, areMessagesEnabled: Bool, minMessagePrice: Int64?, starStats: StarStats?, isAdmin: Bool) { + init(isExpanded: Bool, isEmpty: Bool, hasUnseenMessages: Bool, areMessagesEnabled: Bool, minMessagePrice: Int64?, starStats: StarStats?, isAdmin: Bool, defaultSendAs: EnginePeer.Id?) { self.isExpanded = isExpanded self.isEmpty = isEmpty self.hasUnseenMessages = hasUnseenMessages @@ -127,6 +128,7 @@ final class StoryItemContentComponent: Component { self.minMessagePrice = minMessagePrice self.starStats = starStats self.isAdmin = isAdmin + self.defaultSendAs = defaultSendAs } } @@ -134,12 +136,14 @@ final class StoryItemContentComponent: Component { var areMessagesEnabled: Bool var minMessagePrice: Int64? var isAdmin: Bool + var defaultSendAs: EnginePeer.Id? var isUnifiedStream: Bool - init(areMessagesEnabled: Bool, minMessagePrice: Int64?, isAdmin: Bool, isUnifiedStream: Bool) { + init(areMessagesEnabled: Bool, minMessagePrice: Int64?, isAdmin: Bool, defaultSendAs: EnginePeer.Id?, isUnifiedStream: Bool) { self.areMessagesEnabled = areMessagesEnabled self.minMessagePrice = minMessagePrice self.isAdmin = isAdmin + self.defaultSendAs = defaultSendAs self.isUnifiedStream = isUnifiedStream } } @@ -229,7 +233,8 @@ final class StoryItemContentComponent: Component { areMessagesEnabled: mediaStreamCallState?.areMessagesEnabled ?? false, minMessagePrice: mediaStreamCallState?.minMessagePrice, starStats: starStats, - isAdmin: mediaStreamCallState?.isAdmin ?? false + isAdmin: mediaStreamCallState?.isAdmin ?? false, + defaultSendAs: mediaStreamCallState?.defaultSendAs ) } @@ -1117,6 +1122,7 @@ final class StoryItemContentComponent: Component { areMessagesEnabled: state.messagesAreEnabled, minMessagePrice: state.sendPaidMessageStars, isAdmin: state.canManageCall, + defaultSendAs: state.defaultSendAs, isUnifiedStream: state.isUnifiedStream ) if self.mediaStreamCallState != mappedState { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift index 81f7a6d9d0..7867966f2f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift @@ -103,7 +103,9 @@ final class StoryItemLoadingEffectView: UIView { } func update(size: CGSize, transition: ComponentTransition) { - if self.backgroundView.bounds.size != size { + let backgroundSize = CGSize(width: self.gradientWidth, height: size.height) + + if self.backgroundView.bounds.size != backgroundSize { self.backgroundView.layer.removeAllAnimations() if !self.hasCustomBorder { @@ -115,7 +117,7 @@ final class StoryItemLoadingEffectView: UIView { } } - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: size.height))) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: backgroundSize)) transition.setFrame(view: self.borderContainerView, frame: CGRect(origin: CGPoint(), size: size)) transition.setFrame(view: self.borderGradientView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: size.height))) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 4906ee9031..8dbc46fd69 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -3012,7 +3012,16 @@ public final class StoryItemSetContainerComponent: Component { } } - sendAsConfiguration = self.sendMessageContext.currentSendAsPeer.flatMap { value in + var sendAsPeer: SendAsPeer? + if let currentSendAsPeer = self.sendMessageContext.currentSendAsPeer { + sendAsPeer = currentSendAsPeer + } else { + sendAsPeer = liveChatStateValue.defaultSendAs.flatMap { defaultSendAs in + return self.sendMessageContext.sendAsData?.availablePeers.first(where: { $0.peer.id == defaultSendAs }) + } + } + + sendAsConfiguration = sendAsPeer.flatMap { value in return MessageInputPanelComponent.SendAsConfiguration( currentPeer: EnginePeer(value.peer), subscriberCount: value.subscribers.flatMap(Int.init), @@ -6325,102 +6334,109 @@ public final class StoryItemSetContainerComponent: Component { return .complete() } + var isLiveStream = false + if case .liveStream = component.slice.item.storyItem.media { + isLiveStream = true + } + var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: component.strings.Stories_MenuAddToAlbum, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddToFolder"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in - guard let self, let c else { - f(.default) - return - } - - Task { @MainActor [weak self, weak c] in - guard let self, let component = self.component, let peerId = component.slice.item.peerId, let c else { + if !isLiveStream { + items.append(.action(ContextMenuActionItem(text: component.strings.Stories_MenuAddToAlbum, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddToFolder"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in + guard let self, let c else { + f(.default) return } - let (peerReference, folderPreviews) = await PeerStoryListContext.folderPreviews(peerId: peerId, account: component.context.account).get() - - var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) - }, iconPosition: .left, action: { c ,f in - c?.popItems() - }))) - items.append(.separator) - - items.append(.action(ContextMenuActionItem(text: component.strings.Stories_MenuNewAlbum, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddFolder"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] c, f in - guard let self else { - f(.default) + Task { @MainActor [weak self, weak c] in + guard let self, let component = self.component, let peerId = component.slice.item.peerId, let c else { return } - c?.dismiss(completion: { [weak self] in - guard let self, let component = self.component else { - return - } - self.presentAddStoryFolder(addItems: [component.slice.item.storyItem]) - }) - }))) - - for folderPreview in folderPreviews { - var iconSource: ContextMenuActionItemIconSource? - if let story = folderPreview.item { - var imageSignal: Signal? - - var selectedMedia: Media? - if let image = story.media._asMedia() as? TelegramMediaImage { - selectedMedia = image - } else if let file = story.media._asMedia() as? TelegramMediaFile { - selectedMedia = file - } - - if let selectedMedia { - let directMediaImageCache = DirectMediaImageCache(account: component.context.account) - if let result = directMediaImageCache.getImage(peer: peerReference, story: story, media: selectedMedia, width: 48, aspectRatio: 1.0, possibleWidths: [48], includeBlurred: false, synchronous: true) { - if let loadSignal = result.loadSignal { - imageSignal = .single(result.image) |> then(loadSignal) - } else { - imageSignal = .single(result.image) - } - } - } - - if let imageSignal { - iconSource = ContextMenuActionItemIconSource( - size: CGSize(width: 24.0, height: 24.0), - cornerRadius: 5.0, - signal: imageSignal - ) - } - } + let (peerReference, folderPreviews) = await PeerStoryListContext.folderPreviews(peerId: peerId, account: component.context.account).get() - var icon: (PresentationTheme) -> UIImage? = { _ in nil } - if iconSource == nil { - icon = { theme in - return generateImage(CGSize(width: 24.0, height: 24.0), opaque: false, scale: nil, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(theme.contextMenu.primaryColor.withMultipliedAlpha(0.1).cgColor) - context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 5.0).cgPath) - context.fillPath() - }) - } - } + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { c ,f in + c?.popItems() + }))) + items.append(.separator) - items.append(.action(ContextMenuActionItem(text: folderPreview.folder.title, icon: icon, iconSource: iconSource, iconPosition: .left, action: { [weak self] c, f in - guard let self, let component = self.component else { + items.append(.action(ContextMenuActionItem(text: component.strings.Stories_MenuNewAlbum, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddFolder"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] c, f in + guard let self else { f(.default) return } - c?.dismiss(completion: {}) - - component.addToFolder(folderPreview.folder.id) + c?.dismiss(completion: { [weak self] in + guard let self, let component = self.component else { + return + } + self.presentAddStoryFolder(addItems: [component.slice.item.storyItem]) + }) }))) + + for folderPreview in folderPreviews { + var iconSource: ContextMenuActionItemIconSource? + if let story = folderPreview.item { + var imageSignal: Signal? + + var selectedMedia: Media? + if let image = story.media._asMedia() as? TelegramMediaImage { + selectedMedia = image + } else if let file = story.media._asMedia() as? TelegramMediaFile { + selectedMedia = file + } + + if let selectedMedia { + let directMediaImageCache = DirectMediaImageCache(account: component.context.account) + if let result = directMediaImageCache.getImage(peer: peerReference, story: story, media: selectedMedia, width: 48, aspectRatio: 1.0, possibleWidths: [48], includeBlurred: false, synchronous: true) { + if let loadSignal = result.loadSignal { + imageSignal = .single(result.image) |> then(loadSignal) + } else { + imageSignal = .single(result.image) + } + } + } + + if let imageSignal { + iconSource = ContextMenuActionItemIconSource( + size: CGSize(width: 24.0, height: 24.0), + cornerRadius: 5.0, + signal: imageSignal + ) + } + } + + var icon: (PresentationTheme) -> UIImage? = { _ in nil } + if iconSource == nil { + icon = { theme in + return generateImage(CGSize(width: 24.0, height: 24.0), opaque: false, scale: nil, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.contextMenu.primaryColor.withMultipliedAlpha(0.1).cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 5.0).cgPath) + context.fillPath() + }) + } + } + + items.append(.action(ContextMenuActionItem(text: folderPreview.folder.title, icon: icon, iconSource: iconSource, iconPosition: .left, action: { [weak self] c, f in + guard let self, let component = self.component else { + f(.default) + return + } + + c?.dismiss(completion: {}) + + component.addToFolder(folderPreview.folder.id) + }))) + } + + c.pushItems(items: .single(ContextController.Items(content: .list(items)))) } - - c.pushItems(items: .single(ContextController.Items(content: .list(items)))) - } - }))) + }))) + } if case .file = component.slice.item.storyItem.media { var speedValue: String = presentationData.strings.PlaybackSpeed_Normal @@ -6484,92 +6500,94 @@ public final class StoryItemSetContainerComponent: Component { self.openItemPrivacySettings() }))) - items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Edit, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.openStoryEditing() - }))) - - if case .file = component.slice.item.storyItem.media, component.slice.item.storyItem.isPinned { - items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/EditCover"), color: theme.contextMenu.primaryColor) + if !isLiveStream { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Edit, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) guard let self else { return } - self.openStoryEditing(cover: true) + self.openStoryEditing() }))) - } - - items.append(.separator) - - items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? component.strings.Story_Context_RemoveFromProfile : component.strings.Story_Context_SaveToProfile, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Stories/Context Menu/Unpin" : "Stories/Context Menu/Pin"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - guard let self, let component = self.component else { - return + if case .file = component.slice.item.storyItem.media, component.slice.item.storyItem.isPinned { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/EditCover"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.openStoryEditing(cover: true) + }))) } - let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.slice.effectivePeer.id, ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).startStandalone() + items.append(.separator) - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - if component.slice.item.storyItem.isPinned { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .info(title: nil, text: component.strings.Story_ToastRemovedFromProfileText, timeout: nil, customUndoText: nil), - elevatedLayout: false, - animateInAsReplacement: false, - appearance: UndoOverlayController.Appearance(isBlurred: true), - action: { _ in return false } - ), nil) - } else { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .info(title: component.strings.Story_ToastSavedToProfileTitle, text: component.strings.Story_ToastSavedToProfileText, timeout: nil, customUndoText: nil), - elevatedLayout: false, - animateInAsReplacement: false, - appearance: UndoOverlayController.Appearance(isBlurred: true), - action: { _ in return false } - ), nil) - } - }))) - - let saveText: String = component.strings.Story_Context_SaveToGallery - items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.requestSave() - }))) - - if case let .user(accountUser) = component.slice.effectivePeer { - items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) + items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? component.strings.Story_Context_RemoveFromProfile : component.strings.Story_Context_SaveToProfile, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Stories/Context Menu/Unpin" : "Stories/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) - guard let self else { + guard let self, let component = self.component else { return } - if accountUser.isPremium { - self.sendMessageContext.requestStealthMode(view: self) + + let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.slice.effectivePeer.id, ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).startStandalone() + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + if component.slice.item.storyItem.isPinned { + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: component.strings.Story_ToastRemovedFromProfileText, timeout: nil, customUndoText: nil), + elevatedLayout: false, + animateInAsReplacement: false, + appearance: UndoOverlayController.Appearance(isBlurred: true), + action: { _ in return false } + ), nil) } else { - self.presentStealthModeUpgradeScreen() + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .info(title: component.strings.Story_ToastSavedToProfileTitle, text: component.strings.Story_ToastSavedToProfileText, timeout: nil, customUndoText: nil), + elevatedLayout: false, + animateInAsReplacement: false, + appearance: UndoOverlayController.Appearance(isBlurred: true), + action: { _ in return false } + ), nil) } }))) + + let saveText: String = component.strings.Story_Context_SaveToGallery + items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.requestSave() + }))) + + if case let .user(accountUser) = component.slice.effectivePeer { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + if accountUser.isPremium { + self.sendMessageContext.requestStealthMode(view: self) + } else { + self.presentStealthModeUpgradeScreen() + } + }))) + } } if component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer._asPeer().usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { @@ -6662,6 +6680,11 @@ public final class StoryItemSetContainerComponent: Component { guard let self, let component else { return .complete() } + + var isLiveStream = false + if case .liveStream = component.slice.item.storyItem.media { + isLiveStream = true + } var items: [ContextMenuItem] = [] if case .file = component.slice.item.storyItem.media { @@ -6694,7 +6717,7 @@ public final class StoryItemSetContainerComponent: Component { items.append(.separator) } - if (component.slice.item.storyItem.isMy && channel.hasPermission(.postStories)) || channel.hasPermission(.editStories) { + if !isLiveStream && ((component.slice.item.storyItem.isMy && channel.hasPermission(.postStories)) || channel.hasPermission(.editStories)) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Edit, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6724,7 +6747,7 @@ public final class StoryItemSetContainerComponent: Component { items.append(.separator) } - if channel.hasPermission(.editStories) { + if !isLiveStream && channel.hasPermission(.editStories) { items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? component.strings.Story_Context_RemoveFromChannel : component.strings.Story_Context_SaveToChannel, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Stories/Context Menu/Unpin" : "Stories/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6764,7 +6787,7 @@ public final class StoryItemSetContainerComponent: Component { }))) } - if component.slice.additionalPeerData.canViewStats { + if !isLiveStream && component.slice.additionalPeerData.canViewStats { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_ViewStats, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6786,17 +6809,19 @@ public final class StoryItemSetContainerComponent: Component { }))) } - let saveText: String = component.strings.Story_Context_SaveToGallery - items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.requestSave() - }))) + if !isLiveStream { + let saveText: String = component.strings.Story_Context_SaveToGallery + items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.requestSave() + }))) + } if component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer._asPeer().usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 6bf6183e45..5b7bde2a57 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -107,7 +107,17 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { var currentLiveStreamStarsIsActive: Bool = false var currentLiveStreamStarsIsActiveTimer: Foundation.Timer? - var sendAsData: (isPremium: Bool, availablePeers: [SendAsPeer])? + struct SendAsData: Equatable { + var isPremium: Bool + var availablePeers: [SendAsPeer] + + init(isPremium: Bool, availablePeers: [SendAsPeer]) { + self.isPremium = isPremium + self.availablePeers = availablePeers + } + } + + var sendAsData: SendAsData? var currentSendAsPeer: SendAsPeer? var isSelectingSendAsPeer: Bool = false var sendAsDisposable: Disposable? @@ -244,24 +254,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { availablePeers.append(peer) } - self.sendAsData = ( + let sendAsData = SendAsData( isPremium: isPremium, availablePeers: availablePeers ) - - if availablePeers.count > 1 { - if self.currentSendAsPeer == nil { - self.currentSendAsPeer = availablePeers.first - if !view.isUpdatingComponent { - view.state?.updated(transition: .spring(duration: 0.4)) - } - } - } else { - if self.currentSendAsPeer != nil { - self.currentSendAsPeer = nil - if !view.isUpdatingComponent { - view.state?.updated(transition: .spring(duration: 0.4)) - } + if self.sendAsData != sendAsData { + self.sendAsData = sendAsData + if !view.isUpdatingComponent { + view.state?.updated(transition: .spring(duration: 0.4)) } } }) @@ -684,6 +684,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { var sendPaidMessageStars = self.currentLiveStreamMessageStars var isAdmin = false + var sendAsPeer: SendAsPeer? if let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { if let liveChatStateValue = visibleItemView.liveChatState { if let minMessagePrice = liveChatStateValue.minMessagePrice { @@ -696,6 +697,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } isAdmin = liveChatStateValue.isAdmin + + if let currentSendAsPeer = self.currentSendAsPeer { + sendAsPeer = currentSendAsPeer + } else { + sendAsPeer = liveChatStateValue.defaultSendAs.flatMap { defaultSendAs in + return self.sendAsData?.availablePeers.first(where: { $0.peer.id == defaultSendAs }) + } + } } } @@ -758,7 +767,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let entities = generateChatInputTextEntities(text) - call.sendMessage(fromId: self.currentSendAsPeer?.peer.id, isAdmin: isAdmin, text: text.string, entities: entities, paidStars: sendPaidMessageStars?.value) + call.sendMessage(fromId: sendAsPeer?.peer.id, isAdmin: isAdmin, text: text.string, entities: entities, paidStars: sendPaidMessageStars?.value) component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id)) inputPanelView.clearSendMessageInput(updateState: true) @@ -3991,6 +4000,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { var topPeers: [ReactionsMessageAttribute.TopPeer] = [] var minAmount: Int64 = 1 + var sendAsPeer: SendAsPeer? if let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { if let topItems = visibleItemView.liveChatState?.starStats?.topItems { topPeers = topItems.map { item -> ReactionsMessageAttribute.TopPeer in @@ -4007,12 +4017,20 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if let minMessagePrice = visibleItemView.liveChatState?.minMessagePrice { minAmount = minMessagePrice } + + if let currentSendAsPeer = self.currentSendAsPeer { + sendAsPeer = currentSendAsPeer + } else { + sendAsPeer = visibleItemView.liveChatState?.defaultSendAs.flatMap { defaultSendAs in + return self.sendAsData?.availablePeers.first(where: { $0.peer.id == defaultSendAs }) + } + } } let initialData = await ChatSendStarsScreen.initialData( context: component.context, peerId: peerId, - myPeer: (self.currentSendAsPeer?.peer).flatMap(EnginePeer.init), + myPeer: (sendAsPeer?.peer).flatMap(EnginePeer.init), reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id, minAmount: Int(minAmount), liveChatMessageParams: LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 }))), topPeers: topPeers, completion: { [weak self, weak view] amount, _, _, _ in @@ -4245,17 +4263,38 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let call = itemView.mediaStreamCall else { return } + var sendAsPeer: SendAsPeer? if let liveChatStateValue = itemView.liveChatState { isAdmin = liveChatStateValue.isAdmin + + if let currentSendAsPeer = self.currentSendAsPeer { + sendAsPeer = currentSendAsPeer + } else { + sendAsPeer = liveChatStateValue.defaultSendAs.flatMap { defaultSendAs in + return self.sendAsData?.availablePeers.first(where: { $0.peer.id == defaultSendAs }) + } + } } - call.sendStars(fromId: self.currentSendAsPeer?.peer.id, isAdmin: isAdmin, amount: Int64(count), delay: delay) + call.sendStars(fromId: sendAsPeer?.peer.id, isAdmin: isAdmin, amount: Int64(count), delay: delay) } func openSendAsSelection(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) { - guard let component = view.component, let sendAsData = self.sendAsData, let currentSendAsPeer = self.currentSendAsPeer, let controller = component.controller() else { + guard let component = view.component, let sendAsData = self.sendAsData, let controller = component.controller() else { return } + guard let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View else { + return + } + + var currentSendAsPeer: SendAsPeer? + if let currentSendAsPeerValue = self.currentSendAsPeer { + currentSendAsPeer = currentSendAsPeerValue + } else { + currentSendAsPeer = visibleItemView.liveChatState?.defaultSendAs.flatMap { defaultSendAs in + return self.sendAsData?.availablePeers.first(where: { $0.peer.id == defaultSendAs }) + } + } let focusedItem = component.slice.item guard let peerId = focusedItem.peerId else { @@ -4269,7 +4308,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { context: component.context, chatPeerId: peerId, peers: sendAsData.availablePeers, - selectedPeerId: currentSendAsPeer.peer.id, + selectedPeerId: currentSendAsPeer?.peer.id, isPremium: isPremium, action: { [weak self, weak view] peer in guard let self, let view else {