mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-17 15:09:31 +02:00
* commit '7d18f34a75d1f41cc3bc8b6a36c0ab82ff93f0e4': (59 commits) finish version++ Tidy up event formatter issues. version++ changelog.d: Upgrade MatrixSDK version ([v0.26.9](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.9)). Translated using Weblate (Russian) Translated using Weblate (Chinese (Traditional)) Translated using Weblate (Albanian) Translated using Weblate (Hungarian) Translated using Weblate (Chinese (Traditional)) Translated using Weblate (Chinese (Traditional)) Translated using Weblate (Chinese (Traditional)) Translated using Weblate (German) Translated using Weblate (Slovak) Translated using Weblate (Chinese (Traditional)) Translated using Weblate (Indonesian) Translated using Weblate (Estonian) Translated using Weblate (Ukrainian) Translated using Weblate (Italian) Translated using Weblate (Chinese (Traditional)) Translated using Weblate (German) ... # Conflicts: # Config/AppVersion.xcconfig # Riot/Utils/EventFormatter.m # fastlane/Fastfile
426 lines
19 KiB
Swift
426 lines
19 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 HTMLParser
|
|
import UIKit
|
|
import WysiwygComposer
|
|
|
|
extension RoomViewController {
|
|
// MARK: - Override
|
|
open override func mention(_ roomMember: MXRoomMember) {
|
|
if let wysiwygInputToolbar, wysiwygInputToolbar.textFormattingEnabled {
|
|
wysiwygInputToolbar.mention(roomMember)
|
|
wysiwygInputToolbar.becomeFirstResponder()
|
|
} else {
|
|
guard let attributedText = inputToolbarView.attributedTextMessage else { return }
|
|
let newAttributedString = NSMutableAttributedString(attributedString: attributedText)
|
|
|
|
if attributedText.length > 0 {
|
|
if #available(iOS 15.0, *) {
|
|
newAttributedString.append(PillsFormatter.mentionPill(withRoomMember: roomMember,
|
|
isHighlighted: false,
|
|
font: inputToolbarView.defaultFont))
|
|
} else {
|
|
newAttributedString.appendString(roomMember.displayname.count > 0 ? roomMember.displayname : roomMember.userId)
|
|
}
|
|
newAttributedString.appendString(" ")
|
|
} else if roomMember.userId == self.mainSession.myUser.userId {
|
|
newAttributedString.appendString("/me ")
|
|
newAttributedString.addAttribute(.font,
|
|
value: inputToolbarView.defaultFont,
|
|
range: .init(location: 0, length: newAttributedString.length))
|
|
} else {
|
|
if #available(iOS 15.0, *) {
|
|
newAttributedString.append(PillsFormatter.mentionPill(withRoomMember: roomMember,
|
|
isHighlighted: false,
|
|
font: inputToolbarView.defaultFont))
|
|
} else {
|
|
newAttributedString.appendString(roomMember.displayname.count > 0 ? roomMember.displayname : roomMember.userId)
|
|
}
|
|
newAttributedString.appendString(": ")
|
|
}
|
|
|
|
inputToolbarView.attributedTextMessage = newAttributedString
|
|
inputToolbarView.becomeFirstResponder()
|
|
}
|
|
}
|
|
|
|
|
|
/// Send the formatted text message and its raw counterpart to the room
|
|
///
|
|
/// - Parameter rawTextMsg: the raw text message
|
|
/// - Parameter htmlMsg: the html text message
|
|
@objc func sendFormattedTextMessage(_ rawTextMsg: String, htmlMsg: String) {
|
|
let eventModified = self.roomDataSource.event(withEventId: customizedRoomDataSource?.selectedEventId)
|
|
self.setupRoomDataSource { roomDataSource in
|
|
guard let roomDataSource = roomDataSource as? RoomDataSource else { return }
|
|
if self.wysiwygInputToolbar?.sendMode == .reply, let eventModified = eventModified {
|
|
roomDataSource.sendReply(to: eventModified, rawText: rawTextMsg, htmlText: htmlMsg) { response in
|
|
switch response {
|
|
case .success:
|
|
break
|
|
case .failure:
|
|
MXLog.error("[RoomViewController] sendFormattedTextMessage failed while updating event", context: [
|
|
"event_id": eventModified.eventId
|
|
])
|
|
}
|
|
}
|
|
} else if self.wysiwygInputToolbar?.sendMode == .edit, let eventModified = eventModified {
|
|
roomDataSource.replaceFormattedTextMessage(
|
|
for: eventModified,
|
|
rawText: rawTextMsg,
|
|
html: htmlMsg,
|
|
success: { _ in
|
|
//
|
|
},
|
|
failure: { _ in
|
|
MXLog.error("[RoomViewController] sendFormattedTextMessage failed while updating event", context: [
|
|
"event_id": eventModified.eventId
|
|
])
|
|
})
|
|
} else if !self.send(asIRCStyleCommandIfPossible: rawTextMsg) {
|
|
roomDataSource.sendFormattedTextMessage(rawTextMsg, html: htmlMsg) { response in
|
|
switch response {
|
|
case .success:
|
|
break
|
|
case .failure:
|
|
MXLog.error("[RoomViewController] sendFormattedTextMessage failed")
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.customizedRoomDataSource?.selectedEventId != nil {
|
|
self.cancelEventSelection()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Send given attributed text message to the room
|
|
///
|
|
/// - Parameter attributedTextMsg: the attributed text message
|
|
@objc func sendAttributedTextMessage(_ attributedTextMsg: NSAttributedString) {
|
|
|
|
// bwi: evaluate send message performance
|
|
let sendTextMessageProfile = PerformanceProfile(threshold: BWIBuildSettings.shared.sendMessageThreshold)
|
|
sendTextMessageProfile.startMeasurement()
|
|
|
|
let eventModified = self.roomDataSource.event(withEventId: customizedRoomDataSource?.selectedEventId)
|
|
self.setupRoomDataSource { roomDataSource in
|
|
guard let roomDataSource = roomDataSource as? RoomDataSource else { return }
|
|
|
|
if self.inputToolbar?.sendMode == .reply, let eventModified = eventModified {
|
|
roomDataSource.sendReply(to: eventModified,
|
|
withAttributedTextMessage: attributedTextMsg) { response in
|
|
switch response {
|
|
case .success:
|
|
self.finishTextMessageProfil(sendTextMessageProfile)
|
|
case .failure:
|
|
MXLog.error("[RoomViewController] sendAttributedTextMessage failed while updating event", context: [
|
|
"event_id": eventModified.eventId
|
|
])
|
|
sendTextMessageProfile.abortMeasurement()
|
|
}
|
|
}
|
|
} else if self.inputToolbar?.sendMode == .edit, let eventModified = eventModified {
|
|
roomDataSource.replaceAttributedTextMessage(
|
|
for: eventModified,
|
|
withAttributedTextMessage: attributedTextMsg,
|
|
success: { _ in
|
|
self.finishTextMessageProfil(sendTextMessageProfile)
|
|
},
|
|
failure: { _ in
|
|
MXLog.error("[RoomViewController] sendAttributedTextMessage failed while updating event", context: [
|
|
"event_id": eventModified.eventId
|
|
])
|
|
sendTextMessageProfile.abortMeasurement()
|
|
})
|
|
} else {
|
|
roomDataSource.sendAttributedTextMessage(attributedTextMsg) { response in
|
|
switch response {
|
|
case .success:
|
|
self.finishTextMessageProfil(sendTextMessageProfile)
|
|
case .failure:
|
|
MXLog.error("[RoomViewController] sendAttributedTextMessage failed")
|
|
sendTextMessageProfile.abortMeasurement()
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.customizedRoomDataSource?.selectedEventId != nil {
|
|
self.cancelEventSelection()
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func togglePlainTextMode() {
|
|
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 optionalTextView: UITextView?
|
|
if wysiwygInputToolbar.isFocused {
|
|
let textView = UITextView()
|
|
optionalTextView = textView
|
|
self.view.window?.addSubview(textView)
|
|
optionalTextView?.becomeFirstResponder()
|
|
originalRect = wysiwygInputToolbar.convert(wysiwygInputToolbar.frame, to: view)
|
|
}
|
|
// This tirggers a SwiftUI update that is handled correctly on iOS 16, but needs to be dispatchted async on older versions
|
|
// Dispatching on iOS 16 instead causes some weird SwiftUI update behaviours
|
|
if #available(iOS 16, *) {
|
|
wysiwygInputToolbar.showKeyboard()
|
|
} else {
|
|
DispatchQueue.main.async {
|
|
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)
|
|
optionalTextView?.removeFromSuperview()
|
|
} else {
|
|
let originalRect = wysiwygInputToolbar.convert(wysiwygInputToolbar.frame, to: view)
|
|
var optionalTextView: UITextView?
|
|
if wysiwygInputToolbar.isFocused {
|
|
let textView = UITextView()
|
|
optionalTextView = textView
|
|
self.view.window?.addSubview(textView)
|
|
optionalTextView?.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()
|
|
optionalTextView?.removeFromSuperview()
|
|
}
|
|
}
|
|
|
|
@objc func setMaximisedToolbarIsHiddenIfNeeded(_ isHidden: Bool) {
|
|
if wysiwygInputToolbar?.isMaximised == true {
|
|
roomInputToolbarContainer.superview?.isHidden = isHidden
|
|
}
|
|
}
|
|
|
|
@objc func didSendLinkAction(_ linkAction: LinkActionWrapper) {
|
|
let presenter = ComposerLinkActionBridgePresenter(linkAction: linkAction)
|
|
presenter.delegate = self
|
|
composerLinkActionBridgePresenter = presenter
|
|
presenter.present(from: self, animated: true)
|
|
}
|
|
|
|
@objc func showWaitingOtherParticipantHeader() {
|
|
let controller = VectorHostingController(rootView: RoomWaitingForMembers())
|
|
guard let headerView = controller.view else {
|
|
return
|
|
}
|
|
self.waitingOtherParticipantViewController = controller
|
|
self.addChild(controller)
|
|
|
|
let containerView = UIView()
|
|
containerView.translatesAutoresizingMaskIntoConstraints = false
|
|
headerView.translatesAutoresizingMaskIntoConstraints = false
|
|
containerView.vc_addSubViewMatchingParent(headerView, withInsets: UIEdgeInsets(top: 9, left: 9, bottom: -9, right: -9))
|
|
|
|
self.bubblesTableView.tableHeaderView = containerView
|
|
NSLayoutConstraint.activate([
|
|
containerView.centerXAnchor.constraint(equalTo: self.bubblesTableView.centerXAnchor),
|
|
containerView.widthAnchor.constraint(equalTo: self.bubblesTableView.widthAnchor),
|
|
containerView.topAnchor.constraint(equalTo: self.bubblesTableView.topAnchor)
|
|
])
|
|
controller.didMove(toParent: self)
|
|
|
|
self.bubblesTableView.tableHeaderView?.layoutIfNeeded()
|
|
}
|
|
|
|
@objc func hideWaitingOtherParticipantHeader() {
|
|
guard let waitingOtherParticipantViewController else {
|
|
return
|
|
}
|
|
waitingOtherParticipantViewController.removeFromParent()
|
|
self.bubblesTableView.tableHeaderView = nil
|
|
waitingOtherParticipantViewController.didMove(toParent: nil)
|
|
self.waitingOtherParticipantViewController = nil
|
|
}
|
|
|
|
@objc func waitForOtherParticipant(_ wait: Bool) {
|
|
self.isWaitingForOtherParticipants = wait
|
|
if wait {
|
|
showWaitingOtherParticipantHeader()
|
|
} else {
|
|
hideWaitingOtherParticipantHeader()
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
private extension RoomViewController {
|
|
var inputToolbar: RoomInputToolbarView? {
|
|
return self.inputToolbarView as? RoomInputToolbarView
|
|
}
|
|
|
|
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:
|
|
wysiwygTranslation = wysiwygInputToolbar.maxExpandedHeight
|
|
case .changed:
|
|
let translation = sender.translation(in: view.window)
|
|
let translatedValue = wysiwygInputToolbar.maxExpandedHeight - translation.y
|
|
wysiwygTranslation = translatedValue
|
|
guard translatedValue <= wysiwygInputToolbar.maxExpandedHeight, translatedValue >= wysiwygInputToolbar.compressedHeight else { return }
|
|
wysiwygInputToolbar.idealHeight = translatedValue
|
|
case .ended:
|
|
if wysiwygTranslation <= wysiwygInputToolbar.maxCompressedHeight {
|
|
wysiwygInputToolbar.minimise()
|
|
} else {
|
|
wysiwygTranslation = wysiwygInputToolbar.maxExpandedHeight
|
|
wysiwygInputToolbar.idealHeight = wysiwygInputToolbar.maxExpandedHeight
|
|
}
|
|
case .cancelled:
|
|
wysiwygTranslation = wysiwygInputToolbar.maxExpandedHeight
|
|
wysiwygInputToolbar.idealHeight = wysiwygInputToolbar.maxExpandedHeight
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
extension RoomViewController: ComposerLinkActionBridgePresenterDelegate {
|
|
func didRequestLinkOperation(_ linkOperation: WysiwygLinkOperation) {
|
|
dismissPresenter { [weak self] in
|
|
self?.wysiwygInputToolbar?.performLinkOperation(linkOperation)
|
|
}
|
|
}
|
|
|
|
func didDismissInteractively() {
|
|
cleanup()
|
|
}
|
|
|
|
func didCancel() {
|
|
dismissPresenter(completion: nil)
|
|
}
|
|
|
|
private func dismissPresenter(completion: (() -> Void)?) {
|
|
self.composerLinkActionBridgePresenter?.dismiss(animated: true) { [weak self] in
|
|
completion?()
|
|
self?.cleanup()
|
|
}
|
|
}
|
|
|
|
private func cleanup() {
|
|
composerLinkActionBridgePresenter = nil
|
|
}
|
|
}
|
|
|
|
// MARK: - PermalinkReplacer
|
|
extension RoomViewController: PermalinkReplacer {
|
|
public func replacementForLink(_ url: String, text: String) -> NSAttributedString? {
|
|
guard #available(iOS 15.0, *),
|
|
let url = URL(string: url),
|
|
let session = roomDataSource.mxSession,
|
|
let eventFormatter = roomDataSource.eventFormatter,
|
|
let roomState = roomDataSource.roomState else {
|
|
return nil
|
|
}
|
|
|
|
return PillsFormatter.mentionPill(withUrl: url,
|
|
andLabel: text,
|
|
session: session,
|
|
eventFormatter: eventFormatter,
|
|
roomState: roomState)
|
|
}
|
|
|
|
public func postProcessMarkdown(in attributedString: NSAttributedString) -> NSAttributedString {
|
|
guard #available(iOS 15.0, *),
|
|
let roomDataSource,
|
|
let session = roomDataSource.mxSession,
|
|
let eventFormatter = roomDataSource.eventFormatter,
|
|
let roomState = roomDataSource.roomState else {
|
|
return attributedString
|
|
}
|
|
return PillsFormatter.insertPills(in: attributedString,
|
|
withSession: session,
|
|
eventFormatter: eventFormatter,
|
|
roomState: roomState,
|
|
font: inputToolbarView.defaultFont)
|
|
}
|
|
|
|
public func restoreMarkdown(in attributedString: NSAttributedString) -> String {
|
|
if #available(iOS 15.0, *) {
|
|
return PillsFormatter.stringByReplacingPills(in: attributedString, mode: .markdown)
|
|
} else {
|
|
return attributedString.string
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - VoiceBroadcast
|
|
extension RoomViewController {
|
|
@objc func stopUncompletedVoiceBroadcastIfNeeded() {
|
|
self.roomDataSource?.room.stopUncompletedVoiceBroadcastIfNeeded()
|
|
}
|
|
}
|