From 3d51d83e890487a6565688fdbb60c8db9eca0567 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 11 Nov 2025 18:07:34 +0800 Subject: [PATCH] Stories --- .../Sources/PresentationCallManager.swift | 2 +- .../AvatarNode/Sources/AvatarNode.swift | 124 ------------------ .../Sources/Node/ChatListItem.swift | 110 +++++++++++++++- .../Sources/PresentationGroupCall.swift | 4 +- .../Sources/VideoChatScreen.swift | 2 +- .../TelegramEngine/Calls/GroupCalls.swift | 11 +- .../Sources/LiveStreamSettingsScreen.swift | 68 ++++++---- .../StoryItemSetContainerComponent.swift | 42 ++++-- ...StoryItemSetContainerViewSendMessage.swift | 34 +++++ .../Sources/StoryPeerListItemComponent.swift | 12 +- .../UIViewController+Navigation.m | 12 ++ .../WebUI/Sources/WebAppController.swift | 55 +++++++- 12 files changed, 305 insertions(+), 171 deletions(-) diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 0321b30b7a..62bde63548 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -525,7 +525,7 @@ public protocol PresentationGroupCall: AnyObject { func disableScreencast() func switchVideoCamera() func updateDefaultParticipantsAreMuted(isMuted: Bool) - func updateMessagesEnabled(isEnabled: Bool) + func updateMessagesEnabled(isEnabled: Bool, sendPaidMessageStars: Int64?) func setVolume(peerId: EnginePeer.Id, volume: Int32, sync: Bool) func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo]) func setSuspendVideoChannelRequests(_ value: Bool) diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 650588aa15..3567a7791e 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -1156,11 +1156,6 @@ public final class AvatarNode: ASDisplayNode { public let contentNode: ContentNode private var storyIndicator: ComponentView? - private var contentMaskView: UIView? - private var storyIndicatorMaskView: UIView? - private var liveBadgeMaskView: UIImageView? - private var liveBadgeStoryIndicatorMaskView: UIImageView? - private var liveBadgeView: UIImageView? public private(set) var storyPresentationParams: StoryPresentationParams? private var loadingStatuses = Bag() @@ -1188,7 +1183,6 @@ public final class AvatarNode: ASDisplayNode { } public private(set) var storyStats: StoryStats? - public var displayLiveBadge: Bool = false public var font: UIFont { get { @@ -1504,124 +1498,6 @@ public final class AvatarNode: ASDisplayNode { } } } - - if self.displayLiveBadge, let storyStats = self.storyStats, storyStats.hasLiveItems { - let contentMaskView: UIView - let storyIndicatorMaskView: UIView - let liveBadgeMaskView: UIImageView - let liveBadgeStoryIndicatorMaskView: UIImageView - let liveBadgeView: UIImageView - - var liveBadgeTransition = transition - - if let current = self.contentMaskView { - contentMaskView = current - } else { - liveBadgeTransition = liveBadgeTransition.withAnimation(.none) - contentMaskView = UIView() - contentMaskView.backgroundColor = .white - if let filter = CALayer.luminanceToAlpha() { - contentMaskView.layer.filters = [filter] - } - self.contentMaskView = contentMaskView - self.contentNode.view.mask = contentMaskView - } - - if let current = self.storyIndicatorMaskView { - storyIndicatorMaskView = current - } else { - storyIndicatorMaskView = UIView() - storyIndicatorMaskView.backgroundColor = .white - if let filter = CALayer.luminanceToAlpha() { - storyIndicatorMaskView.layer.filters = [filter] - } - self.storyIndicatorMaskView = storyIndicatorMaskView - self.storyIndicator?.view?.mask = storyIndicatorMaskView - } - - if let current = self.liveBadgeView { - liveBadgeView = current - } else { - liveBadgeView = UIImageView() - - //TODO:localize - let liveString = NSAttributedString(string: "LIVE", font: Font.semibold(10.0), textColor: .white) - let liveStringBounds = liveString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) - let liveBadgeSize = CGSize(width: ceil(liveStringBounds.width) + 4.0 * 2.0, height: ceil(liveStringBounds.height) + 2.0 * 2.0) - liveBadgeView.image = generateImage(liveBadgeSize, rotatedContext: { size, context in - UIGraphicsPushContext(context) - defer { - UIGraphicsPopContext() - } - - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(rgb: 0xFF2D55).cgColor) - context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: size.height * 0.5).cgPath) - context.fillPath() - - liveString.draw(at: CGPoint(x: floorToScreenPixels((size.width - liveStringBounds.width) * 0.5), y: floorToScreenPixels((size.height - liveStringBounds.height) * 0.5))) - }) - - self.view.addSubview(liveBadgeView) - self.liveBadgeView = liveBadgeView - } - - if let current = self.liveBadgeMaskView { - liveBadgeMaskView = current - } else { - liveBadgeMaskView = UIImageView() - self.liveBadgeMaskView = liveBadgeMaskView - contentMaskView.addSubview(liveBadgeMaskView) - - if let image = liveBadgeView.image { - liveBadgeMaskView.image = generateStretchableFilledCircleImage(diameter: image.size.height + 2.0 * 2.0, color: .black) - } - } - - if let current = self.liveBadgeStoryIndicatorMaskView { - liveBadgeStoryIndicatorMaskView = current - } else { - liveBadgeStoryIndicatorMaskView = UIImageView() - self.liveBadgeStoryIndicatorMaskView = liveBadgeStoryIndicatorMaskView - storyIndicatorMaskView.addSubview(liveBadgeStoryIndicatorMaskView) - - if let image = liveBadgeView.image { - liveBadgeStoryIndicatorMaskView.image = generateStretchableFilledCircleImage(diameter: image.size.height + 2.0 * 2.0, color: .black) - } - } - - liveBadgeTransition.setFrame(view: contentMaskView, frame: CGRect(origin: CGPoint(), size: size)) - liveBadgeTransition.setFrame(view: storyIndicatorMaskView, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -6.0, dy: -6.0)) - if let image = liveBadgeView.image { - let badgeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) * 0.5), y: size.height + 5.0 - image.size.height), size: image.size) - liveBadgeTransition.setFrame(view: liveBadgeView, frame: badgeFrame) - liveBadgeTransition.setFrame(view: liveBadgeMaskView, frame: self.contentNode.view.convert(badgeFrame.insetBy(dx: -2.0, dy: -2.0), from: self.view)) - liveBadgeTransition.setFrame(view: liveBadgeStoryIndicatorMaskView, frame: badgeFrame.insetBy(dx: -2.0, dy: -2.0).offsetBy(dx: 1.0, dy: 0.0)) - } - } else { - if let contentMaskView = self.contentMaskView { - self.contentMaskView = nil - contentMaskView.removeFromSuperview() - self.contentNode.view.mask = nil - } - if let storyIndicatorMaskView = self.storyIndicatorMaskView { - self.storyIndicatorMaskView = nil - storyIndicatorMaskView.removeFromSuperview() - self.storyIndicator?.view?.mask = nil - } - if let liveBadgeMaskView = self.liveBadgeMaskView { - self.liveBadgeMaskView = nil - liveBadgeMaskView.removeFromSuperview() - } - if let liveBadgeStoryIndicatorMaskView = self.liveBadgeStoryIndicatorMaskView { - self.liveBadgeStoryIndicatorMaskView = nil - liveBadgeStoryIndicatorMaskView.removeFromSuperview() - } - if let liveBadgeView = self.liveBadgeView { - self.liveBadgeView = nil - liveBadgeView.removeFromSuperview() - } - } } public func cancelLoading() { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 43ccb3faf1..6040bcc70c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1348,6 +1348,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let onlineNode: PeerOnlineMarkerNode var avatarTimerBadge: AvatarBadgeView? private var starView: StarView? + var avatarLiveBadge: (outline: UIImageView, foreground: UIImageView)? let pinnedIconNode: ASImageNode var secretIconNode: ASImageNode? var verifiedIconView: ComponentHostView? @@ -1601,7 +1602,6 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { self.avatarContainerNode = ASDisplayNode() self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) - self.avatarNode.displayLiveBadge = true self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true @@ -1802,6 +1802,92 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { ), transition: .immediate) self.avatarNode.isUserInteractionEnabled = storyState != nil + if let stats = storyState?.stats, stats.hasLiveItems { + if self.avatarLiveBadge == nil { + let avatarLiveBadge: (outline: UIImageView, foreground: UIImageView) = (UIImageView(), UIImageView()) + self.avatarLiveBadge = avatarLiveBadge + self.avatarNode.view.addSubview(avatarLiveBadge.outline) + self.avatarNode.view.addSubview(avatarLiveBadge.foreground) + + //TODO:localize + let liveString = NSAttributedString(string: "LIVE", font: Font.semibold(10.0), textColor: .white) + let liveStringBounds = liveString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + let liveBadgeSize = CGSize(width: ceil(liveStringBounds.width) + 4.0 * 2.0, height: ceil(liveStringBounds.height) + 2.0 * 2.0) + avatarLiveBadge.foreground.image = generateImage(liveBadgeSize, rotatedContext: { size, context in + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor(rgb: 0xFF2D55).cgColor) + + func roundedRectCgPath(roundRect rect: CGRect, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat) -> CGPath { + let path = CGMutablePath() + + let topLeft = rect.origin + let topRight = CGPoint(x: rect.maxX, y: rect.minY) + let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY) + let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY) + + if topLeftRadius != .zero { + path.move(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y)) + } else { + path.move(to: CGPoint(x: topLeft.x, y: topLeft.y)) + } + + if topRightRadius != .zero { + path.addLine(to: CGPoint(x: topRight.x-topRightRadius, y: topRight.y)) + path.addCurve(to: CGPoint(x: topRight.x, y: topRight.y+topRightRadius), control1: CGPoint(x: topRight.x, y: topRight.y), control2:CGPoint(x: topRight.x, y: topRight.y + topRightRadius)) + } else { + path.addLine(to: CGPoint(x: topRight.x, y: topRight.y)) + } + + if bottomRightRadius != .zero { + path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y-bottomRightRadius)) + path.addCurve(to: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y), control1: CGPoint(x: bottomRight.x, y: bottomRight.y), control2: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y)) + } else { + path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y)) + } + + if bottomLeftRadius != .zero { + path.addLine(to: CGPoint(x: bottomLeft.x+bottomLeftRadius, y: bottomLeft.y)) + path.addCurve(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius), control1: CGPoint(x: bottomLeft.x, y: bottomLeft.y), control2: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius)) + } else { + path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y)) + } + + if topLeftRadius != .zero { + path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y+topLeftRadius)) + path.addCurve(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y) , control1: CGPoint(x: topLeft.x, y: topLeft.y) , control2: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y)) + } else { + path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y)) + } + + path.closeSubpath() + + return path + } + + let radius = size.height * 0.5 + context.addPath(roundedRectCgPath(roundRect: CGRect(origin: CGPoint(), size: size), topLeftRadius: radius, topRightRadius: radius, bottomLeftRadius: radius, bottomRightRadius: radius)) + context.fillPath() + + liveString.draw(at: CGPoint(x: floorToScreenPixels((size.width - liveStringBounds.width) * 0.5), y: floorToScreenPixels((size.height - liveStringBounds.height) * 0.5))) + }) + + if let image = avatarLiveBadge.foreground.image { + avatarLiveBadge.outline.image = generateStretchableFilledCircleImage(diameter: image.size.height + 2.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + } + } + } else { + if let avatarLiveBadge = self.avatarLiveBadge { + self.avatarLiveBadge = nil + avatarLiveBadge.outline.removeFromSuperview() + avatarLiveBadge.foreground.removeFromSuperview() + } + } + if let peer = peer { var overrideImage: AvatarNodeImageOverride? if case let .peer(peerData) = item.content, peerData.customMessageListData != nil { @@ -2027,6 +2113,21 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { self.starView?.setOutlineColor(effectiveBackgroundColor, transition: transition) } } + + if let item = self.item { + if let avatarLiveBadge = self.avatarLiveBadge { + let effectiveBackgroundColor: UIColor + if item.isPinned { + effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + } else { + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor + } + + let highlightAlpha = self.highlightedBackgroundNode.supernode == nil ? 0.0 : self.highlightedBackgroundNode.alpha + let outlineColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor.mixedWith(effectiveBackgroundColor, alpha: 1.0 - highlightAlpha) + transition.updateTintColor(view: avatarLiveBadge.outline, color: outlineColor) + } + } } override public func tapped() { @@ -4019,6 +4120,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } transition.updateFrame(node: strongSelf.onlineNode, frame: onlineFrame) + if let avatarLiveBadge = strongSelf.avatarLiveBadge, let iconImage = avatarLiveBadge.foreground.image, let outlineImage = avatarLiveBadge.outline.image { + let outlineInset = (outlineImage.size.height - iconImage.size.height) * 0.5 + let liveBadgeFrame = CGRect(origin: CGPoint(x: floor((avatarFrame.width - iconImage.size.width) * 0.5), y: avatarFrame.height + 5.0 - iconImage.size.height), size: iconImage.size) + transition.updateFrame(view: avatarLiveBadge.foreground, frame: liveBadgeFrame) + transition.updateFrame(view: avatarLiveBadge.outline, frame: liveBadgeFrame.insetBy(dx: -outlineInset, dy: -outlineInset)) + } + let onlineInlineNavigationFraction: CGFloat = item.interaction.inlineNavigationLocation?.progress ?? 0.0 transition.updateAlpha(node: strongSelf.onlineNode, alpha: 1.0 - onlineInlineNavigationFraction) transition.updateSublayerTransformScale(node: strongSelf.onlineNode, scale: (1.0 - onlineInlineNavigationFraction) * 1.0 + onlineInlineNavigationFraction * 0.00001) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index d06ec6059f..e96e827ac2 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -3988,8 +3988,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.participantsContext?.updateDefaultParticipantsAreMuted(isMuted: isMuted) } - public func updateMessagesEnabled(isEnabled: Bool) { - self.participantsContext?.updateMessagesEnabled(isEnabled: isEnabled) + public func updateMessagesEnabled(isEnabled: Bool, sendPaidMessageStars: Int64?) { + self.participantsContext?.updateMessagesEnabled(isEnabled: isEnabled, sendPaidMessageStars: sendPaidMessageStars) } func video(endpointId: String) -> Signal? { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index e63f68e541..78319c7c7d 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -163,7 +163,7 @@ extension VideoChatCall { func setMessagesEnabled(isEnabled: Bool) { switch self { case let .group(group): - group.updateMessagesEnabled(isEnabled: isEnabled) + group.updateMessagesEnabled(isEnabled: isEnabled, sendPaidMessageStars: nil) case .conferenceSource: break } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 03edb0649a..598b58c096 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -2643,13 +2643,18 @@ public final class GroupCallParticipantsContext { })) } - public func updateMessagesEnabled(isEnabled: Bool) { - if isEnabled == self.stateValue.state.messagesAreEnabled.isEnabled { + public func updateMessagesEnabled(isEnabled: Bool, sendPaidMessageStars: Int64?) { + if isEnabled == self.stateValue.state.messagesAreEnabled.isEnabled && self.stateValue.state.messagesAreEnabled.sendPaidMessagesStars == sendPaidMessageStars { return } self.stateValue.state.messagesAreEnabled.isEnabled = isEnabled + self.stateValue.state.messagesAreEnabled.sendPaidMessagesStars = sendPaidMessageStars - self.updateMessagesEnabledDisposable.set((self.account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 2, call: self.reference.apiInputGroupCall, joinMuted: nil, messagesEnabled: isEnabled ? .boolTrue : .boolFalse, sendPaidMessagesStars: nil)) + var flags: Int32 = 1 << 2 + if sendPaidMessageStars != nil { + flags |= 1 << 3 + } + self.updateMessagesEnabledDisposable.set((self.account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 2, call: self.reference.apiInputGroupCall, joinMuted: nil, messagesEnabled: isEnabled ? .boolTrue : .boolFalse, sendPaidMessagesStars: sendPaidMessageStars)) |> deliverOnMainQueue).start(next: { [weak self] updates in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/LiveStreamSettingsScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/LiveStreamSettingsScreen.swift index 9a2ddddc15..0655f7cfba 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/LiveStreamSettingsScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/LiveStreamSettingsScreen.swift @@ -349,7 +349,14 @@ final class LiveStreamSettingsScreenComponent: Component { contentHeight += sectionSpacing } - if screenState.sendAsPeerId?.namespace != Namespaces.Peer.CloudChannel { + var displayPrivacy = true + if screenState.sendAsPeerId?.namespace == Namespaces.Peer.CloudChannel { + displayPrivacy = false + } else if screenState.call != nil && screenState.isEdit { + displayPrivacy = false + } + + if displayPrivacy { var privacySectionItems: [AnyComponentWithIdentity] = [] var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = [] @@ -659,7 +666,7 @@ final class LiveStreamSettingsScreenComponent: Component { }) } - if !screenState.isEdit { + if !screenState.isEdit || (screenState.call != nil && screenState.isEdit) { let externalStreamSectionItems = [AnyComponentWithIdentity(id: 0, component: AnyComponent( ListActionItemComponent( theme: theme, @@ -703,6 +710,7 @@ final class LiveStreamSettingsScreenComponent: Component { } + //TODO:localize var settingsSectionItems: [AnyComponentWithIdentity] = [] settingsSectionItems.append(AnyComponentWithIdentity(id: "comments", component: AnyComponent(ListActionItemComponent( theme: theme, @@ -725,26 +733,29 @@ final class LiveStreamSettingsScreenComponent: Component { action: nil )))) - settingsSectionItems.append(AnyComponentWithIdentity(id: "screenshots", component: AnyComponent(ListActionItemComponent( - theme: theme, - style: .glass, - title: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: "Allow Screenshots", - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: theme.list.itemPrimaryTextColor + if !(screenState.call != nil && screenState.isEdit) { + //TODO:localize + settingsSectionItems.append(AnyComponentWithIdentity(id: "screenshots", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Allow Screenshots", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 )), - maximumNumberOfLines: 1 - )), - accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: !screenState.isForwardingDisabled, action: { [weak self] _ in - guard let self, let component = self.component else { - return - } - component.stateContext.isForwardingDisabled = !component.stateContext.isForwardingDisabled - self.state?.updated(transition: .spring(duration: 0.4)) - })), - action: nil - )))) + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: !screenState.isForwardingDisabled, action: { [weak self] _ in + guard let self, let component = self.component else { + return + } + component.stateContext.isForwardingDisabled = !component.stateContext.isForwardingDisabled + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + )))) + } let settingsSectionSize = self.settingsSection.update( transition: transition, @@ -1025,7 +1036,7 @@ final class LiveStreamSettingsScreenComponent: Component { public class LiveStreamSettingsScreen: ViewControllerComponentContainer { public enum Mode { case create(sendAsPeerId: EnginePeer.Id?, isCustomTarget: Bool, privacy: EngineStoryPrivacy, allowComments: Bool, isForwardingDisabled: Bool, pin: Bool, paidMessageStars: Int64) - case edit(PresentationGroupCall) + case edit(call: PresentationGroupCall, displayPrivacy: Bool) } public struct Result { @@ -1088,6 +1099,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer { } final class State { + var call: PresentationGroupCall? var isEdit: Bool var maxPaidMessageStars: Int64 var sendAsPeerId: EnginePeer.Id? @@ -1105,6 +1117,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer { var grayListPeers: [EnginePeer] init( + call: PresentationGroupCall?, isEdit: Bool, maxPaidMessageStars: Int64, sendAsPeerId: EnginePeer.Id?, @@ -1121,6 +1134,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer { closeFriendsPeers: [EnginePeer], grayListPeers: [EnginePeer] ) { + self.call = call self.isEdit = isEdit self.maxPaidMessageStars = maxPaidMessageStars self.sendAsPeerId = sendAsPeerId @@ -1140,6 +1154,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer { } public final class StateContext { + let mode: LiveStreamSettingsScreen.Mode let blockedPeersContext: BlockedPeersContext? var stateValue: State? @@ -1211,6 +1226,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer { adminedChannels: Signal<[EnginePeer], NoError>, blockedPeersContext: BlockedPeersContext? ) { + self.mode = mode self.blockedPeersContext = blockedPeersContext let grayListPeers: Signal<[EnginePeer], NoError> @@ -1337,6 +1353,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer { savedSelectedPeers[.contacts] = contactsPeers savedSelectedPeers[.nobody] = selectedPeers + let call: PresentationGroupCall? let isEdit: Bool let maxPaidMessageStars: Int64 = 10000 let sendAsPeerId: EnginePeer.Id? @@ -1348,6 +1365,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer { let paidMessageStars: Int64 if let current = self.stateValue { + call = current.call isEdit = current.isEdit sendAsPeerId = current.sendAsPeerId isCustomTarget = current.isCustomTarget @@ -1359,6 +1377,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer { } else { switch mode { case let .create(sendAsPeerIdValue, isCustomTargetValue, privacyValue, allowCommentsValue, isForwardingDisabledValue, pinValue, paidMessageStarsValue): + call = nil isEdit = false sendAsPeerId = sendAsPeerIdValue isCustomTarget = isCustomTargetValue @@ -1367,8 +1386,8 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer { isForwardingDisabled = isForwardingDisabledValue pin = pinValue paidMessageStars = paidMessageStarsValue - case let .edit(call): - let _ = call + case let .edit(callValue, _): + call = callValue isEdit = true sendAsPeerId = nil isCustomTarget = false @@ -1381,6 +1400,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer { } let state = State( + call: call, isEdit: isEdit, maxPaidMessageStars: maxPaidMessageStars, sendAsPeerId: sendAsPeerId, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index c0405ea8fc..c3d73730de 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -4249,23 +4249,13 @@ public final class StoryItemSetContainerComponent: Component { var currentLeftInfoItem: InfoItem? if focusedItem != nil { let leftInfoComponent = AnyComponent(StoryAvatarInfoComponent(context: component.context, peer: component.slice.effectivePeer, isLiveStream: isLiveStream)) - if let leftInfoItem = self.leftInfoItem, leftInfoItem.component == leftInfoComponent { + if let leftInfoItem = self.leftInfoItem { currentLeftInfoItem = leftInfoItem } else { currentLeftInfoItem = InfoItem(component: leftInfoComponent) } } - if let leftInfoItem = self.leftInfoItem, currentLeftInfoItem?.component != leftInfoItem.component { - self.leftInfoItem = nil - if let view = leftInfoItem.view.view { - view.layer.animateScale(from: 1.0, to: 0.5, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in - view?.removeFromSuperview() - }) - } - } - var currentCenterInfoItem: InfoItem? if let focusedItem { var counters: StoryAuthorInfoComponent.Counters? @@ -4295,7 +4285,7 @@ public final class StoryItemSetContainerComponent: Component { isLiveStream: isLiveStream, customSubtitle: customSubtitle )) - if let centerInfoItem = self.centerInfoItem, centerInfoItem.component == centerInfoComponent { + if let centerInfoItem = self.centerInfoItem { currentCenterInfoItem = centerInfoItem } else { currentCenterInfoItem = InfoItem(component: centerInfoComponent) @@ -6644,6 +6634,19 @@ public final class StoryItemSetContainerComponent: Component { } if case .liveStream = component.slice.item.storyItem.media { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Live Settings", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + + self.sendMessageContext.displayLiveStreamSettings(view: self) + }))) + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, a in @@ -6921,6 +6924,21 @@ public final class StoryItemSetContainerComponent: Component { component.controller()?.present(tooltipScreen, in: .current) }))) + if channel.hasPermission(.postStories) { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Live Settings", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + + self.sendMessageContext.displayLiveStreamSettings(view: self) + }))) + } + if (component.slice.item.storyItem.isMy && channel.hasPermission(.postStories)) || channel.hasPermission(.deleteStories) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextDeleteStory, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index e6eb19d47f..ec3dbefb13 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -53,6 +53,7 @@ import ChatMessagePaymentAlertController import ChatSendStarsScreen import AnimatedTextComponent import ChatSendAsContextMenu +import ShareWithPeersScreen private var ObjCKey_DeinitWatcher: Int? @@ -4391,6 +4392,39 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.isSelectingSendAsPeer = true view.state?.updated(transition: .spring(duration: 0.4)) } + + func displayLiveStreamSettings(view: StoryItemSetContainerComponent.View) { + Task { @MainActor [weak self, weak view] in + guard let self, let view, let component = view.component, let controller = component.controller(), let mediaStreamCall = view.mediaStreamCall else { + return + } + + let stateContext = LiveStreamSettingsScreen.StateContext( + context: component.context, + mode: .edit(call: mediaStreamCall, displayPrivacy: false), + closeFriends: component.closeFriends.get(), + adminedChannels: .single([]), + blockedPeersContext: nil + ) + let _ = await (stateContext.ready |> filter { $0 } |> take(1)).get() + let settingsScreen = LiveStreamSettingsScreen( + context: component.context, + stateContext: stateContext, + editCategory: { _, _, _, _, _ in + }, + editBlockedPeers: { _, _, _, _, _ in + }, + completion: { [weak self, weak view] result in + guard let self, let view, let call = view.mediaStreamCall else { + return + } + let _ = self + call.updateMessagesEnabled(isEnabled: result.allowComments, sendPaidMessageStars: result.paidMessageStars) + } + ) + controller.push(settingsScreen) + } + } } public class StoryProgressPauseContext { diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 6b1059505a..044870b6e1 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -843,12 +843,22 @@ public final class StoryPeerListItemComponent: Component { if let image = avatarLiveBadgeView.image { let badgeSize = image.size let badgeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((avatarFrame.width - badgeSize.width) * 0.5), y: avatarFrame.height + 5.0 - badgeSize.height), size: badgeSize) - avatarLiveBadgeTransition.setFrame(view: avatarLiveBadgeView, frame: badgeFrame) + avatarLiveBadgeTransition.setPosition(view: avatarLiveBadgeView, position: badgeFrame.center) + avatarLiveBadgeTransition.setBounds(view: avatarLiveBadgeView, bounds: CGRect(origin: CGPoint(), size: badgeFrame.size)) + + avatarLiveBadgeTransition.setScale(view: avatarLiveBadgeView, scale: max(0.001, component.expandedAlphaFraction)) + avatarLiveBadgeTransition.setAlpha(view: avatarLiveBadgeView, alpha: component.expandedAlphaFraction) avatarLiveBadgeMaskSeenLayer.frame = badgeFrame.offsetBy(dx: 8.0, dy: 8.0).insetBy(dx: -2.0, dy: -2.0) avatarLiveBadgeMaskSeenLayer.cornerRadius = avatarLiveBadgeMaskSeenLayer.bounds.height * 0.5 avatarLiveBadgeMaskUnseenLayer.frame = badgeFrame.offsetBy(dx: 8.0, dy: 8.0).insetBy(dx: -2.0, dy: -2.0) avatarLiveBadgeMaskUnseenLayer.cornerRadius = avatarLiveBadgeMaskUnseenLayer.bounds.height * 0.5 + + avatarLiveBadgeTransition.setScale(layer: avatarLiveBadgeMaskSeenLayer, scale: max(0.001, component.expandedAlphaFraction)) + avatarLiveBadgeTransition.setAlpha(layer: avatarLiveBadgeMaskSeenLayer, alpha: component.expandedAlphaFraction) + + avatarLiveBadgeTransition.setScale(layer: avatarLiveBadgeMaskUnseenLayer, scale: max(0.001, component.expandedAlphaFraction)) + avatarLiveBadgeTransition.setAlpha(layer: avatarLiveBadgeMaskUnseenLayer, alpha: component.expandedAlphaFraction) } } else { if let avatarLiveBadgeView = self.avatarLiveBadgeView { diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m index 04298f70af..11b0a7530d 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m @@ -254,6 +254,18 @@ static void registerEffectViewOverrides(void) { if (@available(iOS 26.0, *)) { registerEffectViewOverrides(); } + + #if DEBUG + Class cls = NSClassFromString(@"WKBrowsingContextController"); + SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); + if ([cls respondsToSelector:sel]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [cls performSelector:sel withObject:@"http"]; + [cls performSelector:sel withObject:@"https"]; +#pragma clang diagnostic pop + } + #endif }); } diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index b92d8db2bc..af47653c60 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -139,6 +139,53 @@ public func generateWebAppThemeParams(_ theme: PresentationTheme) -> [String: An ] } +#if DEBUG +private let registeredProtocols: Void = { + /*class AppURLProtocol: URLProtocol { + var urlTask: URLSessionDataTask? + + override class func canInit(with request: URLRequest) -> Bool { + if request.url?.scheme == "https" { + return false + } + return false + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + super.startLoading() + + /*if self.urlTask != nil { + return + } + self.urlTask = URLSession.shared.dataTask(with: self.request, completionHandler: { [weak self] _, response, error in + guard let self else { + return + } + if let error { + self.client?.urlProtocol(self, didFailWithError: error) + } else { + if let response = response as? HTTPURLResponse { + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + } else { + } + self.client?.urlProtocolDidFinishLoading(self) + } + }) + self.urlTask?.resume()*/ + } + + override func stopLoading() { + self.urlTask?.cancel() + } + } + URLProtocol.registerClass(AppURLProtocol.self)*/ +}() +#endif + public final class WebAppController: ViewController, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } @@ -203,6 +250,10 @@ public final class WebAppController: ViewController, AttachmentContainable { private var validLayout: (ContainerViewLayout, CGFloat)? init(context: AccountContext, controller: WebAppController) { + #if DEBUG + let _ = registeredProtocols + #endif + self.context = context self.controller = controller self.presentationData = controller.presentationData @@ -428,7 +479,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } private func load(url: URL) { - #if DEBUG + /*#if DEBUG if "".isEmpty { if #available(iOS 16.0, *) { let documentsPath = URL.documentsDirectory.path(percentEncoded: false) @@ -464,7 +515,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return } - #endif + #endif*/ self.webView?.load(URLRequest(url: url)) }