diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index a65d30ba05..b62a13ff69 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -998,6 +998,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { sourceSendButton: node, textInputView: textInputNode.textView, mediaPreview: mediaPreview, + mediaCaptionIsAbove: (false, { _ in + }), emojiViewProvider: textInputPanelNode.emojiViewProvider, attachment: true, canSendWhenOnline: sendWhenOnlineAvailable, diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index 457bec6f6f..b60d00bfb2 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -185,6 +185,7 @@ public func makeChatSendMessageActionSheetController( sourceSendButton: ASDisplayNode, textInputView: UITextView, mediaPreview: ChatSendMessageContextScreenMediaPreview? = nil, + mediaCaptionIsAbove: (Bool, (Bool) -> Void)? = nil, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, wallpaperBackgroundNode: WallpaperBackgroundNode? = nil, attachment: Bool = false, @@ -228,6 +229,7 @@ public func makeChatSendMessageActionSheetController( sourceSendButton: sourceSendButton, textInputView: textInputView, mediaPreview: mediaPreview, + mediaCaptionIsAbove: mediaCaptionIsAbove, emojiViewProvider: emojiViewProvider, wallpaperBackgroundNode: wallpaperBackgroundNode, attachment: attachment, diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index b91868aa58..6ce29a2d88 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -63,6 +63,7 @@ final class ChatSendMessageContextScreenComponent: Component { let sourceSendButton: ASDisplayNode let textInputView: UITextView let mediaPreview: ChatSendMessageContextScreenMediaPreview? + let mediaCaptionIsAbove: (Bool, (Bool) -> Void)? let emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? let wallpaperBackgroundNode: WallpaperBackgroundNode? let attachment: Bool @@ -85,6 +86,7 @@ final class ChatSendMessageContextScreenComponent: Component { sourceSendButton: ASDisplayNode, textInputView: UITextView, mediaPreview: ChatSendMessageContextScreenMediaPreview?, + mediaCaptionIsAbove: (Bool, (Bool) -> Void)?, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, wallpaperBackgroundNode: WallpaperBackgroundNode?, attachment: Bool, @@ -106,6 +108,7 @@ final class ChatSendMessageContextScreenComponent: Component { self.sourceSendButton = sourceSendButton self.textInputView = textInputView self.mediaPreview = mediaPreview + self.mediaCaptionIsAbove = mediaCaptionIsAbove self.emojiViewProvider = emojiViewProvider self.wallpaperBackgroundNode = wallpaperBackgroundNode self.attachment = attachment @@ -160,6 +163,8 @@ final class ChatSendMessageContextScreenComponent: Component { private weak var state: EmptyComponentState? private var isUpdating: Bool = false + private var mediaCaptionIsAbove: Bool = false + private let messageEffectDisposable = MetaDisposable() private var selectedMessageEffect: AvailableMessageEffects.MessageEffect? private var standaloneReactionAnimation: AnimatedStickerNode? @@ -278,6 +283,8 @@ final class ChatSendMessageContextScreenComponent: Component { let themeUpdated = environment.theme !== self.environment?.theme if self.component == nil { + self.mediaCaptionIsAbove = component.mediaCaptionIsAbove?.0 ?? false + component.gesture.externalUpdated = { [weak self] view, location in guard let self, let actionsStackNode = self.actionsStackNode else { return @@ -345,9 +352,103 @@ final class ChatSendMessageContextScreenComponent: Component { sendButtonScale = 1.0 } + var reminders = false + var isSecret = false + var canSchedule = false + if let peerId = component.peerId { + reminders = peerId == component.context.account.peerId + isSecret = peerId.namespace == Namespaces.Peer.SecretChat + canSchedule = !isSecret + } + if component.isScheduledMessages { + canSchedule = false + } + + var items: [ContextMenuItem] = [] + if component.mediaCaptionIsAbove != nil { + //TODO:localize + let mediaCaptionIsAbove = self.mediaCaptionIsAbove + items.append(.action(ContextMenuActionItem( + id: AnyHashable("captionPosition"), + text: mediaCaptionIsAbove ? "Move Caption Down" : "Move Caption Up", + icon: { _ in + return nil + }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: !mediaCaptionIsAbove ? "message_preview_sort_above" : "message_preview_sort_below" + ), action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.mediaCaptionIsAbove = !self.mediaCaptionIsAbove + component.mediaCaptionIsAbove?.1(self.mediaCaptionIsAbove) + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.35)) + } + } + ))) + } + if !reminders { + items.append(.action(ContextMenuActionItem( + id: AnyHashable("silent"), + text: environment.strings.Conversation_SendMessage_SendSilently, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.animateOutToEmpty = true + component.sendMessage(.silently, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) + self.environment?.controller()?.dismiss() + } + ))) + + if component.canSendWhenOnline && canSchedule { + items.append(.action(ContextMenuActionItem( + id: AnyHashable("whenOnline"), + text: environment.strings.Conversation_SendMessage_SendWhenOnline, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.animateOutToEmpty = true + component.sendMessage(.whenOnline, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) + self.environment?.controller()?.dismiss() + } + ))) + } + } + if canSchedule { + items.append(.action(ContextMenuActionItem( + id: AnyHashable("schedule"), + text: reminders ? environment.strings.Conversation_SendMessage_SetReminder: environment.strings.Conversation_SendMessage_ScheduleMessage, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.animateOutToEmpty = true + component.schedule(self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) + self.environment?.controller()?.dismiss() + } + ))) + } + let actionsStackNode: ContextControllerActionsStackNode if let current = self.actionsStackNode { actionsStackNode = current + + actionsStackNode.replace(item: ContextControllerActionsListStackItem( + id: AnyHashable("items"), + items: items, + reactionItems: nil, + tip: nil, + tipSignal: .single(nil), + dismissed: nil + ), animated: !transition.animation.isImmediate) } else { actionsStackNode = ContextControllerActionsStackNode( getController: { @@ -366,69 +467,9 @@ final class ChatSendMessageContextScreenComponent: Component { ) actionsStackNode.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) - var reminders = false - var isSecret = false - var canSchedule = false - if let peerId = component.peerId { - reminders = peerId == component.context.account.peerId - isSecret = peerId.namespace == Namespaces.Peer.SecretChat - canSchedule = !isSecret - } - if component.isScheduledMessages { - canSchedule = false - } - - var items: [ContextMenuItem] = [] - if !reminders { - items.append(.action(ContextMenuActionItem( - text: environment.strings.Conversation_SendMessage_SendSilently, - icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, _ in - guard let self, let component = self.component else { - return - } - self.animateOutToEmpty = true - component.sendMessage(.silently, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) - self.environment?.controller()?.dismiss() - } - ))) - - if component.canSendWhenOnline && canSchedule { - items.append(.action(ContextMenuActionItem( - text: environment.strings.Conversation_SendMessage_SendWhenOnline, - icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, _ in - guard let self, let component = self.component else { - return - } - self.animateOutToEmpty = true - component.sendMessage(.whenOnline, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) - self.environment?.controller()?.dismiss() - } - ))) - } - } - if canSchedule { - items.append(.action(ContextMenuActionItem( - text: reminders ? environment.strings.Conversation_SendMessage_SetReminder: environment.strings.Conversation_SendMessage_ScheduleMessage, - icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, _ in - guard let self, let component = self.component else { - return - } - self.animateOutToEmpty = true - component.schedule(self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) - self.environment?.controller()?.dismiss() - } - ))) - } - actionsStackNode.push( item: ContextControllerActionsListStackItem( - id: nil, + id: AnyHashable("items"), items: items, reactionItems: nil, tip: nil, @@ -505,6 +546,7 @@ final class ChatSendMessageContextScreenComponent: Component { sourceTextInputView: component.textInputView as? ChatInputTextView, emojiViewProvider: component.emojiViewProvider, sourceMediaPreview: component.mediaPreview, + mediaCaptionIsAbove: self.mediaCaptionIsAbove, textInsets: messageTextInsets, explicitBackgroundSize: explicitMessageBackgroundSize, maxTextWidth: localSourceTextInputViewFrame.width, @@ -1093,6 +1135,7 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha sourceSendButton: ASDisplayNode, textInputView: UITextView, mediaPreview: ChatSendMessageContextScreenMediaPreview?, + mediaCaptionIsAbove: (Bool, (Bool) -> Void)?, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, wallpaperBackgroundNode: WallpaperBackgroundNode?, attachment: Bool, @@ -1119,6 +1162,7 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha sourceSendButton: sourceSendButton, textInputView: textInputView, mediaPreview: mediaPreview, + mediaCaptionIsAbove: mediaCaptionIsAbove, emojiViewProvider: emojiViewProvider, wallpaperBackgroundNode: wallpaperBackgroundNode, attachment: attachment, diff --git a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift index 63d6ef7050..a5aee8a88a 100644 --- a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift +++ b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift @@ -202,6 +202,7 @@ final class MessageItemView: UIView { sourceTextInputView: ChatInputTextView?, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, sourceMediaPreview: ChatSendMessageContextScreenMediaPreview?, + mediaCaptionIsAbove: Bool, textInsets: UIEdgeInsets, explicitBackgroundSize: CGSize?, maxTextWidth: CGFloat, @@ -255,6 +256,16 @@ final class MessageItemView: UIView { backgroundNode: backgroundNode ) + self.backgroundNode.setType( + type: .outgoing(.None), + highlighted: false, + graphics: themeGraphics, + maskMode: true, + hasWallpaper: true, + transition: transition.containedViewLayoutTransition, + backgroundNode: backgroundNode + ) + if let sourceMediaPreview { let mediaPreviewClippingView: UIView if let current = self.mediaPreviewClippingView { @@ -281,7 +292,7 @@ final class MessageItemView: UIView { let mediaPreviewSize = sourceMediaPreview.update(containerSize: containerSize, transition: transition) var backgroundSize = CGSize(width: mediaPreviewSize.width, height: mediaPreviewSize.height) - let mediaPreviewFrame: CGRect + var mediaPreviewFrame: CGRect switch sourceMediaPreview.layoutType { case .message, .media: backgroundSize.width += 7.0 @@ -290,8 +301,135 @@ final class MessageItemView: UIView { mediaPreviewFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: mediaPreviewSize) } + let backgroundAlpha: CGFloat + switch sourceMediaPreview.layoutType { + case .media: + backgroundAlpha = explicitBackgroundSize != nil ? 0.0 : 1.0 + case .message, .videoMessage: + backgroundAlpha = 0.0 + } + + var backgroundFrame = mediaPreviewFrame.insetBy(dx: -2.0, dy: -2.0) + backgroundFrame.size.width += 6.0 + + if textString.length != 0 { + let textNode: ChatInputTextNode + if let current = self.textNode { + textNode = current + } else { + textNode = ChatInputTextNode(disableTiling: true) + textNode.textView.isScrollEnabled = false + textNode.isUserInteractionEnabled = false + self.textNode = textNode + self.textClippingContainer.addSubview(textNode.view) + + if let sourceTextInputView { + var textContainerInset = sourceTextInputView.defaultTextContainerInset + textContainerInset.right = 0.0 + textNode.textView.defaultTextContainerInset = textContainerInset + } + + let messageAttributedText = NSMutableAttributedString(attributedString: textString) + textNode.attributedText = messageAttributedText + } + + let mainColor = presentationData.theme.chat.message.outgoing.accentControlColor + let mappedLineStyle: ChatInputTextView.Theme.Quote.LineStyle + if let sourceTextInputView, let textTheme = sourceTextInputView.theme { + switch textTheme.quote.lineStyle { + case .solid: + mappedLineStyle = .solid(color: mainColor) + case .doubleDashed: + mappedLineStyle = .doubleDashed(mainColor: mainColor, secondaryColor: .clear) + case .tripleDashed: + mappedLineStyle = .tripleDashed(mainColor: mainColor, secondaryColor: .clear, tertiaryColor: .clear) + } + } else { + mappedLineStyle = .solid(color: mainColor) + } + + textNode.textView.theme = ChatInputTextView.Theme( + quote: ChatInputTextView.Theme.Quote( + background: mainColor.withMultipliedAlpha(0.1), + foreground: mainColor, + lineStyle: mappedLineStyle, + codeBackground: mainColor.withMultipliedAlpha(0.1), + codeForeground: mainColor + ) + ) + + let maxTextWidth = mediaPreviewFrame.width + + let textPositioningInsets = UIEdgeInsets(top: -5.0, left: 0.0, bottom: -4.0, right: -4.0) + + let currentRightInset: CGFloat = 0.0 + let textHeight = textNode.textHeightForWidth(maxTextWidth, rightInset: currentRightInset) + textNode.updateLayout(size: CGSize(width: maxTextWidth, height: textHeight)) + + let textBoundingRect = textNode.textView.currentTextBoundingRect().integral + let lastLineBoundingRect = textNode.textView.lastLineBoundingRect().integral + + let textWidth = textBoundingRect.width + let textSize = CGSize(width: textWidth, height: textHeight) + + var positionedTextSize = CGSize(width: textSize.width + textPositioningInsets.left + textPositioningInsets.right, height: textSize.height + textPositioningInsets.top + textPositioningInsets.bottom) + + let effectInset: CGFloat = 12.0 + if effect != nil, lastLineBoundingRect.width > textSize.width - effectInset { + if lastLineBoundingRect != textBoundingRect { + positionedTextSize.height += 11.0 + } else { + positionedTextSize.width += effectInset + } + } + let unclippedPositionedTextHeight = positionedTextSize.height - (textPositioningInsets.top + textPositioningInsets.bottom) + + positionedTextSize.height = min(positionedTextSize.height, maxTextHeight) + + let size = CGSize(width: positionedTextSize.width + textInsets.left + textInsets.right, height: positionedTextSize.height + textInsets.top + textInsets.bottom) + + var textFrame = CGRect(origin: CGPoint(x: textInsets.left - 6.0, y: backgroundFrame.height - 4.0 + textInsets.top), size: positionedTextSize) + if mediaCaptionIsAbove { + textFrame.origin.y = 5.0 + } + + backgroundFrame.size.height += textSize.height + 2.0 + if mediaCaptionIsAbove { + mediaPreviewFrame.origin.y += textSize.height + 2.0 + } + + let backgroundSize = explicitBackgroundSize ?? size + + let previousSize = self.currentSize + self.currentSize = backgroundFrame.size + let _ = previousSize + + let textClippingContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + 1.0, y: backgroundFrame.minY + 1.0), size: CGSize(width: backgroundFrame.width - 1.0 - 7.0, height: backgroundFrame.height - 1.0 - 1.0)) + + var textClippingContainerBounds = CGRect(origin: CGPoint(), size: textClippingContainerFrame.size) + if explicitBackgroundSize != nil, let sourceTextInputView { + textClippingContainerBounds.origin.y = sourceTextInputView.contentOffset.y + } else { + textClippingContainerBounds.origin.y = unclippedPositionedTextHeight - backgroundSize.height + 4.0 + textClippingContainerBounds.origin.y = max(0.0, textClippingContainerBounds.origin.y) + } + + transition.setPosition(view: self.textClippingContainer, position: textClippingContainerFrame.center) + transition.setBounds(view: self.textClippingContainer, bounds: textClippingContainerBounds) + + transition.setFrame(view: textNode.view, frame: CGRect(origin: CGPoint(x: textFrame.minX + textPositioningInsets.left - textClippingContainerFrame.minX, y: textFrame.minY + textPositioningInsets.top - textClippingContainerFrame.minY), size: CGSize(width: maxTextWidth, height: textHeight))) + self.updateTextContents() + } + transition.setFrame(view: sourceMediaPreview.view, frame: mediaPreviewFrame) + transition.setFrame(view: self.backgroundWallpaperNode.view, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.setAlpha(view: self.backgroundWallpaperNode.view, alpha: backgroundAlpha) + self.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.backgroundNode.view, frame: backgroundFrame) + transition.setAlpha(view: self.backgroundNode.view, alpha: backgroundAlpha) + self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition) + if let effectIcon = self.effectIcon, let effectIconSize { if let effectIconView = effectIcon.view { var animateIn = false @@ -367,7 +505,7 @@ final class MessageItemView: UIView { } } - return backgroundSize + return backgroundFrame.size } else { let textNode: ChatInputTextNode if let current = self.textNode { @@ -384,7 +522,6 @@ final class MessageItemView: UIView { } let messageAttributedText = NSMutableAttributedString(attributedString: textString) - //messageAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: presentationData.theme.chat.message.outgoing.primaryTextColor, range: NSMakeRange(0, (messageAttributedText.string as NSString).length)) textNode.attributedText = messageAttributedText } @@ -446,16 +583,6 @@ final class MessageItemView: UIView { let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: positionedTextSize) - self.backgroundNode.setType( - type: .outgoing(.None), - highlighted: false, - graphics: themeGraphics, - maskMode: true, - hasWallpaper: true, - transition: transition.containedViewLayoutTransition, - backgroundNode: backgroundNode - ) - let backgroundSize = explicitBackgroundSize ?? size let previousSize = self.currentSize