diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 360ddd9f2c..be331d25db 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -178,6 +178,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case storiesDualCameraTooltip = 44 case displayChatListArchiveTooltip = 45 case displayStoryReactionTooltip = 46 + case storyStealthModeReplyCount = 47 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -419,6 +420,10 @@ private struct ApplicationSpecificNoticeKeys { static func displayStoryReactionTooltip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayStoryReactionTooltip.key) } + + static func storyStealthModeReplyCount() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.storyStealthModeReplyCount.key) + } } public struct ApplicationSpecificNotice { @@ -1580,4 +1585,30 @@ public struct ApplicationSpecificNotice { } |> ignoreValues } + + public static func storyStealthModeReplyCount(accountManager: AccountManager) -> Signal { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.storyStealthModeReplyCount()) + |> map { view -> Int in + if let value = view.value?.get(ApplicationSpecificCounterNotice.self) { + return Int(value.value) + } else { + return 0 + } + } + |> take(1) + } + + public static func incrementStoryStealthModeReplyCount(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Void in + var value: Int32 = 0 + if let item = transaction.getNotice(ApplicationSpecificNoticeKeys.storyStealthModeReplyCount())?.get(ApplicationSpecificCounterNotice.self) { + value = item.value + } + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: value + 1)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.storyStealthModeReplyCount(), entry) + } + } + |> ignoreValues + } } diff --git a/submodules/TelegramUI/Components/OptionButtonComponent/Sources/OptionButtonComponent.swift b/submodules/TelegramUI/Components/OptionButtonComponent/Sources/OptionButtonComponent.swift index 3f480934e3..d8c3b8e963 100644 --- a/submodules/TelegramUI/Components/OptionButtonComponent/Sources/OptionButtonComponent.swift +++ b/submodules/TelegramUI/Components/OptionButtonComponent/Sources/OptionButtonComponent.swift @@ -90,7 +90,7 @@ public final class OptionButtonComponent: Component { self.component = component - let size = CGSize(width: 52.0, height: 28.0) + let size = CGSize(width: 53.0, height: 28.0) if previousComponent?.colors.background != component.colors.background { self.backgroundView.image = generateStretchableFilledCircleImage(diameter: size.height, color: component.colors.background) @@ -121,8 +121,8 @@ public final class OptionButtonComponent: Component { } if let iconSize = self.iconView.image?.size, let arrowSize = self.arrowView.image?.size { - transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: 3.0, y: floor((size.height - iconSize.height) * 0.5)), size: iconSize)) - transition.setFrame(view: self.arrowView, frame: CGRect(origin: CGPoint(x: size.width - 8.0 - arrowSize.width, y: floor((size.height - arrowSize.height) * 0.5)), size: arrowSize)) + transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: 4.0, y: floor((size.height - iconSize.height) * 0.5)), size: iconSize)) + transition.setFrame(view: self.arrowView, frame: CGRect(origin: CGPoint(x: size.width - 8.0 - arrowSize.width, y: 1.0 + floor((size.height - arrowSize.height) * 0.5)), size: arrowSize)) } transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index c191b3d94e..34704727eb 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -541,7 +541,13 @@ public final class StoryItemSetContainerComponent: Component { guard let self else { return [] } - let _ = self + + if self.itemsContainerView.frame.contains(point) { + if !self.isPointInsideContentArea(point: point) { + return [] + } + } + return [.down] }) verticalPanRecognizer.delegate = self @@ -1029,6 +1035,14 @@ public final class StoryItemSetContainerComponent: Component { } verticalPanState.startContentOffsetY = recognizer.translation(in: self).y + } else { + if verticalPanState.fraction <= -0.15 { + if let activate = self.activateInputWhileDragging() { + recognizer.state = .cancelled + + activate() + } + } } self.state?.updated(transition: .immediate) @@ -1268,7 +1282,7 @@ public final class StoryItemSetContainerComponent: Component { let currentContentScale = itemLayout.contentMinScale * itemLayout.contentScaleFraction + 1.0 * (1.0 - itemLayout.contentScaleFraction) let scaledCentralVisibleItemWidth = itemLayout.contentFrame.width * currentContentScale - let scaledSideVisibleItemWidth = scaledCentralVisibleItemWidth - 30.0 * itemLayout.contentScaleFraction + let scaledSideVisibleItemWidth = scaledCentralVisibleItemWidth - 54.0 * itemLayout.contentScaleFraction let scaledFullItemScrollDistance = scaledCentralVisibleItemWidth * 0.5 + itemLayout.itemSpacing + scaledSideVisibleItemWidth * 0.5 let scaledHalfItemScrollDistance = scaledSideVisibleItemWidth * 0.5 + itemLayout.itemSpacing + scaledSideVisibleItemWidth * 0.5 @@ -3803,135 +3817,154 @@ public final class StoryItemSetContainerComponent: Component { self.reactionContextNode = reactionContextNode reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in - guard let self, let component = self.component else { + guard let self else { return } + let action: () -> Void = { [weak self] in + guard let self, let component = self.component else { + return + } + + if self.displayLikeReactions { + if component.slice.item.storyItem.myReaction == updateReaction.reaction { + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: nil).start() + self.displayLikeReactions = false + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + } else { + if hasFirstResponder(self) { + self.sendMessageContext.currentInputMode = .text + self.endEditing(true) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + + self.waitingForReactionAnimateOutToLike = updateReaction.reaction + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: updateReaction.reaction).start() + } + } else { + let _ = (component.context.engine.stickers.availableReactions() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self, weak reactionContextNode] availableReactions in + guard let self, let component = self.component, let availableReactions else { + return + } + + var animation: TelegramMediaFile? + for reaction in availableReactions.reactions { + if reaction.value == updateReaction.reaction { + animation = reaction.centerAnimation + break + } + } + + let targetView = UIView(frame: CGRect(origin: CGPoint(x: floor((self.bounds.width - 100.0) * 0.5), y: floor((self.bounds.height - 100.0) * 0.5)), size: CGSize(width: 100.0, height: 100.0))) + targetView.isUserInteractionEnabled = false + self.addSubview(targetView) + + if let reactionContextNode { + reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction) + reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: "".isEmpty ? nil : { [weak self] standaloneReactionAnimation in + guard let self else { + return + } + standaloneReactionAnimation.frame = self.bounds + self.addSubview(standaloneReactionAnimation.view) + }, completion: { [weak targetView, weak reactionContextNode] in + targetView?.removeFromSuperview() + if let reactionContextNode { + reactionContextNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, removeOnCompletion: false) + reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in + reactionContextNode?.view.removeFromSuperview() + }) + } + }) + } + + if hasFirstResponder(self) { + self.sendMessageContext.currentInputMode = .text + self.endEditing(true) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + + var text = "" + var messageAttributes: [MessageAttribute] = [] + var inlineStickers: [MediaId : Media] = [:] + switch updateReaction { + case let .builtin(textValue): + text = textValue + case let .custom(fileId, file): + if let file { + animation = file + loop: for attribute in file.attributes { + switch attribute { + case let .CustomEmoji(_, _, displayText, _): + text = displayText + let length = (text as NSString).length + messageAttributes = [TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< length, type: .CustomEmoji(stickerPack: nil, fileId: fileId))])] + inlineStickers = [file.fileId: file] + break loop + default: + break + } + } + } + } + + let message: EnqueueMessage = .message( + text: text, + attributes: messageAttributes, + inlineStickers: inlineStickers, + mediaReference: nil, + replyToMessageId: nil, + replyToStoryId: StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id), + localGroupingKey: nil, + correlationId: nil, + bubbleUpEmojiOrStickersets: [] + ) + + let context = component.context + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + let presentController = component.presentController + let peer = component.slice.peer + + let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: [message]) + |> deliverOnMainQueue).start(next: { [weak self] messageIds in + if let animation, let self, let component = self.component { + let controller = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: context, file: animation, loop: false, title: nil, text: component.strings.Story_ToastReactionSent, undoText: component.strings.Story_ToastViewInChat, customAction: { [weak self] in + if let messageId = messageIds.first, let self { + self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: false, timecode: nil) }) + } + }), + elevatedLayout: false, + animateInAsReplacement: false, + action: { [weak self] _ in + self?.sendMessageContext.tooltipScreen = nil + self?.updateIsProgressPaused() + return false + } + ) + self.sendMessageContext.tooltipScreen?.dismiss() + self.sendMessageContext.tooltipScreen = controller + self.updateIsProgressPaused() + presentController(controller, nil) + } + }) + }) + } + } if self.displayLikeReactions { if component.slice.item.storyItem.myReaction == updateReaction.reaction { - let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: nil).start() - self.displayLikeReactions = false - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) + action() } else { - if hasFirstResponder(self) { - self.sendMessageContext.currentInputMode = .text - self.endEditing(true) - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) - - self.waitingForReactionAnimateOutToLike = updateReaction.reaction - let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: updateReaction.reaction).start() + self.sendMessageContext.performWithPossibleStealthModeConfirmation(view: self, action: { + action() + }) } } else { - let _ = (component.context.engine.stickers.availableReactions() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self, weak reactionContextNode] availableReactions in - guard let self, let component = self.component, let availableReactions else { - return - } - - var animation: TelegramMediaFile? - for reaction in availableReactions.reactions { - if reaction.value == updateReaction.reaction { - animation = reaction.centerAnimation - break - } - } - - let targetView = UIView(frame: CGRect(origin: CGPoint(x: floor((self.bounds.width - 100.0) * 0.5), y: floor((self.bounds.height - 100.0) * 0.5)), size: CGSize(width: 100.0, height: 100.0))) - targetView.isUserInteractionEnabled = false - self.addSubview(targetView) - - if let reactionContextNode { - reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction) - reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: "".isEmpty ? nil : { [weak self] standaloneReactionAnimation in - guard let self else { - return - } - standaloneReactionAnimation.frame = self.bounds - self.addSubview(standaloneReactionAnimation.view) - }, completion: { [weak targetView, weak reactionContextNode] in - targetView?.removeFromSuperview() - if let reactionContextNode { - reactionContextNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, removeOnCompletion: false) - reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in - reactionContextNode?.view.removeFromSuperview() - }) - } - }) - } - - if hasFirstResponder(self) { - self.sendMessageContext.currentInputMode = .text - self.endEditing(true) - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) - - var text = "" - var messageAttributes: [MessageAttribute] = [] - var inlineStickers: [MediaId : Media] = [:] - switch updateReaction { - case let .builtin(textValue): - text = textValue - case let .custom(fileId, file): - if let file { - animation = file - loop: for attribute in file.attributes { - switch attribute { - case let .CustomEmoji(_, _, displayText, _): - text = displayText - let length = (text as NSString).length - messageAttributes = [TextEntitiesMessageAttribute(entities: [MessageTextEntity(range: 0 ..< length, type: .CustomEmoji(stickerPack: nil, fileId: fileId))])] - inlineStickers = [file.fileId: file] - break loop - default: - break - } - } - } - } - - let message: EnqueueMessage = .message( - text: text, - attributes: messageAttributes, - inlineStickers: inlineStickers, - mediaReference: nil, - replyToMessageId: nil, - replyToStoryId: StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id), - localGroupingKey: nil, - correlationId: nil, - bubbleUpEmojiOrStickersets: [] - ) - - let context = component.context - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - let presentController = component.presentController - let peer = component.slice.peer - - let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: [message]) - |> deliverOnMainQueue).start(next: { [weak self] messageIds in - if let animation, let self, let component = self.component { - let controller = UndoOverlayController( - presentationData: presentationData, - content: .sticker(context: context, file: animation, loop: false, title: nil, text: component.strings.Story_ToastReactionSent, undoText: component.strings.Story_ToastViewInChat, customAction: { [weak self] in - if let messageId = messageIds.first, let self { - self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: false, timecode: nil) }) - } - }), - elevatedLayout: false, - animateInAsReplacement: false, - action: { [weak self] _ in - self?.sendMessageContext.tooltipScreen = nil - self?.updateIsProgressPaused() - return false - } - ) - self.sendMessageContext.tooltipScreen?.dismiss() - self.sendMessageContext.tooltipScreen = controller - self.updateIsProgressPaused() - presentController(controller, nil) - } - }) + self.sendMessageContext.performWithPossibleStealthModeConfirmation(view: self, action: { + action() }) } } @@ -4805,59 +4838,73 @@ public final class StoryItemSetContainerComponent: Component { guard let component = self.component else { return } - guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else { - return - } - guard let likeButtonView = inputPanelView.likeButtonView else { - return - } - let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: component.slice.item.storyItem.myReaction == nil ? .builtin("❤") : nil).start() + let action: () -> Void = { [weak self] in + guard let self, let component = self.component else { + return + } + guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else { + return + } + guard let likeButtonView = inputPanelView.likeButtonView else { + return + } + + let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: component.slice.item.storyItem.myReaction == nil ? .builtin("❤") : nil).start() + + if component.slice.item.storyItem.myReaction != nil { + return + } + + var reactionItem: ReactionItem? + guard let availableReactions = component.availableReactions else { + return + } + for item in availableReactions.reactionItems { + if case .builtin("❤") = item.reaction.rawValue { + reactionItem = item + break + } + } + + guard let reactionItem else { + return + } + + let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false) + self.componentContainerView.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = self.bounds + standaloneReactionAnimation.animateReactionSelection( + context: component.context, + theme: component.theme, + animationCache: component.context.animationCache, + reaction: reactionItem, + avatarPeers: [], + playHaptic: true, + isLarge: false, + hideCenterAnimation: true, + targetView: likeButtonView, + addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in + guard let self else { + return + } + + standaloneReactionAnimation.frame = self.bounds + self.componentContainerView.addSubnode(standaloneReactionAnimation) + }, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) + } if component.slice.item.storyItem.myReaction != nil { - return + action() + } else { + self.sendMessageContext.performWithPossibleStealthModeConfirmation(view: self, action: { + action() + }) } - - var reactionItem: ReactionItem? - guard let availableReactions = component.availableReactions else { - return - } - for item in availableReactions.reactionItems { - if case .builtin("❤") = item.reaction.rawValue { - reactionItem = item - break - } - } - - guard let reactionItem else { - return - } - - let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false) - self.componentContainerView.addSubnode(standaloneReactionAnimation) - standaloneReactionAnimation.frame = self.bounds - standaloneReactionAnimation.animateReactionSelection( - context: component.context, - theme: component.theme, - animationCache: component.context.animationCache, - reaction: reactionItem, - avatarPeers: [], - playHaptic: true, - isLarge: false, - hideCenterAnimation: true, - targetView: likeButtonView, - addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in - guard let self else { - return - } - - standaloneReactionAnimation.frame = self.bounds - self.componentContainerView.addSubnode(standaloneReactionAnimation) - }, - completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - } - ) } private func performLikeOptionsAction(sourceView: UIView) { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 0366cee143..64d2a85634 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -483,125 +483,191 @@ final class StoryItemSetContainerSendMessage { view.updateIsProgressPaused() } + func performWithPossibleStealthModeConfirmation(view: StoryItemSetContainerComponent.View, action: @escaping () -> Void) { + guard let component = view.component, component.stealthModeTimeout != nil else { + action() + return + } + + let _ = (combineLatest( + component.context.engine.data.get( + TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState() + ), + ApplicationSpecificNotice.storyStealthModeReplyCount(accountManager: component.context.sharedContext.accountManager) + ) + |> deliverOnMainQueue).start(next: { [weak self, weak view] data, noticeCount in + let config = data + + guard let self, let view, let component = view.component else { + return + } + + let timestamp = Int32(Date().timeIntervalSince1970) + if noticeCount < 3, let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp { + + let theme = component.theme + let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) + + //TODO:localize + let alertController = textAlertController( + context: component.context, + updatedPresentationData: updatedPresentationData, + title: "You are in Stealth Mode now", + text: "If you send a reply or reaction, the creator of the story will also see you in the list of viewers.", + actions: [ + TextAlertAction(type: .defaultAction, title: "Cancel", action: {}), + TextAlertAction(type: .genericAction, title: "Proceed", action: { + action() + }) + ] + ) + alertController.dismissed = { [weak self, weak view] _ in + guard let self, let view else { + return + } + self.actionSheet = nil + view.updateIsProgressPaused() + } + self.actionSheet = alertController + view.updateIsProgressPaused() + + component.controller()?.presentInGlobalOverlay(alertController) + } else { + action() + } + }) + } + func performSendMessageAction( view: StoryItemSetContainerComponent.View, silentPosting: Bool = false, scheduleTime: Int32? = nil ) { - guard let component = view.component else { - return - } - let focusedItem = component.slice.item - guard let peerId = focusedItem.peerId else { - return - } - let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) - guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { - return - } - let peer = component.slice.peer - - let controller = component.controller() as? StoryContainerScreen - - if let recordedAudioPreview = self.recordedAudioPreview { - self.recordedAudioPreview = nil + self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak self, weak view] in + guard let self, let view else { + return + } + guard let component = view.component else { + return + } - let waveformBuffer = recordedAudioPreview.waveform.makeBitstream() + let focusedItem = component.slice.item + guard let peerId = focusedItem.peerId else { + return + } + let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) + guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { + return + } + let peer = component.slice.peer - let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedAudioPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedAudioPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedAudioPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + let controller = component.controller() as? StoryContainerScreen - let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: messages).start() - - view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) - } else if self.hasRecordedVideoPreview, let videoRecorderValue = self.videoRecorderValue { - videoRecorderValue.send() - self.hasRecordedVideoPreview = false - self.videoRecorder.set(.single(nil)) - view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) - } else { - switch inputPanelView.getSendMessageInput() { - case let .text(text): - if !text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - let entities = generateChatInputTextEntities(text) - let _ = (component.context.engine.messages.enqueueOutgoingMessage( - to: peerId, - replyTo: nil, - storyId: focusedStoryId, - content: .text(text.string, entities), - silentPosting: silentPosting, - scheduleTime: scheduleTime - ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in - Queue.mainQueue().after(0.3) { - if let self, let view { - self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }, isScheduled: scheduleTime != nil) + if let recordedAudioPreview = self.recordedAudioPreview { + self.recordedAudioPreview = nil + + let waveformBuffer = recordedAudioPreview.waveform.makeBitstream() + + let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedAudioPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedAudioPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedAudioPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + + let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: messages).start() + + view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + } else if self.hasRecordedVideoPreview, let videoRecorderValue = self.videoRecorderValue { + videoRecorderValue.send() + self.hasRecordedVideoPreview = false + self.videoRecorder.set(.single(nil)) + view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + } else { + switch inputPanelView.getSendMessageInput() { + case let .text(text): + if !text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + let entities = generateChatInputTextEntities(text) + let _ = (component.context.engine.messages.enqueueOutgoingMessage( + to: peerId, + replyTo: nil, + storyId: focusedStoryId, + content: .text(text.string, entities), + silentPosting: silentPosting, + scheduleTime: scheduleTime + ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in + Queue.mainQueue().after(0.3) { + if let self, let view { + self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }, isScheduled: scheduleTime != nil) + } } + }) + inputPanelView.clearSendMessageInput() + + self.currentInputMode = .text + if hasFirstResponder(view) { + view.endEditing(true) + } else { + view.state?.updated(transition: .spring(duration: 0.3)) } - }) - inputPanelView.clearSendMessageInput() - - self.currentInputMode = .text - if hasFirstResponder(view) { - view.endEditing(true) - } else { - view.state?.updated(transition: .spring(duration: 0.3)) + controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) } - controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) - } - } - } - } - - func performSendStickerAction(view: StoryItemSetContainerComponent.View, fileReference: FileMediaReference) { - guard let component = view.component else { - return - } - let focusedItem = component.slice.item - guard let peerId = focusedItem.peerId else { - return - } - let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) - let peer = component.slice.peer - - let controller = component.controller() as? StoryContainerScreen - - if let navigationController = controller?.navigationController as? NavigationController { - var controllers = navigationController.viewControllers - for controller in controllers.reversed() { - if !(controller is StoryContainerScreen) { - controllers.removeLast() - } else { - break - } - } - navigationController.setViewControllers(controllers, animated: true) - - controller?.window?.forEachController({ controller in - if let controller = controller as? StickerPackScreenImpl { - controller.dismiss() - } - }) - } - - let _ = (component.context.engine.messages.enqueueOutgoingMessage( - to: peerId, - replyTo: nil, - storyId: focusedStoryId, - content: .file(fileReference) - ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in - Queue.mainQueue().after(0.3) { - if let self, let view { - self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }) } } }) - - self.currentInputMode = .text - if hasFirstResponder(view) { - view.endEditing(true) - } else { - view.state?.updated(transition: .spring(duration: 0.3)) - } - controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) + } + + func performSendStickerAction(view: StoryItemSetContainerComponent.View, fileReference: FileMediaReference) { + self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak self, weak view] in + guard let self, let view else { + return + } + guard let component = view.component else { + return + } + let focusedItem = component.slice.item + guard let peerId = focusedItem.peerId else { + return + } + let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) + let peer = component.slice.peer + + let controller = component.controller() as? StoryContainerScreen + + if let navigationController = controller?.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + for controller in controllers.reversed() { + if !(controller is StoryContainerScreen) { + controllers.removeLast() + } else { + break + } + } + navigationController.setViewControllers(controllers, animated: true) + + controller?.window?.forEachController({ controller in + if let controller = controller as? StickerPackScreenImpl { + controller.dismiss() + } + }) + } + + let _ = (component.context.engine.messages.enqueueOutgoingMessage( + to: peerId, + replyTo: nil, + storyId: focusedStoryId, + content: .file(fileReference) + ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in + Queue.mainQueue().after(0.3) { + if let self, let view { + self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }) + } + } + }) + + self.currentInputMode = .text + if hasFirstResponder(view) { + view.endEditing(true) + } else { + view.state?.updated(transition: .spring(duration: 0.3)) + } + controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) + }) } func performSendContextResultAction(view: StoryItemSetContainerComponent.View, results: ChatContextResultCollection, result: ChatContextResult) { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 6981b1ec07..7d72037fb8 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -1139,7 +1139,10 @@ final class StoryItemSetViewListComponent: Component { } let _ = self - sourceView?.alpha = 1.0 + if let sourceView { + let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + transition.setAlpha(view: sourceView, alpha: 1.0) + } } controller.present(contextController, in: .window(.root)) } @@ -1170,7 +1173,7 @@ final class StoryItemSetViewListComponent: Component { component: AnyComponent(TabSelectorComponent( colors: TabSelectorComponent.Colors( foreground: .white, - selection: UIColor(rgb: 0xffffff, alpha: 0.2) + selection: UIColor(rgb: 0xffffff, alpha: 0.09) ), items: [ TabSelectorComponent.Item( @@ -1220,7 +1223,7 @@ final class StoryItemSetViewListComponent: Component { transition: transition, component: AnyComponent(OptionButtonComponent( colors: OptionButtonComponent.Colors( - background: UIColor(rgb: 0x767680, alpha: 0.2), + background: UIColor(rgb: 0xffffff, alpha: 0.09), foreground: .white ), icon: self.sortMode == .recentFirst ? "Chat/Context Menu/Time" : "Chat/Context Menu/Reactions", @@ -1281,11 +1284,11 @@ final class StoryItemSetViewListComponent: Component { if !component.hasPremium, component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { } else { if let views = component.storyItem.views { - if views.seenCount >= 20 { + if views.seenCount >= 20 || component.context.sharedContext.immediateExperimentalUISettings.storiesExperiment { displayModeSelector = true displaySearchBar = true } - if views.reactedCount >= 10 { + if views.reactedCount >= 10 || component.context.sharedContext.immediateExperimentalUISettings.storiesExperiment { displaySortSelector = true } } @@ -1299,9 +1302,9 @@ final class StoryItemSetViewListComponent: Component { if component.isSearchActive { navigationHeight = navigationSearchSize.height } else if displaySearchBar { - navigationHeight = 50.0 + navigationSearchSize.height + navigationHeight = 56.0 + navigationSearchSize.height - 6.0 } else { - navigationHeight = 50.0 + navigationHeight = 56.0 } let navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - visualHeight + 12.0), size: CGSize(width: availableSize.width, height: navigationHeight)) @@ -1313,7 +1316,7 @@ final class StoryItemSetViewListComponent: Component { self.navigationContainerView.addSubview(tabSelectorView) } tabSelectorView.isHidden = !displayModeSelector - transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabSelectorSize.width) * 0.5), y: floor((50.0 - tabSelectorSize.height) * 0.5) + (component.isSearchActive ? (-50.0) : 0.0)), size: tabSelectorSize)) + transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabSelectorSize.width) * 0.5), y: floor((56.0 - tabSelectorSize.height) * 0.5) + (component.isSearchActive ? (-56.0) : 0.0)), size: tabSelectorSize)) transition.setAlpha(view: tabSelectorView, alpha: component.isSearchActive ? 0.0 : 1.0) } if let titleView = self.title.view { @@ -1322,7 +1325,7 @@ final class StoryItemSetViewListComponent: Component { } titleView.isHidden = displayModeSelector - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((50.0 - titleSize.height) * 0.5) + (component.isSearchActive ? (-50.0) : 0.0)), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((56.0 - titleSize.height) * 0.5) + (component.isSearchActive ? (-56.0) : 0.0)), size: titleSize) transition.setFrame(view: titleView, frame: titleFrame) transition.setAlpha(view: titleView, alpha: component.isSearchActive ? 0.0 : 1.0) @@ -1332,7 +1335,7 @@ final class StoryItemSetViewListComponent: Component { if orderSelectorView.superview == nil { self.navigationContainerView.addSubview(orderSelectorView) } - transition.setFrame(view: orderSelectorView, frame: CGRect(origin: CGPoint(x: availableSize.width - sideInset - orderSelectorSize.width, y: floor((50.0 - orderSelectorSize.height) * 0.5) + (component.isSearchActive ? (-50.0) : 0.0)), size: orderSelectorSize)) + transition.setFrame(view: orderSelectorView, frame: CGRect(origin: CGPoint(x: availableSize.width - sideInset - orderSelectorSize.width, y: floor((56.0 - orderSelectorSize.height) * 0.5) + (component.isSearchActive ? (-56.0) : 0.0)), size: orderSelectorSize)) transition.setAlpha(view: orderSelectorView, alpha: component.isSearchActive ? 0.0 : 1.0) orderSelectorView.isHidden = !displaySortSelector diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index 42953b5887..fc37c57798 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -155,7 +155,7 @@ public final class TabSelectorComponent: Component { } itemTransition.setPosition(view: itemTitleView, position: itemTitleFrame.origin) itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemTitleFrame.size)) - itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId ? 1.0 : 0.6) + itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId ? 1.0 : 0.4) } }