diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 7ab83c663f..15b6a429e2 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1306,6 +1306,10 @@ public struct ComponentTransition { } public func animateBlur(layer: CALayer, fromRadius: CGFloat, toRadius: CGFloat, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + if case .none = self.animation { + return + } + if let blurFilter = CALayer.blur() { blurFilter.setValue(toRadius as NSNumber, forKey: "inputRadius") layer.filters = [blurFilter] diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 1cf0e4ab0d..90c815b381 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -4079,9 +4079,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } - public func deleteMessage(id: GroupCallMessagesContext.Message.Id) { + public func deleteMessage(id: GroupCallMessagesContext.Message.Id, reportSpam: Bool) { if let messagesContext = self.messagesContext { - messagesContext.deleteMessage(id: id) + messagesContext.deleteMessage(id: id, reportSpam: reportSpam) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 8ed4f7fce1..6b348c2229 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -4465,7 +4465,7 @@ public final class GroupCallMessagesContext { }) } - func deleteMessage(id: Message.Id) { + func deleteMessage(id: Message.Id, reportSpam: Bool) { var updatedState: State? if let index = self.state.messages.firstIndex(where: { $0.id == id }) { if updatedState == nil { @@ -4482,6 +4482,12 @@ public final class GroupCallMessagesContext { if let updatedState { self.state = updatedState } + + var flags: Int32 = 0 + if reportSpam { + flags |= 1 << 0 + } + let _ = self.account.network.request(Api.functions.phone.deleteGroupCallMessages(flags: flags, call: self.reference.apiInputGroupCall, messages: [Int32(clamping: id.id)])).startStandalone() } } @@ -4526,9 +4532,9 @@ public final class GroupCallMessagesContext { } } - public func deleteMessage(id: Message.Id) { + public func deleteMessage(id: Message.Id, reportSpam: Bool) { self.impl.with { impl in - impl.deleteMessage(id: id) + impl.deleteMessage(id: id, reportSpam: reportSpam) } } diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index b7c09062aa..414cd30cd0 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -427,6 +427,14 @@ private final class AdminUserActionsSheetComponent: Component { ) } + private func calculateLiveStreamResult() -> AdminUserActionsSheet.LiveStreamResult { + return AdminUserActionsSheet.LiveStreamResult( + reportSpam: !self.optionReportSelectedPeers.isEmpty, + deleteAll: !self.optionDeleteAllSelectedPeers.isEmpty, + ban: !self.optionBanSelectedPeers.isEmpty + ) + } + private func updateScrolling(transition: ComponentTransition) { guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { return @@ -578,7 +586,7 @@ private final class AdminUserActionsSheetComponent: Component { if themeUpdated { self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor + self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor @@ -663,6 +671,9 @@ private final class AdminUserActionsSheetComponent: Component { } } } + case .liveStream: + availableOptions.append(.deleteAll) + availableOptions.append(.ban) } let optionsItem: (OptionsSection) -> AnyComponentWithIdentity = { section in @@ -912,6 +923,13 @@ private final class AdminUserActionsSheetComponent: Component { titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount)) } } + case let .liveStream(messageCount, deleteAllMessageCount, _): + titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(messageCount)) + if let deleteAllMessageCount { + if self.optionDeleteAllSelectedPeers == Set(component.peers.map(\.peer.id)) { + titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount)) + } + } } let titleSize = self.title.update( @@ -965,6 +983,7 @@ private final class AdminUserActionsSheetComponent: Component { transition: optionsSectionTransition, component: AnyComponent(ListSectionComponent( theme: environment.theme, + style: .glass, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.Chat_AdminActionSheet_RestrictSectionHeader, @@ -1042,6 +1061,7 @@ private final class AdminUserActionsSheetComponent: Component { } if case let .channel(channel) = component.chatPeer, channel.isMonoForum { + } else if case .liveStream = component.mode { } else { var allConfigItems: [(ConfigItem, Bool)] = [] if !self.allowedMediaRights.isEmpty || !self.allowedParticipantRights.isEmpty { @@ -1362,9 +1382,11 @@ private final class AdminUserActionsSheetComponent: Component { transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, - pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 54.0 * 0.5 ), content: AnyComponentWithIdentity( id: AnyHashable(0), @@ -1389,11 +1411,13 @@ private final class AdminUserActionsSheetComponent: Component { completion(self.calculateMonoforumResult()) case let .chat(_, _, completion): completion(self.calculateChatResult()) + case let .liveStream(_, _, completion): + completion(self.calculateLiveStreamResult()) } } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 54.0) ) let bottomPanelHeight = 8.0 + environment.safeInsets.bottom + actionButtonSize.height let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) @@ -1462,6 +1486,7 @@ private final class AdminUserActionsSheetComponent: Component { public class AdminUserActionsSheet: ViewControllerComponentContainer { public enum Mode { case chat(messageCount: Int, deleteAllMessageCount: Int?, completion: (ChatResult) -> Void) + case liveStream(messageCount: Int, deleteAllMessageCount: Int?, completion: (LiveStreamResult) -> Void) case monoforum(completion: (MonoforumResult) -> Void) } @@ -1479,6 +1504,18 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer { } } + public final class LiveStreamResult { + public let reportSpam: Bool + public let deleteAll: Bool + public let ban: Bool + + init(reportSpam: Bool, deleteAll: Bool, ban: Bool) { + self.reportSpam = reportSpam + self.deleteAll = deleteAll + self.ban = ban + } + } + public final class MonoforumResult { public let ban: Bool public let reportSpam: Bool @@ -1493,9 +1530,9 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer { private var isDismissed: Bool = false - public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], mode: Mode) { + public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], mode: Mode, customTheme: PresentationTheme? = nil) { self.context = context - super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, mode: mode), navigationBarAppearance: .none) + super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, mode: mode), navigationBarAppearance: .none, theme: customTheme.flatMap({ .custom($0) }) ?? .default) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD index 2118c650c4..0d53fbe7c2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD @@ -27,6 +27,9 @@ swift_library( "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/ComponentFlow", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramUI/Components/GlassControls", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index 0e394297a6..7f11567782 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -18,8 +18,11 @@ import TelegramNotices import GlassBackgroundComponent import ComponentFlow import ComponentDisplayAdapters +import GlassControls +import BundleIconComponent +import MultilineTextComponent -private enum SubscriberAction: Equatable { +private enum SubscriberAction: Equatable, Hashable { case join case joinGroup case applyToJoin @@ -143,7 +146,10 @@ private func actionForPeer(context: AccountContext, peer: Peer, interfaceState: private let badgeFont = Font.regular(14.0) public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { - private let buttonBackgroundView: GlassBackgroundView + private let panelContainer = UIView() + private let panel = ComponentView() + + /*private let buttonBackgroundView: GlassBackgroundView private let button: HighlightableButton private let buttonTitle: ImmediateTextNode private let buttonTintTitle: ImmediateTextNode @@ -158,7 +164,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { private let suggestedPostButtonBackgroundView: GlassBackgroundView private let suggestedPostButton: HighlightableButton - private let suggestedPostButtonIconView: UIImageView + private let suggestedPostButtonIconView: UIImageView*/ private var action: SubscriberAction? @@ -171,7 +177,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { private var layoutData: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, CGFloat, Bool, LayoutMetrics)? public override init() { - self.button = HighlightableButton() + /*self.button = HighlightableButton() self.buttonBackgroundView = GlassBackgroundView() self.buttonBackgroundView.isUserInteractionEnabled = false self.buttonTitle = ImmediateTextNode() @@ -203,18 +209,20 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { self.suggestedPostButtonIconView = GlassBackgroundView.ContentImageView() self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButtonIconView) self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButton) - self.suggestedPostButtonBackgroundView.isHidden = true + self.suggestedPostButtonBackgroundView.isHidden = true*/ super.init() - self.view.addSubview(self.buttonBackgroundView) + /*self.view.addSubview(self.buttonBackgroundView) self.view.addSubview(self.helpButtonBackgroundView) self.view.addSubview(self.giftButtonBackgroundView) self.view.addSubview(self.suggestedPostButtonBackgroundView) self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) self.helpButton.addTarget(self, action: #selector(self.helpPressed), for: .touchUpInside) self.giftButton.addTarget(self, action: #selector(self.giftPressed), for: .touchUpInside) - self.suggestedPostButton.addTarget(self, action: #selector(self.suggestedPostPressed), for: .touchUpInside) + self.suggestedPostButton.addTarget(self, action: #selector(self.suggestedPostPressed), for: .touchUpInside)*/ + + self.view.addSubview(self.panelContainer) } deinit { @@ -330,11 +338,17 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } #endif*/ - if giftCount < 2 && !self.giftButton.isHidden { + let giftItemView = (self.panel.view as? GlassControlPanelComponent.View)?.leftItemView?.itemView(id: AnyHashable("gift")) + let suggestPostItemView = (self.panel.view as? GlassControlPanelComponent.View)?.leftItemView?.itemView(id: AnyHashable("suggestPost")) + + if giftCount < 2, let giftItemView { let _ = ApplicationSpecificNotice.incrementChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager).start() - Queue.mainQueue().after(0.4, { - let absoluteFrame = self.giftButton.convert(self.giftButton.bounds, to: parentController.view) + Queue.mainQueue().after(0.4, { [weak giftItemView] in + guard let giftItemView else { + return + } + let absoluteFrame = giftItemView.convert(giftItemView.bounds, to: parentController.view) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY), size: CGSize()) let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -357,11 +371,14 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { ) self.interfaceInteraction?.presentControllerInCurrent(tooltipController, nil) }) - } else if suggestCount < 2 && !self.suggestedPostButton.isHidden { + } else if suggestCount < 2, let suggestPostItemView { let _ = ApplicationSpecificNotice.incrementChannelSuggestTooltip(accountManager: context.sharedContext.accountManager).start() - Queue.mainQueue().after(0.4, { - let absoluteFrame = self.suggestedPostButton.convert(self.suggestedPostButton.bounds, to: parentController.view) + Queue.mainQueue().after(0.4, { [weak suggestPostItemView] in + guard let suggestPostItemView else { + return + } + let absoluteFrame = suggestPostItemView.convert(suggestPostItemView.bounds, to: parentController.view) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY), size: CGSize()) let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -394,7 +411,133 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { let isFirstTime = self.layoutData == nil self.layoutData = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, isSecondary, metrics) - if self.presentationInterfaceState != interfaceState || force { + var transition = transition + if !isFirstTime && !transition.isAnimated { + transition = .animated(duration: 0.4, curve: .spring) + } + + self.presentationInterfaceState = interfaceState + + var centerAction: (title: String, isAccent: Bool)? + if let context = self.context, let peer = interfaceState.renderedPeer?.peer, let action = actionForPeer(context: context, peer: peer, interfaceState: interfaceState, isJoining: self.isJoining, isMuted: interfaceState.peerIsMuted) { + self.action = action + let (title, _) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings) + + var isAccent = false + if case .join = self.action { + isAccent = true + } + centerAction = (title, isAccent) + } + + var displayGift = false + var displaySuggestPost = false + var displayHelp = false + + if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel { + if case .broadcast = peer.info, interfaceState.starGiftsAvailable { + displayGift = true + } + if case let .broadcast(broadcastInfo) = peer.info, broadcastInfo.flags.contains(.hasMonoforum) { + displaySuggestPost = true + } + if peer.flags.contains(.isGigagroup), self.action == .muteNotifications || self.action == .unmuteNotifications { + displayHelp = true + } + } + + var leftInset = leftInset + 8.0 + var rightInset = rightInset + 8.0 + if bottomInset <= 32.0 { + leftInset += 18.0 + rightInset += 18.0 + } + + var leftPanelItems: [GlassControlGroupComponent.Item] = [] + if displaySuggestPost { + leftPanelItems.append(GlassControlGroupComponent.Item( + id: "suggestPost", + content: .icon("Chat/Input/Accessory Panels/SuggestPost"), + action: { [weak self] in + self?.suggestedPostPressed() + } + )) + } + if displayGift { + leftPanelItems.append(GlassControlGroupComponent.Item( + id: "gift", + content: .icon("Chat/Input/Accessory Panels/Gift"), + action: { [weak self] in + self?.giftPressed() + } + )) + } + if displayHelp { + leftPanelItems.append(GlassControlGroupComponent.Item( + id: "help", + content: .icon("Chat/Input/Accessory Panels/Help"), + action: { [weak self] in + self?.helpPressed() + } + )) + } + + var centerPanelItem: GlassControlPanelComponent.Item? + if let centerAction { + centerPanelItem = GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: 0, + content: .text(centerAction.title), + action: { [weak self] in + self?.buttonPressed() + } + )], + background: centerAction.isAccent ? .activeTint : .panel + ) + } + + var rightPanelItems: [GlassControlGroupComponent.Item] = [] + rightPanelItems.append(GlassControlGroupComponent.Item( + id: "search", + content: .icon("Chat List/SearchIcon"), + action: { [weak self] in + guard let self else { + return + } + self.interfaceInteraction?.beginMessageSearch(.everything, "") + } + )) + + let panelHeight = defaultHeight(metrics: metrics) + let _ = isFirstTime + let panelFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: width - leftInset - rightInset, height: panelHeight)) + + let _ = self.panel.update( + transition: ComponentTransition(transition), + component: AnyComponent(GlassControlPanelComponent( + theme: interfaceState.theme, + leftItem: leftPanelItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: leftPanelItems, + background: .panel + ), + centralItem: centerPanelItem, + rightItem: rightPanelItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: rightPanelItems, + background: .panel + ) + )), + environment: {}, + containerSize: panelFrame.size + ) + if let panelView = self.panel.view { + if panelView.superview == nil { + self.panelContainer.addSubview(panelView) + } + transition.updateFrame(view: self.panelContainer, frame: panelFrame) + transition.updateFrame(view: panelView, frame: CGRect(origin: CGPoint(), size: panelFrame.size)) + } + + /*if self.presentationInterfaceState != interfaceState || force { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState @@ -504,7 +647,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { transition.updateFrame(view: self.suggestedPostButtonIconView, frame: image.size.centered(in: CGRect(origin: CGPoint(), size: suggestedPostButtonFrame.size))) } transition.updateFrame(view: self.suggestedPostButton, frame: CGRect(origin: CGPoint(), size: suggestedPostButtonFrame.size)) - self.suggestedPostButtonBackgroundView.update(size: suggestedPostButtonFrame.size, cornerRadius: suggestedPostButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition)) + self.suggestedPostButtonBackgroundView.update(size: suggestedPostButtonFrame.size, cornerRadius: suggestedPostButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition))*/ return panelHeight } diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 41982ac68f..faa89c9e21 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -2094,6 +2094,8 @@ private final class ChatSendStarsScreenComponent: Component { let separatorSpacing: CGFloat = 10.0 transition.setFrame(layer: topPeersLeftSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: separatorY), size: CGSize(width: max(0.0, topPeersBackgroundFrame.minX - separatorSpacing - sideInset), height: UIScreenPixel))) transition.setFrame(layer: topPeersRightSeparator, frame: CGRect(origin: CGPoint(x: topPeersBackgroundFrame.maxX + separatorSpacing, y: separatorY), size: CGSize(width: max(0.0, availableSize.width - sideInset - (topPeersBackgroundFrame.maxX + separatorSpacing)), height: UIScreenPixel))) + + contentHeight += 60.0 } var mappedTopPeers = reactData.topPeers @@ -2261,7 +2263,7 @@ private final class ChatSendStarsScreenComponent: Component { itemComponentView.alpha = 0.0 } - let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 60.0), size: itemSize) + let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight), size: itemSize) if animateItem { itemPositionTransition.setPosition(view: itemComponentView, position: itemFrame.center) @@ -2277,7 +2279,7 @@ private final class ChatSendStarsScreenComponent: Component { itemX += itemSize.width + itemSpacing } - contentHeight += 164.0 + contentHeight += 104.0 } if !reactData.topPeers.isEmpty { diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index c7097060af..ae42042d98 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -2956,7 +2956,10 @@ 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)) - transition.updateFrame(node: self.sendAsAvatarButtonNode, frame: sendAsButtonFrame) + 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.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) @@ -3000,7 +3003,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } var mediaActionButtonsFrame = CGRect(origin: CGPoint(x: textInputContainerBackgroundFrame.maxX + 6.0, y: textInputContainerBackgroundFrame.maxY - mediaActionButtonsSize.height), size: mediaActionButtonsSize) - if inputHasText || self.extendedSearchLayout || hasMediaDraft { + if inputHasText || self.extendedSearchLayout || hasMediaDraft || interfaceState.interfaceState.forwardMessageIds != nil { mediaActionButtonsFrame.origin.x = width + 8.0 } transition.updateFrame(node: self.mediaActionButtons, frame: mediaActionButtonsFrame) diff --git a/submodules/TelegramUI/Components/GlassControls/BUILD b/submodules/TelegramUI/Components/GlassControls/BUILD new file mode 100644 index 0000000000..0b4fa6a2e7 --- /dev/null +++ b/submodules/TelegramUI/Components/GlassControls/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GlassControls", + module_name = "GlassControls", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift new file mode 100644 index 0000000000..f4d3c15e56 --- /dev/null +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift @@ -0,0 +1,236 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import GlassBackgroundComponent +import PlainButtonComponent +import BundleIconComponent +import MultilineTextComponent + +public final class GlassControlGroupComponent: Component { + public final class Item: Equatable { + public enum Content: Hashable { + case icon(String) + case text(String) + } + + public let id: AnyHashable + public let content: Content + public let action: (() -> Void)? + + public init(id: AnyHashable, content: Content, action: (() -> Void)?) { + self.id = id + self.content = content + self.action = action + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.content != rhs.content { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + } + + public enum Background { + case panel + case activeTint + } + + public let theme: PresentationTheme + public let background: Background + public let items: [Item] + public let minWidth: CGFloat + + public init( + theme: PresentationTheme, + background: Background, + items: [Item], + minWidth: CGFloat + ) { + self.theme = theme + self.background = background + self.items = items + self.minWidth = minWidth + } + + public static func ==(lhs: GlassControlGroupComponent, rhs: GlassControlGroupComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.background != rhs.background { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.minWidth != rhs.minWidth { + return false + } + return true + } + + public final class View: UIView { + private let backgroundView: GlassBackgroundView + private var itemViews: [AnyHashable: ComponentView] = [:] + + private var component: GlassControlGroupComponent? + private weak var state: EmptyComponentState? + + override public init(frame: CGRect) { + self.backgroundView = GlassBackgroundView() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func itemView(id: AnyHashable) -> UIView? { + return self.itemViews[id]?.view + } + + func update(component: GlassControlGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) + + self.component = component + self.state = state + + struct ItemId: Hashable { + var id: AnyHashable + var contentId: AnyHashable + + init(id: AnyHashable, contentId: AnyHashable) { + self.id = id + self.contentId = contentId + } + } + + var contentsWidth: CGFloat = 0.0 + var validIds: [AnyHashable] = [] + var isInteractive = false + for item in component.items { + let itemId = ItemId(id: item.id, contentId: item.content) + + validIds.append(itemId) + + let itemView: ComponentView + var itemTransition = transition + if let current = self.itemViews[itemId] { + itemView = current + } else { + itemView = ComponentView() + self.itemViews[itemId] = itemView + itemTransition = itemTransition.withAnimation(.none) + } + + if item.action != nil { + isInteractive = true + } + + let content: AnyComponent + var itemInsets = UIEdgeInsets() + switch item.content { + case let .icon(name): + content = AnyComponent(BundleIconComponent( + name: name, + tintColor: component.background == .activeTint ? component.theme.list.itemCheckColors.foregroundColor : component.theme.chat.inputPanel.panelControlColor + )) + case let .text(string): + content = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: string, font: Font.semibold(15.0), textColor: component.background == .activeTint ? component.theme.list.itemCheckColors.foregroundColor : component.theme.chat.inputPanel.panelControlColor)) + )) + itemInsets.left = 10.0 + itemInsets.right = itemInsets.left + } + + var minItemWidth: CGFloat = 40.0 + if component.items.count == 1 { + minItemWidth = max(minItemWidth, component.minWidth) + } + + let itemSize = itemView.update( + transition: itemTransition, + component: AnyComponent(PlainButtonComponent( + content: content, + minSize: CGSize(width: minItemWidth, height: 40.0), + contentInsets: itemInsets, + action: { + item.action?() + }, + isEnabled: item.action != nil, + animateAlpha: false, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + let itemFrame = CGRect(origin: CGPoint(x: contentsWidth, y: 0.0), size: itemSize) + + if let itemComponentView = itemView.view { + var animateIn = false + if itemComponentView.superview == nil { + animateIn = true + self.backgroundView.contentView.addSubview(itemComponentView) + itemComponentView.alpha = 0.0 + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + if animateIn { + alphaTransition.setAlpha(view: itemComponentView, alpha: 1.0) + alphaTransition.animateBlur(layer: itemComponentView.layer, fromRadius: 8.0, toRadius: 0.0) + } + } + + contentsWidth += itemSize.width + } + + var removeIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removeIds.append(id) + if let itemComponentView = itemView.view { + alphaTransition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + alphaTransition.animateBlur(layer: itemComponentView.layer, fromRadius: 0.0, toRadius: 8.0, removeOnCompletion: false) + } + } + } + for id in removeIds { + self.itemViews.removeValue(forKey: id) + } + + let size = CGSize(width: contentsWidth, height: availableSize.height) + let tintColor: GlassBackgroundView.TintColor + switch component.background { + case .panel: + tintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)) + case .activeTint: + tintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7), innerColor: component.theme.list.itemCheckColors.fillColor) + } + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, isInteractive: isInteractive, transition: transition) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift new file mode 100644 index 0000000000..dba0a57b4d --- /dev/null +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift @@ -0,0 +1,273 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import GlassBackgroundComponent + +public final class GlassControlPanelComponent: Component { + public final class Item: Equatable { + public let items: [GlassControlGroupComponent.Item] + public let background: GlassControlGroupComponent.Background + + public init(items: [GlassControlGroupComponent.Item], background: GlassControlGroupComponent.Background) { + self.items = items + self.background = background + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.items != rhs.items { + return false + } + if lhs.background != rhs.background { + return false + } + return true + } + } + + public let theme: PresentationTheme + public let leftItem: Item? + public let rightItem: Item? + public let centralItem: Item? + + public init( + theme: PresentationTheme, + leftItem: Item?, + centralItem: Item?, + rightItem: Item? + ) { + self.theme = theme + self.leftItem = leftItem + self.centralItem = centralItem + self.rightItem = rightItem + } + + public static func ==(lhs: GlassControlPanelComponent, rhs: GlassControlPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.leftItem != rhs.leftItem { + return false + } + if lhs.centralItem != rhs.centralItem { + return false + } + if lhs.rightItem != rhs.rightItem { + return false + } + return true + } + + public final class View: UIView { + private let glassContainerView: GlassBackgroundContainerView + + private var leftItemComponent: ComponentView? + private var centralItemComponent: ComponentView? + private var rightItemComponent: ComponentView? + + private var component: GlassControlPanelComponent? + private weak var state: EmptyComponentState? + + public var leftItemView: GlassControlGroupComponent.View? { + return self.leftItemComponent?.view as? GlassControlGroupComponent.View + } + + public var centerItemView: GlassControlGroupComponent.View? { + return self.centralItemComponent?.view as? GlassControlGroupComponent.View + } + + public var rightItemView: GlassControlGroupComponent.View? { + return self.rightItemComponent?.view as? GlassControlGroupComponent.View + } + + override public init(frame: CGRect) { + self.glassContainerView = GlassBackgroundContainerView() + + super.init(frame: frame) + + self.addSubview(self.glassContainerView) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: GlassControlPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) + let minSpacing: CGFloat = 8.0 + + var leftItemFrame: CGRect? + if let leftItem = component.leftItem { + let leftItemComponent: ComponentView + var leftItemTransition = transition + if let current = self.leftItemComponent { + leftItemComponent = current + } else { + leftItemComponent = ComponentView() + self.leftItemComponent = leftItemComponent + leftItemTransition = transition.withAnimation(.none) + } + + let leftItemSize = leftItemComponent.update( + transition: leftItemTransition, + component: AnyComponent(GlassControlGroupComponent( + theme: component.theme, + background: leftItem.background, + items: leftItem.items, + minWidth: 40.0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + let leftItemFrameValue = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: leftItemSize) + leftItemFrame = leftItemFrameValue + if let leftItemComponentView = leftItemComponent.view { + var animateIn = false + if leftItemComponentView.superview == nil { + animateIn = true + self.glassContainerView.contentView.addSubview(leftItemComponentView) + ComponentTransition.immediate.setScale(view: leftItemComponentView, scale: 0.001) + } + leftItemTransition.setPosition(view: leftItemComponentView, position: leftItemFrameValue.center) + leftItemTransition.setBounds(view: leftItemComponentView, bounds: CGRect(origin: CGPoint(), size: leftItemFrameValue.size)) + if animateIn { + alphaTransition.animateAlpha(view: leftItemComponentView, from: 0.0, to: 1.0) + transition.setScale(view: leftItemComponentView, scale: 1.0) + } + } + } else if let leftItemComponent = self.leftItemComponent { + self.leftItemComponent = nil + if let leftItemComponentView = leftItemComponent.view { + transition.setScale(view: leftItemComponentView, scale: 0.001) + alphaTransition.setAlpha(view: leftItemComponentView, alpha: 0.0, completion: { [weak leftItemComponentView] _ in + leftItemComponentView?.removeFromSuperview() + }) + } + } + + var rightItemFrame: CGRect? + if let rightItem = component.rightItem { + let rightItemComponent: ComponentView + var rightItemTransition = transition + if let current = self.rightItemComponent { + rightItemComponent = current + } else { + rightItemComponent = ComponentView() + self.rightItemComponent = rightItemComponent + rightItemTransition = transition.withAnimation(.none) + } + + let rightItemSize = rightItemComponent.update( + transition: rightItemTransition, + component: AnyComponent(GlassControlGroupComponent( + theme: component.theme, + background: rightItem.background, + items: rightItem.items, + minWidth: 40.0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + let rightItemFrameValue = CGRect(origin: CGPoint(x: availableSize.width - rightItemSize.width, y: 0.0), size: rightItemSize) + rightItemFrame = rightItemFrameValue + if let rightItemComponentView = rightItemComponent.view { + var animateIn = false + if rightItemComponentView.superview == nil { + animateIn = true + self.glassContainerView.contentView.addSubview(rightItemComponentView) + ComponentTransition.immediate.setScale(view: rightItemComponentView, scale: 0.001) + } + rightItemTransition.setPosition(view: rightItemComponentView, position: rightItemFrameValue.center) + rightItemTransition.setBounds(view: rightItemComponentView, bounds: CGRect(origin: CGPoint(), size: rightItemFrameValue.size)) + if animateIn { + alphaTransition.animateAlpha(view: rightItemComponentView, from: 0.0, to: 1.0) + transition.setScale(view: rightItemComponentView, scale: 1.0) + } + } + } else if let rightItemComponent = self.rightItemComponent { + self.rightItemComponent = nil + if let rightItemComponentView = rightItemComponent.view { + transition.setScale(view: rightItemComponentView, scale: 0.001) + alphaTransition.setAlpha(view: rightItemComponentView, alpha: 0.0, completion: { [weak rightItemComponentView] _ in + rightItemComponentView?.removeFromSuperview() + }) + } + } + + if let centralItem = component.centralItem { + let centralItemComponent: ComponentView + var centralItemTransition = transition + if let current = self.centralItemComponent { + centralItemComponent = current + } else { + centralItemComponent = ComponentView() + self.centralItemComponent = centralItemComponent + centralItemTransition = transition.withAnimation(.none) + } + + var maxCentralItemSize = CGSize(width: availableSize.width, height: availableSize.height) + var centralRightInset: CGFloat = 0.0 + if let rightItemFrame { + centralRightInset = availableSize.width - rightItemFrame.minX + minSpacing + } + var centralLeftInset: CGFloat = 0.0 + if let leftItemFrame { + centralLeftInset = leftItemFrame.maxX + minSpacing + } + maxCentralItemSize.width = max(1.0, availableSize.width - centralLeftInset - centralRightInset) + + let centralItemSize = centralItemComponent.update( + transition: centralItemTransition, + component: AnyComponent(GlassControlGroupComponent( + theme: component.theme, + background: centralItem.background, + items: centralItem.items, + minWidth: 165.0 + )), + environment: {}, + containerSize: maxCentralItemSize + ) + let centralItemFrameValue = CGRect(origin: CGPoint(x: centralLeftInset + floor((availableSize.width - centralLeftInset - centralRightInset - centralItemSize.width) * 0.5), y: 0.0), size: centralItemSize) + if let centralItemComponentView = centralItemComponent.view { + var animateIn = false + if centralItemComponentView.superview == nil { + animateIn = true + self.glassContainerView.contentView.addSubview(centralItemComponentView) + ComponentTransition.immediate.setScale(view: centralItemComponentView, scale: 0.001) + } + centralItemTransition.setPosition(view: centralItemComponentView, position: centralItemFrameValue.center) + centralItemTransition.setBounds(view: centralItemComponentView, bounds: CGRect(origin: CGPoint(), size: centralItemFrameValue.size)) + if animateIn { + alphaTransition.animateAlpha(view: centralItemComponentView, from: 0.0, to: 1.0) + transition.setScale(view: centralItemComponentView, scale: 1.0) + } + } + } else if let centralItemComponent = self.centralItemComponent { + self.centralItemComponent = nil + if let centralItemComponentView = centralItemComponent.view { + transition.setScale(view: centralItemComponentView, scale: 0.001) + alphaTransition.setAlpha(view: centralItemComponentView, alpha: 0.0, completion: { [weak centralItemComponentView] _ in + centralItemComponentView?.removeFromSuperview() + }) + } + } + + transition.setFrame(view: self.glassContainerView, frame: CGRect(origin: CGPoint(), size: availableSize)) + self.glassContainerView.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition) + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift index 10ad6baf5d..0c5901f1f0 100644 --- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -331,9 +331,15 @@ public final class ListSectionComponent: Component { case legacy } + public enum BackgroundColor { + case base + case modal + } + public let theme: PresentationTheme public let style: Style public let background: Background + public let backgroundColor: BackgroundColor public let header: AnyComponent? public let footer: AnyComponent? public let items: [AnyComponentWithIdentity] @@ -345,6 +351,7 @@ public final class ListSectionComponent: Component { theme: PresentationTheme, style: Style = .legacy, background: Background = .all, + backgroundColor: BackgroundColor = .base, header: AnyComponent?, footer: AnyComponent?, items: [AnyComponentWithIdentity], @@ -355,6 +362,7 @@ public final class ListSectionComponent: Component { self.theme = theme self.style = style self.background = background + self.backgroundColor = backgroundColor self.header = header self.footer = footer self.items = items @@ -373,6 +381,9 @@ public final class ListSectionComponent: Component { if lhs.background != rhs.background { return false } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } if lhs.header != rhs.header { return false } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 9750e110ba..7333c3e16d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -109,6 +109,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent", "//submodules/TelegramUI/Components/StarsParticleEffect", "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/AdminUserActionsSheet", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift index bf204575e5..f8f981a35d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift @@ -17,6 +17,7 @@ import MultilineTextComponent import ContextUI import StarsParticleEffect import StoryLiveChatMessageComponent +import AdminUserActionsSheet private final class PinnedBarMessageComponent: Component { let context: AccountContext @@ -374,6 +375,7 @@ final class StoryContentLiveChatComponent: Component { let call: PresentationGroupCall let storyPeerId: EnginePeer.Id let insets: UIEdgeInsets + let controller: () -> ViewController? init( external: External, @@ -382,7 +384,8 @@ final class StoryContentLiveChatComponent: Component { theme: PresentationTheme, call: PresentationGroupCall, storyPeerId: EnginePeer.Id, - insets: UIEdgeInsets + insets: UIEdgeInsets, + controller: @escaping () -> ViewController? ) { self.external = external self.context = context @@ -391,6 +394,7 @@ final class StoryContentLiveChatComponent: Component { self.call = call self.storyPeerId = storyPeerId self.insets = insets + self.controller = controller } static func ==(lhs: StoryContentLiveChatComponent, rhs: StoryContentLiveChatComponent) -> Bool { @@ -437,6 +441,7 @@ final class StoryContentLiveChatComponent: Component { private var stateDisposable: Disposable? private var currentListIsEmpty: Bool = true + private var isMessageContextMenuOpen: Bool = false public var isChatEmpty: Bool { guard let messagesState = self.messagesState else { @@ -551,6 +556,60 @@ final class StoryContentLiveChatComponent: Component { self.state?.updated(transition: .spring(duration: 0.4)) } + private func displayDeleteMessageAndBan(id: GroupCallMessagesContext.Message.Id) { + Task { @MainActor [weak self] in + guard let self, let component = self.component else { + return + } + guard let chatPeer = await component.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: component.storyPeerId) + ).get() else { + return + } + guard let messagesState = self.messagesState, let message = messagesState.messages.first(where: { $0.id == id }) else { + return + } + guard let author = message.author else { + return + } + var totalCount = 0 + for message in messagesState.messages { + if message.author?.id == author.id { + totalCount += 1 + } + } + guard let controller = component.controller() else { + return + } + controller.push(AdminUserActionsSheet( + context: component.context, + chatPeer: chatPeer, + peers: [RenderedChannelParticipant( + participant: .member( + id: author.id, + invitedAt: 0, + adminInfo: nil, + banInfo: nil, + rank: nil, + subscriptionUntilDate: nil + ), + peer: author._asPeer() + )], + mode: .liveStream( + messageCount: 1, + deleteAllMessageCount: totalCount, + completion: { [weak self] result in + guard let self else { + return + } + let _ = self + } + ), + customTheme: defaultDarkColorPresentationTheme + )) + } + } + private func openMessageContextMenu(id: GroupCallMessagesContext.Message.Id, gesture: ContextGesture, sourceNode: ContextExtractedContentContainingNode) { Task { @MainActor [weak self] in guard let self else { @@ -583,7 +642,52 @@ final class StoryContentLiveChatComponent: Component { }))) let state = await (component.call.state |> take(1)).get() - if state.canManageCall || component.storyPeerId == component.context.account.peerId { + + var isAdmin = state.canManageCall + if component.storyPeerId == component.context.account.peerId { + isAdmin = true + } + var canDelete = isAdmin + var isMyMessage = false + guard let messagesState = self.messagesState, let message = messagesState.messages.first(where: { $0.id == id }) else { + return + } + if message.author?.id == component.context.account.peerId { + isMyMessage = true + canDelete = true + } + + if !isMyMessage, let author = message.author { + items.append(.action(ContextMenuActionItem(text: "Open Profile", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + guard let self else { + return + } + + c?.dismiss(completion: { [weak self] in + guard let self, let component = self.component else { + return + } + guard let controller = component.controller(), let navigationController = controller.navigationController as? NavigationController else { + return + } + component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: component.context, + chatLocation: .peer(author), + keepStack: .always + )) + }) + }))) + } + + #if DEBUG + if "".isEmpty { + isAdmin = true + canDelete = true + } + #endif + + if canDelete { items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in guard let self else { return @@ -594,7 +698,11 @@ final class StoryContentLiveChatComponent: Component { return } if let call = component.call as? PresentationGroupCallImpl { - call.deleteMessage(id: id) + if isAdmin && !isMyMessage { + self.displayDeleteMessageAndBan(id: id) + } else { + call.deleteMessage(id: id, reportSpam: false) + } } }) }))) @@ -615,17 +723,18 @@ final class StoryContentLiveChatComponent: Component { guard let self else { return } - if let listView = self.list.view { - let transition: ComponentTransition = .easeInOut(duration: 0.2) - transition.setAlpha(view: listView, alpha: 1.0) + self.isMessageContextMenuOpen = false + if !self.isUpdating { + self.state?.updated(transition: .easeInOut(duration: 0.2), isLocal: true) } } - if let listView = self.list.view { - let transition: ComponentTransition = .easeInOut(duration: 0.2) - transition.setAlpha(view: listView, alpha: 0.25) + + self.isMessageContextMenuOpen = true + if !self.isUpdating { + self.state?.updated(transition: .easeInOut(duration: 0.2), isLocal: true) } - component.context.sharedContext.mainWindow?.presentInGlobalOverlay(contextController) + component.controller()?.presentInGlobalOverlay(contextController) } } @@ -793,7 +902,14 @@ final class StoryContentLiveChatComponent: Component { } transition.setPosition(view: listView, position: listFrame.offsetBy(dx: 0.0, dy: self.isChatExpanded ? 0.0 : listFrame.height).center) transition.setBounds(view: listView, bounds: CGRect(origin: CGPoint(), size: listFrame.size)) - alphaTransition.setAlpha(view: listView, alpha: listItems.isEmpty ? 0.0 : 1.0) + + let listAlpha: CGFloat + if self.isMessageContextMenuOpen { + listAlpha = 0.25 + } else { + listAlpha = listItems.isEmpty ? 0.0 : 1.0 + } + alphaTransition.setAlpha(view: listView, alpha: listAlpha) } transition.setFrame(view: self.listContainer, frame: CGRect(origin: CGPoint(), size: availableSize)) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index f4cb40c54b..ddbdc7d3c5 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -40,8 +40,9 @@ final class StoryItemContentComponent: Component { let preferHighQuality: Bool let isEmbeddedInCamera: Bool let activateReaction: (UIView, MessageReaction.Reaction) -> Void + let controller: () -> ViewController? - init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, availableReactions: StoryAvailableReactions?, entityFiles: [MediaId: TelegramMediaFile], audioMode: StoryContentItem.AudioMode, baseRate: Double, isVideoBuffering: Bool, isCurrent: Bool, preferHighQuality: Bool, isEmbeddedInCamera: Bool, activateReaction: @escaping (UIView, MessageReaction.Reaction) -> Void) { + init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, availableReactions: StoryAvailableReactions?, entityFiles: [MediaId: TelegramMediaFile], audioMode: StoryContentItem.AudioMode, baseRate: Double, isVideoBuffering: Bool, isCurrent: Bool, preferHighQuality: Bool, isEmbeddedInCamera: Bool, activateReaction: @escaping (UIView, MessageReaction.Reaction) -> Void, controller: @escaping () -> ViewController?) { self.context = context self.strings = strings self.peer = peer @@ -55,6 +56,7 @@ final class StoryItemContentComponent: Component { self.preferHighQuality = preferHighQuality self.isEmbeddedInCamera = isEmbeddedInCamera self.activateReaction = activateReaction + self.controller = controller } static func ==(lhs: StoryItemContentComponent, rhs: StoryItemContentComponent) -> Bool { @@ -859,7 +861,13 @@ final class StoryItemContentComponent: Component { theme: environment.theme, call: mediaStreamCall, storyPeerId: component.peer.id, - insets: environment.containerInsets + insets: environment.containerInsets, + controller: { [weak self] in + guard let self, let component = self.component else { + return nil + } + return component.controller() + } )), environment: {}, containerSize: availableSize diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 528371d88d..14660949ec 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1628,6 +1628,12 @@ public final class StoryItemSetContainerComponent: Component { return } self.sendMessageContext.activateInlineReaction(view: self, reactionView: reactionView, reaction: reaction) + }, + controller: { [weak self] in + guard let self, let component = self.component else { + return nil + } + return component.controller() } )), environment: {