mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-18 15:38:28 +02:00
* commit 'd786f7bb4f37b77478a8a55df44a6e87247f96c1': (36 commits) finish version++ Release notes version++ changelog.d: Upgrade MatrixSDK version ([v0.27.4](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.27.4)). Fix missing placeholder. Translated using Weblate (Catalan) Translated using Weblate (Catalan) Translated using Weblate (Catalan) Translated using Weblate (Arabic) Translated using Weblate (Arabic) Translated using Weblate (Chinese (Simplified)) Translated using Weblate (Vietnamese) Translated using Weblate (Chinese (Simplified)) Translated using Weblate (Chinese (Simplified)) Fix: Remove the “Quote” action from the menu of the selected message. Update RTE to 2.18.0 to fix an issue with Speech-to-Text Code cleanup Dismiss the keyboard and minimise the composer when pasting an image, a video or a file Fix: focus, keyboard visibility, composer height Restore composer tint color ... # Conflicts: # Config/AppVersion.xcconfig # Riot/Modules/Room/RoomViewController.m
583 lines
22 KiB
Swift
583 lines
22 KiB
Swift
//
|
|
// Copyright 2022 New Vector Ltd
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
import Foundation
|
|
import Reusable
|
|
import WysiwygComposer
|
|
import HTMLParser
|
|
import SwiftUI
|
|
import Combine
|
|
import UIKit
|
|
import CoreGraphics
|
|
|
|
@objc protocol HtmlRoomInputToolbarViewProtocol: RoomInputToolbarViewProtocol {
|
|
@objc var htmlContent: String { get set }
|
|
}
|
|
|
|
// The toolbar for editing with rich text
|
|
|
|
class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInputToolbarViewProtocol {
|
|
// MARK: - Properties
|
|
|
|
// MARK: Private
|
|
private var keyboardHeight: CGFloat = .zero {
|
|
didSet {
|
|
updateTextViewHeight()
|
|
}
|
|
}
|
|
private var voiceMessageToolbarView: VoiceMessageToolbarView?
|
|
private var cancellables = Set<AnyCancellable>()
|
|
private var heightConstraint: NSLayoutConstraint!
|
|
private var voiceMessageBottomConstraint: NSLayoutConstraint?
|
|
private var hostingViewController: VectorHostingController!
|
|
private var wysiwygViewModel = WysiwygComposerViewModel(
|
|
parserStyle: WysiwygInputToolbarView.parserStyle
|
|
)
|
|
/// Compute current HTML parser style for composer.
|
|
private static var parserStyle: HTMLParserStyle {
|
|
return HTMLParserStyle(
|
|
textColor: ThemeService.shared().theme.colors.primaryContent,
|
|
linkColor: ThemeService.shared().theme.colors.links,
|
|
codeBlockStyle: BlockStyle(backgroundColor: ThemeService.shared().theme.selectedBackgroundColor,
|
|
borderColor: ThemeService.shared().theme.textQuinaryColor,
|
|
borderWidth: 1.0,
|
|
cornerRadius: 4.0,
|
|
padding: .init(horizontal: 10.0, vertical: 12.0),
|
|
type: .background),
|
|
quoteBlockStyle: BlockStyle(backgroundColor: ThemeService.shared().theme.selectedBackgroundColor,
|
|
borderColor: ThemeService.shared().theme.selectedBackgroundColor,
|
|
borderWidth: 0.0,
|
|
cornerRadius: 0.0,
|
|
padding: .init(horizontal: 25.0, vertical: 12.0),
|
|
type: .side(offset: 5, width: 4)))
|
|
}
|
|
private var viewModel: ComposerViewModelProtocol!
|
|
|
|
private var isLandscapePhone: Bool {
|
|
let device = UIDevice.current
|
|
return device.isPhone && device.orientation.isLandscape
|
|
}
|
|
|
|
// MARK: Public
|
|
|
|
override var delegate: MXKRoomInputToolbarViewDelegate! {
|
|
didSet {
|
|
setupComposerIfNeeded()
|
|
}
|
|
}
|
|
|
|
override var placeholder: String! {
|
|
get {
|
|
viewModel.placeholder
|
|
}
|
|
set {
|
|
viewModel.placeholder = newValue
|
|
}
|
|
}
|
|
|
|
override var isFocused: Bool {
|
|
viewModel.isFocused
|
|
}
|
|
|
|
override var attributedTextMessage: NSAttributedString? {
|
|
// Note: this is only interactive in plain text mode. If RTE is enabled,
|
|
// APIs from the composer view model should be used.
|
|
get {
|
|
guard !self.textFormattingEnabled else {
|
|
MXLog.failure("[WysiwygInputToolbarView] Trying to get attributedTextMessage in RTE mode")
|
|
return nil
|
|
}
|
|
return self.wysiwygViewModel.textView.attributedText
|
|
}
|
|
set {
|
|
guard !self.textFormattingEnabled else {
|
|
MXLog.failure("[WysiwygInputToolbarView] Trying to set attributedTextMessage in RTE mode")
|
|
return
|
|
}
|
|
self.wysiwygViewModel.textView.attributedText = newValue
|
|
}
|
|
}
|
|
|
|
override var defaultFont: UIFont {
|
|
return UIFont.preferredFont(forTextStyle: .body)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
override func paste(_ sender: Any?) {
|
|
let pasteboard = MXKPasteboardManager.shared.pasteboard
|
|
let types = pasteboard.types.map { UTI(rawValue: $0) }
|
|
|
|
// Minimise the composer and dismiss the keyboard if it's an image, a video or a file
|
|
if types.contains(where: { $0.conforms(to: .image) || $0.conforms(to: .movie) || $0.conforms(to: .video) || $0.conforms(to: .application) }) {
|
|
wysiwygViewModel.maximised = false
|
|
DispatchQueue.main.async {
|
|
self.viewModel.dismissKeyboard()
|
|
}
|
|
}
|
|
super.paste(sender)
|
|
}
|
|
|
|
// MARK: - Setup
|
|
|
|
override class func instantiate() -> MXKRoomInputToolbarView! {
|
|
return loadFromNib()
|
|
}
|
|
|
|
private weak var toolbarViewDelegate: RoomInputToolbarViewDelegate? {
|
|
return (delegate as? RoomInputToolbarViewDelegate) ?? nil
|
|
}
|
|
|
|
private var permalinkReplacer: MentionReplacer? {
|
|
return (delegate as? MentionReplacer)
|
|
}
|
|
|
|
override func awakeFromNib() {
|
|
super.awakeFromNib()
|
|
|
|
setupComposerIfNeeded()
|
|
}
|
|
|
|
override func customizeRendering() {
|
|
super.customizeRendering()
|
|
self.backgroundColor = .clear
|
|
}
|
|
|
|
override func dismissKeyboard() {
|
|
self.viewModel.dismissKeyboard()
|
|
}
|
|
|
|
@discardableResult
|
|
override func becomeFirstResponder() -> Bool {
|
|
self.wysiwygViewModel.textView.becomeFirstResponder()
|
|
}
|
|
|
|
override func dismissValidationView(_ validationView: MXKImageView!) {
|
|
super.dismissValidationView(validationView)
|
|
if isMaximised {
|
|
showKeyboard()
|
|
}
|
|
}
|
|
|
|
override func setPartialContent(_ attributedTextMessage: NSAttributedString) {
|
|
let content: String
|
|
if #available(iOS 15.0, *) {
|
|
content = PillsFormatter.stringByReplacingPills(in: attributedTextMessage, mode: .markdown)
|
|
} else {
|
|
content = attributedTextMessage.string
|
|
}
|
|
self.wysiwygViewModel.setMarkdownContent(content)
|
|
}
|
|
|
|
func showKeyboard() {
|
|
self.wysiwygViewModel.textView.becomeFirstResponder()
|
|
self.viewModel.showKeyboard()
|
|
}
|
|
|
|
func minimise() {
|
|
wysiwygViewModel.maximised = false
|
|
}
|
|
|
|
func performLinkOperation(_ linkOperation: WysiwygLinkOperation) {
|
|
if let selectionToRestore = viewModel.selectionToRestore {
|
|
wysiwygViewModel.select(range: selectionToRestore)
|
|
}
|
|
wysiwygViewModel.applyLinkOperation(linkOperation)
|
|
}
|
|
|
|
func mention(_ member: MXRoomMember) {
|
|
guard let userId = member.userId else {
|
|
return
|
|
}
|
|
|
|
let displayName = member.displayname ?? userId
|
|
|
|
self.wysiwygViewModel.setMention(url: MXTools.permalinkToUser(withUserId: userId),
|
|
name: displayName,
|
|
mentionType: .user)
|
|
}
|
|
|
|
func command(_ command: String) {
|
|
self.wysiwygViewModel.setCommand(name: command)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func setupComposerIfNeeded() {
|
|
guard hostingViewController == nil,
|
|
let toolbarViewDelegate,
|
|
let permalinkReplacer else { return }
|
|
|
|
viewModel = ComposerViewModel(
|
|
initialViewState: ComposerViewState(textFormattingEnabled: RiotSettings.shared.enableWysiwygTextFormatting,
|
|
isLandscapePhone: isLandscapePhone,
|
|
bindings: ComposerBindings(focused: false)))
|
|
|
|
viewModel.callback = { [weak self] result in
|
|
self?.handleViewModelResult(result)
|
|
}
|
|
wysiwygViewModel.plainTextMode = !RiotSettings.shared.enableWysiwygTextFormatting
|
|
wysiwygViewModel.mentionReplacer = permalinkReplacer
|
|
|
|
inputAccessoryViewForKeyboard = UIView(frame: .zero)
|
|
|
|
let composer = Composer(
|
|
viewModel: viewModel.context,
|
|
wysiwygViewModel: wysiwygViewModel,
|
|
completionSuggestionSharedContext: toolbarViewDelegate.completionSuggestionContext().context,
|
|
resizeAnimationDuration: Double(kResizeComposerAnimationDuration),
|
|
sendMessageAction: { [weak self] content in
|
|
guard let self = self else { return }
|
|
self.sendWysiwygMessage(content: content)
|
|
}, showSendMediaActions: { [weak self] in
|
|
guard let self = self else { return }
|
|
self.showSendMediaActions()
|
|
})
|
|
.introspectTextView { [weak self] textView in
|
|
guard let self = self else { return }
|
|
textView.inputAccessoryView = self.inputAccessoryViewForKeyboard
|
|
}
|
|
.environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: toolbarViewDelegate.mediaManager())))
|
|
|
|
hostingViewController = VectorHostingController(rootView: composer)
|
|
hostingViewController.publishHeightChanges = true
|
|
let height = hostingViewController.sizeThatFits(in: CGSize(width: self.frame.width, height: UIView.layoutFittingExpandedSize.height)).height
|
|
let subView: UIView = hostingViewController.view
|
|
self.addSubview(subView)
|
|
|
|
self.translatesAutoresizingMaskIntoConstraints = false
|
|
subView.translatesAutoresizingMaskIntoConstraints = false
|
|
heightConstraint = subView.heightAnchor.constraint(equalToConstant: height)
|
|
NSLayoutConstraint.activate([
|
|
heightConstraint,
|
|
subView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
|
subView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
|
subView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
|
|
])
|
|
|
|
cancellables = [
|
|
hostingViewController.heightPublisher
|
|
.removeDuplicates()
|
|
.sink(receiveValue: { [weak self] idealHeight in
|
|
guard let self = self else { return }
|
|
self.updateToolbarHeight(wysiwygHeight: idealHeight)
|
|
}),
|
|
// Required to update the view constraints after minimise/maximise is tapped
|
|
wysiwygViewModel.$idealHeight
|
|
.removeDuplicates()
|
|
.sink { [weak hostingViewController] _ in
|
|
hostingViewController?.view.setNeedsLayout()
|
|
},
|
|
|
|
wysiwygViewModel.$maximised
|
|
.dropFirst()
|
|
.removeDuplicates()
|
|
.sink { [weak self] value in
|
|
guard let self = self else { return }
|
|
self.toolbarViewDelegate?.didChangeMaximisedState(value)
|
|
self.hostingViewController.view.layer.cornerRadius = value ? 20 : 0
|
|
if !value {
|
|
self.voiceMessageBottomConstraint?.constant = 2
|
|
}
|
|
},
|
|
|
|
wysiwygViewModel.$plainTextContent
|
|
.removeDuplicates()
|
|
.dropFirst()
|
|
.sink { [weak self] attributed in
|
|
// Note: filter out `plainTextMode` being off, as switching to RTE will trigger this
|
|
// publisher with empty content. This avoids saving the partial text message
|
|
// or trying to compute suggestion from this empty content.
|
|
guard let self, self.wysiwygViewModel.plainTextMode else { return }
|
|
self.textMessage = attributed.string
|
|
self.toolbarViewDelegate?.roomInputToolbarViewDidChangeTextMessage(self)
|
|
self.toolbarViewDelegate?.roomInputToolbarView?(self, shouldStorePartialContent: attributed)
|
|
},
|
|
|
|
wysiwygViewModel.$attributedContent
|
|
.removeDuplicates(by: {
|
|
$0.text == $1.text
|
|
})
|
|
.dropFirst()
|
|
.sink { [weak self] _ in
|
|
// Note: filter out `plainTextMode` being on, as switching to plain text mode will trigger this
|
|
// publisher with empty content. This avoids saving the partial text message
|
|
// or trying to compute suggestion from this empty content.
|
|
guard let self, !self.wysiwygViewModel.plainTextMode else { return }
|
|
let markdown = self.wysiwygViewModel.content.markdown
|
|
let attributed = NSAttributedString(string: markdown, attributes: [.font: self.defaultFont])
|
|
self.toolbarViewDelegate?.roomInputToolbarView?(self, shouldStorePartialContent: attributed)
|
|
}
|
|
]
|
|
|
|
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)
|
|
}
|
|
|
|
@objc private func keyboardWillShow(_ notification: Notification) {
|
|
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
|
|
let keyboardRectangle = keyboardFrame.cgRectValue
|
|
keyboardHeight = keyboardRectangle.height
|
|
if self.isMaximised {
|
|
self.voiceMessageBottomConstraint?.constant = keyboardHeight - (window?.safeAreaInsets.bottom ?? 0) + 2
|
|
} else {
|
|
self.voiceMessageBottomConstraint?.constant = 2
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func keyboardWillHide(_ notification: Notification) {
|
|
if self.isMaximised {
|
|
self.voiceMessageBottomConstraint?.constant = 2
|
|
}
|
|
}
|
|
|
|
@objc private func deviceDidRotate(_ notification: Notification) {
|
|
viewModel.isLandscapePhone = isLandscapePhone
|
|
DispatchQueue.main.async {
|
|
self.updateTextViewHeight()
|
|
}
|
|
}
|
|
|
|
private func updateToolbarHeight(wysiwygHeight: CGFloat) {
|
|
self.heightConstraint.constant = wysiwygHeight
|
|
toolbarViewDelegate?.roomInputToolbarView?(self, heightDidChanged: wysiwygHeight, completion: nil)
|
|
}
|
|
|
|
private func sendWysiwygMessage(content: WysiwygComposerContent) {
|
|
// bwi: #4955 disable WYSIWYG commands
|
|
if BWIBuildSettings.shared.enableWYSIWYGCommands && content.markdown.prefix(while: { $0 == "/" }).count == 1 {
|
|
let commandText: String
|
|
if content.markdown.hasPrefix(MXKSlashCommand.emote.cmd) {
|
|
// `/me` command works with markdown content
|
|
commandText = content.markdown
|
|
} else if #available(iOS 15.0, *) {
|
|
// Other commands should see pills replaced by matrix identifiers
|
|
commandText = PillsFormatter.stringByReplacingPills(in: self.wysiwygViewModel.textView.attributedText, mode: .identifier)
|
|
} else {
|
|
// Without Pills support, just use the raw text for command
|
|
commandText = self.wysiwygViewModel.textView.text
|
|
}
|
|
|
|
// Fix potential command failures due to trailing characters
|
|
// or NBSP that are not properly handled by the command interpreter
|
|
let sanitizedCommand = commandText
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
.replacingOccurrences(of: String.nbsp, with: " ")
|
|
|
|
delegate?.roomInputToolbarView?(self, sendCommand: sanitizedCommand)
|
|
} else {
|
|
delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.markdown)
|
|
}
|
|
|
|
if isMaximised {
|
|
minimise()
|
|
}
|
|
}
|
|
|
|
private func showSendMediaActions() {
|
|
delegate?.roomInputToolbarViewShowSendMediaActions?(self)
|
|
}
|
|
|
|
private func handleViewModelResult(_ result: ComposerViewModelResult) {
|
|
switch result {
|
|
case .cancel:
|
|
toolbarViewDelegate?.roomInputToolbarViewDidTapCancel(self)
|
|
case let .contentDidChange(isEmpty):
|
|
setVoiceMessageToolbarIsHidden(!isEmpty)
|
|
case let .linkTapped(linkAction):
|
|
toolbarViewDelegate?.didSendLinkAction(LinkActionWrapper(linkAction))
|
|
case let .suggestion(pattern):
|
|
toolbarViewDelegate?.didDetectTextPattern(SuggestionPatternWrapper(pattern))
|
|
}
|
|
}
|
|
|
|
private func setVoiceMessageToolbarIsHidden(_ isHidden: Bool) {
|
|
guard let voiceMessageToolbarView = voiceMessageToolbarView else { return }
|
|
UIView.transition(
|
|
with: voiceMessageToolbarView, duration: 0.15,
|
|
options: .transitionCrossDissolve,
|
|
animations: {
|
|
voiceMessageToolbarView.isHidden = isHidden
|
|
}
|
|
)
|
|
}
|
|
|
|
private func registerThemeServiceDidChangeThemeNotification() {
|
|
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
|
|
}
|
|
|
|
@objc private func themeDidChange() {
|
|
self.update(theme: ThemeService.shared().theme)
|
|
}
|
|
|
|
private func update(theme: Theme) {
|
|
hostingViewController.view.backgroundColor = theme.colors.background
|
|
wysiwygViewModel.parserStyle = WysiwygInputToolbarView.parserStyle
|
|
}
|
|
|
|
private func updateTextViewHeight() {
|
|
let height = UIScreen.main.bounds.height
|
|
let barOffset: CGFloat = 68
|
|
let toolbarHeight: CGFloat = sendMode == .send ? 96 : 110
|
|
let finalHeight = height - keyboardHeight - toolbarHeight - barOffset
|
|
wysiwygViewModel.maxExpandedHeight = finalHeight
|
|
if finalHeight < 200 {
|
|
wysiwygViewModel.maxCompressedHeight = finalHeight > wysiwygViewModel.minHeight ? finalHeight : wysiwygViewModel.minHeight
|
|
} else {
|
|
wysiwygViewModel.maxCompressedHeight = 200
|
|
}
|
|
}
|
|
|
|
// MARK: - HtmlRoomInputToolbarViewProtocol
|
|
var isEncryptionEnabled = false {
|
|
didSet {
|
|
updatePlaceholderText()
|
|
}
|
|
}
|
|
|
|
/// The current html content of the composer
|
|
var htmlContent: String {
|
|
get {
|
|
wysiwygViewModel.content.html
|
|
}
|
|
set {
|
|
wysiwygViewModel.setHtmlContent(newValue)
|
|
}
|
|
}
|
|
|
|
/// The display name to show when in edit/reply
|
|
var eventSenderDisplayName: String! {
|
|
get {
|
|
viewModel.eventSenderDisplayName
|
|
}
|
|
set {
|
|
viewModel.eventSenderDisplayName = newValue
|
|
}
|
|
}
|
|
|
|
/// Whether the composer is in send, reply or edit mode.
|
|
var sendMode: RoomInputToolbarViewSendMode {
|
|
get {
|
|
viewModel.sendMode.legacySendMode
|
|
}
|
|
set {
|
|
viewModel.sendMode = ComposerSendMode(from: newValue)
|
|
updatePlaceholderText()
|
|
updateTextViewHeight()
|
|
}
|
|
}
|
|
|
|
/// Whether text formatting is currently enabled in the composer.
|
|
var textFormattingEnabled: Bool {
|
|
get {
|
|
self.viewModel.textFormattingEnabled
|
|
}
|
|
set {
|
|
self.viewModel.textFormattingEnabled = newValue
|
|
self.wysiwygViewModel.plainTextMode = !newValue
|
|
}
|
|
}
|
|
|
|
/// Add the voice message toolbar to the composer
|
|
/// - Parameter voiceMessageToolbarView: the voice message toolbar UIView
|
|
func setVoiceMessageToolbarView(_ voiceMessageToolbarView: UIView!) {
|
|
if let voiceMessageToolbarView = voiceMessageToolbarView as? VoiceMessageToolbarView {
|
|
self.voiceMessageToolbarView = voiceMessageToolbarView
|
|
voiceMessageToolbarView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.deactivate(voiceMessageToolbarView.containersTopConstraints)
|
|
addSubview(voiceMessageToolbarView)
|
|
let bottomConstraint = hostingViewController.view.bottomAnchor.constraint(equalTo: voiceMessageToolbarView.bottomAnchor, constant: 2)
|
|
voiceMessageBottomConstraint = bottomConstraint
|
|
NSLayoutConstraint.activate(
|
|
[
|
|
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
|
|
}
|
|
}
|
|
|
|
func toolbarHeight() -> CGFloat {
|
|
return heightConstraint.constant
|
|
}
|
|
}
|
|
|
|
// MARK: - LegacySendModeAdapter
|
|
|
|
fileprivate extension ComposerSendMode {
|
|
init(from sendMode: RoomInputToolbarViewSendMode) {
|
|
switch sendMode {
|
|
case .reply: self = .reply
|
|
case .edit: self = .edit
|
|
case .createDM: self = .createDM
|
|
default: self = .send
|
|
}
|
|
}
|
|
|
|
var legacySendMode: RoomInputToolbarViewSendMode {
|
|
switch self {
|
|
case .createDM: return .createDM
|
|
case .reply: return .reply
|
|
case .edit: return .edit
|
|
case .send: return .send
|
|
}
|
|
}
|
|
}
|