diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h index 2e71969ea..d7bf9d8fc 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h @@ -47,6 +47,7 @@ typedef enum : NSUInteger @class MXKRoomInputToolbarView; +@class MXKImageView; @protocol MXKRoomInputToolbarViewDelegate /** @@ -381,4 +382,6 @@ typedef enum : NSUInteger */ @property (nonatomic) NSAttributedString *attributedTextMessage; +- (void)dismissValidationView:(MXKImageView*)validationView; + @end diff --git a/Riot/Modules/Room/MXKRoomViewController.h b/Riot/Modules/Room/MXKRoomViewController.h index dd3bfd205..7c1eaa577 100644 --- a/Riot/Modules/Room/MXKRoomViewController.h +++ b/Riot/Modules/Room/MXKRoomViewController.h @@ -214,14 +214,14 @@ typedef NS_ENUM(NSUInteger, MXKRoomViewControllerJoinRoomResult) { @property (weak, nonatomic) IBOutlet UITableView *bubblesTableView; @property (weak, nonatomic) IBOutlet UIView *roomTitleViewContainer; -@property (weak, nonatomic) IBOutlet UIView *roomInputToolbarContainer; +@property (strong, nonatomic) IBOutlet UIView *roomInputToolbarContainer; @property (weak, nonatomic) IBOutlet UIView *roomActivitiesContainer; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *bubblesTableViewTopConstraint; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *bubblesTableViewBottomConstraint; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *roomActivitiesContainerHeightConstraint; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *roomInputToolbarContainerHeightConstraint; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *roomInputToolbarContainerBottomConstraint; +@property (strong, nonatomic) IBOutlet NSLayoutConstraint *roomInputToolbarContainerBottomConstraint; #pragma mark - Class methods diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index af49e8f6e..d30891f59 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -72,6 +72,7 @@ extern NSTimeInterval const kResizeComposerAnimationDuration; @property (weak, nonatomic, nullable) IBOutlet UIView *inputBackgroundView; @property (weak, nonatomic, nullable) IBOutlet UIButton *scrollToBottomButton; @property (weak, nonatomic, nullable) IBOutlet BadgeLabel *scrollToBottomBadgeLabel; +@property (nonatomic, strong) IBOutlet UIView *overlayContainerView; // Remove Jitsi widget container @property (weak, nonatomic, nullable) IBOutlet UIView *removeJitsiWidgetContainer; @@ -115,6 +116,11 @@ extern NSTimeInterval const kResizeComposerAnimationDuration; // The voice broadcast service @property (nonatomic, nullable) VoiceBroadcastService *voiceBroadcastService; +@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *toolbarContainerConstraints; + +@property (strong, nonatomic, nullable) UIView* maximisedToolbarDimmingView; + + /** Retrieve the live data source in cases where the timeline is not live. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index a0834940a..25170d023 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -187,7 +187,6 @@ static CGSize kThreadListBarButtonItemImageSize; MXTaskProfile *notificationTaskProfile; } -@property (nonatomic, weak) IBOutlet UIView *overlayContainerView; @property (nonatomic, strong) RemoveJitsiWidgetView *removeJitsiWidgetView; @@ -470,6 +469,9 @@ static CGSize kThreadListBarButtonItemImageSize; self.jumpToLastUnreadBanner.backgroundColor = ThemeService.shared.theme.colors.navigation; [self.jumpToLastUnreadBanner vc_removeShadow]; self.resetReadMarkerButton.tintColor = ThemeService.shared.theme.colors.quarterlyContent; + if (self.maximisedToolbarDimmingView) { + self.maximisedToolbarDimmingView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.29]; + } } else { @@ -481,6 +483,9 @@ static CGSize kThreadListBarButtonItemImageSize; radius:8 opacity:0.1]; self.resetReadMarkerButton.tintColor = ThemeService.shared.theme.colors.tertiaryContent; + if (self.maximisedToolbarDimmingView) { + self.maximisedToolbarDimmingView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.12]; + } } self.scrollToBottomBadgeLabel.badgeColor = ThemeService.shared.theme.tintColor; @@ -603,6 +608,8 @@ static CGSize kThreadListBarButtonItemImageSize; // Stop the loading indicator even if the session is still in progress [self stopLoadingUserIndicator]; + + [self setMaximisedToolbarIsHiddenIfNeeded: YES]; } - (void)viewDidAppear:(BOOL)animated @@ -678,6 +685,8 @@ static CGSize kThreadListBarButtonItemImageSize; // Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early) self.inputToolbarView.attributedTextMessage = self.roomDataSource.partialAttributedTextMessage; } + + [self setMaximisedToolbarIsHiddenIfNeeded: NO]; } - (void)viewDidDisappear:(BOOL)animated @@ -1212,8 +1221,6 @@ static CGSize kThreadListBarButtonItemImageSize; if (!self.inputToolbarView || ![self.inputToolbarView isMemberOfClass:roomInputToolbarViewClass]) { [super setRoomInputToolbarViewClass:roomInputToolbarViewClass]; - - if ([self.inputToolbarView.class conformsToProtocol:@protocol(RoomInputToolbarViewProtocol)]) { id inputToolbar = (id)self.inputToolbarView; [inputToolbar setVoiceMessageToolbarView:self.voiceMessageController.voiceMessageToolbarView]; diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 7ac56f0d2..e53b019e2 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -154,6 +154,85 @@ extension RoomViewController { RiotSettings.shared.enableWysiwygTextFormatting.toggle() wysiwygInputToolbar?.textFormattingEnabled.toggle() } + + @objc func didChangeMaximisedState(_ isMaximised: Bool) { + guard let wysiwygInputToolbar = wysiwygInputToolbar else { return } + if isMaximised { + var view: UIView! + // iPhone + if let navView = self.navigationController?.navigationController?.view { + view = navView + // iPad + } else if let navView = self.navigationController?.view { + view = navView + } else { + return + } + var originalRect = roomInputToolbarContainer.convert(roomInputToolbarContainer.frame, to: view) + var textView: UITextView? + if wysiwygInputToolbar.isFocused { + textView = UITextView() + self.view.window?.addSubview(textView!) + textView?.becomeFirstResponder() + originalRect = wysiwygInputToolbar.convert(wysiwygInputToolbar.frame, to: view) + } + wysiwygInputToolbar.showKeyboard() + roomInputToolbarContainer.removeFromSuperview() + let dimmingView = UIView() + dimmingView.translatesAutoresizingMaskIntoConstraints = false + // Same as the system dimming background color + dimmingView.backgroundColor = .black.withAlphaComponent(ThemeService.shared().isCurrentThemeDark() ? 0.29 : 0.12) + maximisedToolbarDimmingView = dimmingView + view.addSubview(dimmingView) + dimmingView.frame = view.bounds + NSLayoutConstraint.activate( + [ + dimmingView.topAnchor.constraint(equalTo: view.topAnchor), + dimmingView.leftAnchor.constraint(equalTo: view.leftAnchor), + dimmingView.rightAnchor.constraint(equalTo: view.rightAnchor), + dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ] + ) + dimmingView.addSubview(self.roomInputToolbarContainer) + roomInputToolbarContainer.frame = originalRect + roomInputToolbarContainer.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true + roomInputToolbarContainer.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true + roomInputToolbarContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true + UIView.animate(withDuration: kResizeComposerAnimationDuration, delay: 0, options: [.curveEaseInOut]) { + view.layoutIfNeeded() + } + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPanRoomToolbarContainer(_ :))) + roomInputToolbarContainer.addGestureRecognizer(panGesture) + textView?.removeFromSuperview() + } else { + let originalRect = wysiwygInputToolbar.convert(wysiwygInputToolbar.frame, to: view) + var textView: UITextView? + if wysiwygInputToolbar.isFocused { + textView = UITextView() + self.view.window?.addSubview(textView!) + textView?.becomeFirstResponder() + wysiwygInputToolbar.showKeyboard() + } + self.roomInputToolbarContainer.removeFromSuperview() + maximisedToolbarDimmingView?.removeFromSuperview() + maximisedToolbarDimmingView = nil + self.view.insertSubview(self.roomInputToolbarContainer, belowSubview: self.overlayContainerView) + roomInputToolbarContainer.frame = originalRect + NSLayoutConstraint.activate(self.toolbarContainerConstraints) + self.roomInputToolbarContainerBottomConstraint.isActive = true + UIView.animate(withDuration: kResizeComposerAnimationDuration, delay: 0, options: [.curveEaseInOut]) { + self.view.layoutIfNeeded() + } + roomInputToolbarContainer.gestureRecognizers?.removeAll() + textView?.removeFromSuperview() + } + } + + @objc func setMaximisedToolbarIsHiddenIfNeeded(_ isHidden: Bool) { + if wysiwygInputToolbar?.isMaximised == true { + roomInputToolbarContainer.superview?.isHidden = isHidden + } + } } // MARK: - Private Helpers @@ -165,4 +244,28 @@ private extension RoomViewController { var wysiwygInputToolbar: WysiwygInputToolbarView? { return self.inputToolbarView as? WysiwygInputToolbarView } + + @objc private func didPanRoomToolbarContainer(_ sender: UIPanGestureRecognizer) { + guard let wysiwygInputToolbar = wysiwygInputToolbar else { return } + switch sender.state { + case .began: + break + case .changed: + let translation = sender.translation(in: view) + let translatedValue = wysiwygInputToolbar.maxExpandedHeight - translation.y + guard translatedValue <= wysiwygInputToolbar.maxExpandedHeight, translatedValue >= wysiwygInputToolbar.compressedHeight else { return } + wysiwygInputToolbar.idealHeight = translatedValue + case .ended: + if wysiwygInputToolbar.idealHeight <= wysiwygInputToolbar.maxCompressedHeight { + wysiwygInputToolbar.minimise() + } else { + wysiwygInputToolbar.idealHeight = wysiwygInputToolbar.maxExpandedHeight + } + case .cancelled: + wysiwygInputToolbar.idealHeight = wysiwygInputToolbar.maxExpandedHeight + default: + break + } + } + } diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index 073276604..f9be745c3 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -35,6 +35,11 @@ + + + + + diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index 4bdea353b..4e351806c 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -75,6 +75,8 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode) */ - (void)roomInputToolbarView:(RoomInputToolbarView *)toolbarView sendAttributedTextMessage:(NSAttributedString *)attributedTextMessage; +- (void)didChangeMaximisedState: (BOOL) isMaximised; + @end /** diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index e6fad8e09..45c10004f 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -32,9 +32,15 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp // MARK: - Properties // MARK: Private + private var keyboardHeight: CGFloat = .zero { + didSet { + updateTextViewHeight() + } + } private var voiceMessageToolbarView: VoiceMessageToolbarView? private var cancellables = Set() private var heightConstraint: NSLayoutConstraint! + private var voiceMessageBottomConstraint: NSLayoutConstraint? private var hostingViewController: VectorHostingController! private var wysiwygViewModel = WysiwygComposerViewModel(textColor: ThemeService.shared().theme.colors.primaryContent) private var viewModel: ComposerViewModelProtocol = ComposerViewModel( @@ -52,6 +58,35 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } } + override var isFocused: Bool { + viewModel.isFocused + } + + var isMaximised: Bool { + wysiwygViewModel.maximised + } + + var idealHeight: CGFloat { + get { + wysiwygViewModel.idealHeight + } + set { + wysiwygViewModel.idealHeight = newValue + } + } + + var compressedHeight: CGFloat { + wysiwygViewModel.compressedHeight + } + + var maxExpandedHeight: CGFloat { + wysiwygViewModel.maxExpandedHeight + } + + var maxCompressedHeight: CGFloat { + wysiwygViewModel.maxCompressedHeight + } + // MARK: - Setup override class func instantiate() -> MXKRoomInputToolbarView! { @@ -115,11 +150,32 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp .removeDuplicates() .sink { [weak hostingViewController] _ in hostingViewController?.view.setNeedsLayout() + }, + + wysiwygViewModel.$maximised + .removeDuplicates() + .sink { [weak self] value in + guard let self = self else { return } + self.toolbarViewDelegate?.didChangeMaximisedState(value) + self.hostingViewController.view.layer.cornerRadius = value ? 20 : 0 } ] update(theme: ThemeService.shared().theme) registerThemeServiceDidChangeThemeNotification() + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillShow), + name: UIResponder.keyboardWillShowNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillHide), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) + NotificationCenter.default.addObserver(self, selector: #selector(deviceDidRotate), name: UIDevice.orientationDidChangeNotification, object: nil) } override func customizeRendering() { @@ -131,8 +187,53 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp self.viewModel.dismissKeyboard() } + override func dismissValidationView(_ validationView: MXKImageView!) { + super.dismissValidationView(validationView) + if isMaximised { + showKeyboard() + } + } + + func showKeyboard() { + self.viewModel.showKeyboard() + } + + func minimise() { + wysiwygViewModel.maximised = false + } + // MARK: - Private + @objc private func keyboardWillShow(_ notification: Notification) { + if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { + let keyboardRectangle = keyboardFrame.cgRectValue + keyboardHeight = keyboardRectangle.height + UIView.performWithoutAnimation { + if self.isMaximised { + self.voiceMessageBottomConstraint?.constant = keyboardHeight - (window?.safeAreaInsets.bottom ?? 0) + 4 + } else { + self.voiceMessageBottomConstraint?.constant = 4 + } + self.layoutIfNeeded() + } + } + } + + @objc private func keyboardWillHide(_ notification: Notification) { + if self.isMaximised { + UIView.performWithoutAnimation { + self.voiceMessageBottomConstraint?.constant = 4 + self.layoutIfNeeded() + } + } + } + + @objc private func deviceDidRotate(_ notification: Notification) { + DispatchQueue.main.async { + self.updateTextViewHeight() + } + } + private func updateToolbarHeight(wysiwygHeight: CGFloat) { self.heightConstraint.constant = wysiwygHeight toolbarViewDelegate?.roomInputToolbarView?(self, heightDidChanged: wysiwygHeight, completion: nil) @@ -140,6 +241,9 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private func sendWysiwygMessage(content: WysiwygComposerContent) { delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.markdown) + if isMaximised { + minimise() + } } private func showSendMediaActions() { @@ -179,6 +283,14 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp wysiwygViewModel.textColor = theme.colors.primaryContent } + private func updateTextViewHeight() { + let height = UIScreen.main.bounds.height + let barOffset: CGFloat = 68 + let toolbarHeight: CGFloat = 82 + let finalHeight = height - keyboardHeight - toolbarHeight - barOffset + wysiwygViewModel.maxExpandedHeight = finalHeight + } + // MARK: - HtmlRoomInputToolbarViewProtocol var isEncryptionEnabled = false { didSet { @@ -239,17 +351,21 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp voiceMessageToolbarView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.deactivate(voiceMessageToolbarView.containersTopConstraints) addSubview(voiceMessageToolbarView) + let bottomConstraint = hostingViewController.view.bottomAnchor.constraint(equalTo: voiceMessageToolbarView.bottomAnchor, constant: 4) + voiceMessageBottomConstraint = bottomConstraint NSLayoutConstraint.activate( [ - hostingViewController.view.topAnchor.constraint(equalTo: voiceMessageToolbarView.topAnchor), - hostingViewController.view.leftAnchor.constraint(equalTo: voiceMessageToolbarView.leftAnchor), - hostingViewController.view.bottomAnchor.constraint(equalTo: voiceMessageToolbarView.bottomAnchor, constant: 4), - hostingViewController.view.rightAnchor.constraint(equalTo: voiceMessageToolbarView.rightAnchor) + hostingViewController.view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: voiceMessageToolbarView.topAnchor), + hostingViewController.view.safeAreaLayoutGuide.leftAnchor.constraint(equalTo: voiceMessageToolbarView.leftAnchor), + bottomConstraint, + hostingViewController.view.safeAreaLayoutGuide.rightAnchor.constraint(equalTo: voiceMessageToolbarView.rightAnchor) ] ) } else { self.voiceMessageToolbarView?.removeFromSuperview() self.voiceMessageToolbarView = nil + self.voiceMessageBottomConstraint?.isActive = false + self.voiceMessageBottomConstraint = nil } } diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index f255c25a5..c21bd5ed5 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -36,7 +36,7 @@ struct Composer: View { private let horizontalPadding: CGFloat = 12 private let borderHeight: CGFloat = 40 - private var minTextViewHeight: CGFloat = 22 + private let minTextViewHeight: CGFloat = 22 private var verticalPadding: CGFloat { (borderHeight - minTextViewHeight) / 2 } @@ -78,7 +78,7 @@ struct Composer: View { ) } } - + private var composerContainer: some View { let rect = RoundedRectangle(cornerRadius: cornerRadius) return VStack(spacing: 12) { @@ -147,7 +147,7 @@ struct Composer: View { } } } - + private var sendMediaButton: some View { return Button { showSendMediaActions() @@ -162,7 +162,7 @@ struct Composer: View { .padding(.trailing, 8) .accessibilityLabel(VectorL10n.create) } - + private var sendButton: some View { return Button { sendMessageAction(wysiwygViewModel.content) @@ -204,6 +204,12 @@ struct Composer: View { var body: some View { VStack(spacing: 8) { + if wysiwygViewModel.maximised { + RoundedRectangle(cornerRadius: 4) + .fill(theme.colors.quinaryContent) + .frame(width: 36, height: 5) + .padding(.top, 10) + } HStack(alignment: .bottom, spacing: 0) { if !viewModel.viewState.textFormattingEnabled { sendMediaButton diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift index 8ad3ebd27..21c09c89c 100644 --- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift @@ -63,6 +63,10 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol } } + var isFocused: Bool { + state.bindings.focused + } + // MARK: - Public override func process(viewAction: ComposerViewAction) { @@ -77,4 +81,8 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol func dismissKeyboard() { state.bindings.focused = false } + + func showKeyboard() { + state.bindings.focused = true + } } diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift index a1674ff4d..3a7f79af9 100644 --- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift @@ -23,6 +23,8 @@ protocol ComposerViewModelProtocol { var textFormattingEnabled: Bool { get set } var eventSenderDisplayName: String? { get set } var placeholder: String? { get set } + var isFocused: Bool { get } func dismissKeyboard() + func showKeyboard() } diff --git a/changelog.d/7058.change b/changelog.d/7058.change new file mode 100644 index 000000000..bbcc66ac3 --- /dev/null +++ b/changelog.d/7058.change @@ -0,0 +1 @@ +Rich Text Composer: Fullscreen mode now is matching the design requirements. \ No newline at end of file