From 4b5ccefbc8db7a8fe3a656b14cdb933d1735fd84 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 1 Sep 2023 00:02:49 +0400 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 14 ++ .../Sources/DrawingStickerEntity.swift | 8 +- .../ChatItemGalleryFooterContentNode.swift | 4 + .../Sources/LegacyMessageInputPanel.swift | 62 ++++---- .../Sources/MessageInputPanelComponent.swift | 41 +----- .../Sources/TimeoutContentComponent.swift | 137 ++++++++++++++++++ 6 files changed, 198 insertions(+), 68 deletions(-) create mode 100644 submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 185f84c5be..2a92f3ebe8 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9878,3 +9878,17 @@ Sorry for the inconvenience."; "SecretImage.ViewOnce.Title" = "Disappearing Photo"; "SecretVideo.ViewOnce.Title" = "Disappearing Video"; + +"MediaPicker.Timer.Description" = "Choose how long the media will be kept after opening."; +"MediaPicker.Timer.ViewOnce" = "View Once"; +"MediaPicker.Timer.Seconds_1" = "%d Second"; +"MediaPicker.Timer.Seconds_any" = "%d Seconds"; +"MediaPicker.Timer.DoNotDelete" = "Do Not Delete"; + +"MediaPicker.Timer.Photo.ViewOnceTooltip" = "Photo set to view once."; +"MediaPicker.Timer.Photo.TimerTooltip" = "Photo will be deleted in\n%@ seconds after opening."; +"MediaPicker.Timer.Photo.KeepTooltip" = "Photo will be kept in chat."; + +"MediaPicker.Timer.Video.ViewOnceTooltip" = "Video set to view once."; +"MediaPicker.Timer.Video.TimerTooltip" = "Video will be deleted in\n%@ seconds after opening."; +"MediaPicker.Timer.Video.KeepTooltip" = "Video will be kept in chat."; diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index c31b1e5969..f0ccd4f1d1 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -392,11 +392,15 @@ public final class DrawingStickerEntityView: DrawingEntityView { } override func onSelection() { - self.presentReactionSelection() + if self.isReaction { + self.presentReactionSelection() + } } func onDeselection() { - let _ = self.dismissReactionSelection() + if self.isReaction { + let _ = self.dismissReactionSelection() + } } private weak var reactionContextNode: ReactionContextNode? diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index ab0adbfdff..675f3aecf5 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -880,6 +880,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll displayCaption = !self.textNode.isHidden } + if metrics.isTablet { + self.fullscreenButton.isHidden = true + } + var textFrame = CGRect() var visibleTextHeight: CGFloat = 0.0 if !self.textNode.isHidden { diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index ebc43e38ec..5143f56eb7 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -81,6 +81,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { } public func setTimeout(_ timeout: Int32) { + self.dismissTimeoutTooltip() var timeout: Int32? = timeout if timeout == 0 { timeout = nil @@ -250,12 +251,12 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { let currentValue = self.currentTimeout let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) - let title = "Choose how long the media will be kept after opening." + let title = presentationData.strings.MediaPicker_Timer_Description let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) - items.append(.action(ContextMenuActionItem(text: "View Once", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_ViewOnce, icon: { theme in return currentValue == viewOnceTimeout ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil }, action: { _, a in a(.default) @@ -263,31 +264,19 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { updateTimeout(viewOnceTimeout) }))) - items.append(.action(ContextMenuActionItem(text: "3 Seconds", icon: { theme in - return currentValue == 3 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { _, a in - a(.default) - - updateTimeout(3) - }))) + let values: [Int32] = [3, 10, 30] - items.append(.action(ContextMenuActionItem(text: "10 Seconds", icon: { theme in - return currentValue == 10 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { _, a in - a(.default) + for value in values { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_Seconds(value), icon: { theme in + return currentValue == value ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil + }, action: { _, a in + a(.default) + + updateTimeout(value) + }))) + } - updateTimeout(10) - }))) - - items.append(.action(ContextMenuActionItem(text: "30 Seconds", icon: { theme in - return currentValue == 30 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { _, a in - a(.default) - - updateTimeout(30) - }))) - - items.append(.action(ContextMenuActionItem(text: "Do Not Delete", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_DoNotDelete, icon: { theme in return currentValue == nil ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil }, action: { _, a in a(.default) @@ -300,29 +289,36 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { } private weak var tooltipController: TooltipScreen? - private func presentTimeoutTooltip(sourceView: UIView, timeout: Int32?) { - guard let superview = self.view.superview?.superview else { - return - } + + private func dismissTimeoutTooltip() { if let tooltipController = self.tooltipController { self.tooltipController = nil tooltipController.dismiss() } + } + + private func presentTimeoutTooltip(sourceView: UIView, timeout: Int32?) { + guard let superview = self.view.superview?.superview else { + return + } + self.dismissTimeoutTooltip() let parentFrame = superview.convert(superview.bounds, to: nil) let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 2.0), size: CGSize()) + let isVideo = !"".isEmpty + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let text: String let iconName: String if timeout == viewOnceTimeout { - text = "Photo set to view once." + text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_ViewOnceTooltip : presentationData.strings.MediaPicker_Timer_Photo_ViewOnceTooltip iconName = "anim_autoremove_on" } else if let timeout { - text = "Photo will be deleted in \(timeout) seconds after opening." + text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_TimerTooltip("\(timeout)").string : presentationData.strings.MediaPicker_Timer_Photo_TimerTooltip("\(timeout)").string iconName = "anim_autoremove_on" } else { - text = "Photo will be kept in chat." + text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_KeepTooltip : presentationData.strings.MediaPicker_Timer_Photo_KeepTooltip iconName = "anim_autoremove_off" } @@ -330,7 +326,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), - balancedTextLayout: true, + balancedTextLayout: false, style: .customBlur(UIColor(rgb: 0x18181a), 0.0), arrowStyle: .small, icon: .animation(name: iconName, delay: 0.1, tintColor: nil), diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 911e501c3a..911970a76b 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -1456,42 +1456,17 @@ public final class MessageInputPanelComponent: Component { let accentColor = component.theme.chat.inputPanel.panelControlAccentColor if let timeoutAction = component.timeoutAction, let timeoutValue = component.timeoutValue { - func generateIcon(value: String, selected: Bool) -> UIImage? { - let image = UIImage(bundleImageName: "Media Editor/Timeout")! - let valueString = NSAttributedString(string: value, font: Font.with(size: value.count == 1 ? 12.0 : 10.0, design: .round, weight: .semibold), textColor: .white, paragraphAlignment: .center) - - return generateImage(image.size, contextGenerator: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - if selected { - context.setFillColor(accentColor.cgColor) - context.fillEllipse(in: CGRect(origin: .zero, size: size)) - } else { - if let cgImage = image.cgImage { - context.draw(cgImage, in: CGRect(origin: .zero, size: size)) - } - } - - var offset: CGPoint = CGPoint(x: 0.0, y: -3.0 - UIScreenPixel) - if value == "∞" { - offset.x += UIScreenPixel - offset.y += 1.0 - UIScreenPixel - } - - let valuePath = CGMutablePath() - valuePath.addRect(bounds.offsetBy(dx: offset.x, dy: offset.y)) - let valueFramesetter = CTFramesetterCreateWithAttributedString(valueString as CFAttributedString) - let valyeFrame = CTFramesetterCreateFrame(valueFramesetter, CFRangeMake(0, valueString.length), valuePath, nil) - CTFrameDraw(valyeFrame, context) - }) - } - - let icon = generateIcon(value: timeoutValue, selected: component.timeoutSelected) let timeoutButtonSize = self.timeoutButton.update( transition: transition, component: AnyComponent(Button( - content: AnyComponent(Image(image: icon, size: CGSize(width: 20.0, height: 20.0))), + content: AnyComponent( + TimeoutContentComponent( + color: .white, + accentColor: accentColor, + isSelected: component.timeoutSelected, + value: timeoutValue + ) + ), action: { [weak self] in guard let self, let timeoutButtonView = self.timeoutButton.view else { return diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift new file mode 100644 index 0000000000..64a511b0bd --- /dev/null +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift @@ -0,0 +1,137 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +public final class TimeoutContentComponent: Component { + public let color: UIColor + public let accentColor: UIColor + public let isSelected: Bool + public let value: String + + public init( + color: UIColor, + accentColor: UIColor, + isSelected: Bool, + value: String + ) { + self.color = color + self.accentColor = accentColor + self.isSelected = isSelected + self.value = value + } + + public static func ==(lhs: TimeoutContentComponent, rhs: TimeoutContentComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.isSelected != rhs.isSelected { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + public final class View: UIView { + private var component: TimeoutContentComponent? + private weak var state: EmptyComponentState? + + private let background: UIImageView + private let foreground: UIImageView + private let text = ComponentView() + + override init(frame: CGRect) { + self.background = UIImageView(image: UIImage(bundleImageName: "Media Editor/Timeout")) + self.foreground = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.background) + self.addSubview(self.foreground) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: TimeoutContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + self.component = component + self.state = state + + let size = CGSize(width: 20.0, height: 20.0) + if previousComponent?.accentColor != component.accentColor { + self.foreground.image = generateFilledCircleImage(diameter: size.width, color: component.accentColor) + } + + var updated = false + if let previousComponent { + if previousComponent.isSelected != component.isSelected { + updated = true + } + if previousComponent.value != component.value { + if let textView = self.text.view, let snapshotView = textView.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = textView.frame + self.addSubview(snapshotView) + snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 3.0), duration: 0.2, removeOnCompletion: false, additive: true) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + + textView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + textView.layer.animatePosition(from: CGPoint(x: 0.0, y: -3.0), to: .zero, duration: 0.2, additive: true) + } + } + } + + let fontSize: CGFloat + let textOffset: CGFloat + if component.value.count == 1 { + fontSize = 12.0 + textOffset = UIScreenPixel + } else { + fontSize = 10.0 + textOffset = -UIScreenPixel + } + + let font = Font.with(size: fontSize, design: .round, weight: .semibold) + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(Text(text: component.value, font: font, color: .white)), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0) + UIScreenPixel, y: floorToScreenPixels((size.height - textSize.height) / 2.0) + textOffset), size: textSize) + transition.setPosition(view: textView, position: textFrame.center) + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } + + self.background.frame = CGRect(origin: .zero, size: size) + + self.foreground.bounds = CGRect(origin: .zero, size: size) + self.foreground.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + + let foregroundTransition: Transition = updated ? .easeInOut(duration: 0.2) : transition + foregroundTransition.setScale(view: self.foreground, scale: component.isSelected ? 1.0 : 0.001) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +}