Merge pull request #6831 from vector-im/langleyd/6830_wysiwyg_core_formatting

Wysiwyg: Core Formatting
This commit is contained in:
David Langley
2022-10-13 16:03:27 +01:00
committed by GitHub
100 changed files with 2342 additions and 164 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Bold.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Bold@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Bold@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Code.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Code@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Code@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Indent increase.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Indent increase@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Indent increase@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Italic.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Italic@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Italic@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Link.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Link@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Link@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Numbered list.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Numbered list@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Numbered list@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Quote.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Quote@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Quote@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Strikethrough.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Strikethrough@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Strikethrough@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Underlined.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Underlined@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Underlined@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Bullet list.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Bullet list@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Bullet list@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Indent decrease.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Indent decrease@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Indent decrease@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "maximise_composer.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "maximise_composer@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "maximise_composer@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "minimise_composer.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "minimise_composer@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "minimise_composer@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "start_compose_module.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "start_compose_module@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "start_compose_module@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

+14
View File
@@ -797,6 +797,7 @@ Tap the + to start adding people.";
"settings_labs_enable_new_session_manager" = "New session manager";
"settings_labs_enable_new_client_info_feature" = "Record the client name, version, and url to recognise sessions more easily in session manager";
"settings_labs_enable_new_app_layout" = "New Application Layout";
"settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor (plain text mode coming soon)";
"settings_version" = "Version %@";
"settings_olm_version" = "Olm Version %@";
@@ -2489,6 +2490,19 @@ To enable access, tap Settings> Location and select Always";
"user_session_overview_current_session_title" = "Current session";
"user_session_overview_session_title" = "Session";
"user_session_overview_session_details_button_title" = "Session details";
// Mark: - WYSIWYG Composer
//Send Media Actions
"wysiwyg_composer_start_action_media_picker" = "Photo Library";
"wysiwyg_composer_start_action_stickers" = "Stickers";
"wysiwyg_composer_start_action_attachments" = "Attachments";
"wysiwyg_composer_start_action_polls" = "Polls";
"wysiwyg_composer_start_action_location" = "Location";
"wysiwyg_composer_start_action_camera" = "Camera";
"wysiwyg_composer_start_action_text_formatting" = "Text Formatting";
// MARK: - MatrixKit
+14
View File
@@ -100,6 +100,20 @@ internal class Asset: NSObject {
internal static let touchidIcon = ImageAsset(name: "touchid_icon")
internal static let addGroupParticipant = ImageAsset(name: "add_group_participant")
internal static let removeIconBlue = ImageAsset(name: "remove_icon_blue")
internal static let indentIncrease = ImageAsset(name: "Indent_increase")
internal static let bold = ImageAsset(name: "bold")
internal static let bulletList = ImageAsset(name: "bullet_list")
internal static let code = ImageAsset(name: "code")
internal static let indentDecrease = ImageAsset(name: "indent_decrease")
internal static let italic = ImageAsset(name: "italic")
internal static let link = ImageAsset(name: "link")
internal static let maximiseComposer = ImageAsset(name: "maximise_composer")
internal static let minimiseComposer = ImageAsset(name: "minimise_composer")
internal static let numberedList = ImageAsset(name: "numbered list")
internal static let quote = ImageAsset(name: "quote")
internal static let startComposeModule = ImageAsset(name: "start_compose_module")
internal static let strikethrough = ImageAsset(name: "strikethrough")
internal static let underlined = ImageAsset(name: "underlined")
internal static let findYourContactsFacepile = ImageAsset(name: "find_your_contacts_facepile")
internal static let captureAvatar = ImageAsset(name: "capture_avatar")
internal static let deleteAvatar = ImageAsset(name: "delete_avatar")
+32
View File
@@ -7535,6 +7535,10 @@ public class VectorL10n: NSObject {
public static var settingsLabsEnableThreads: String {
return VectorL10n.tr("Vector", "settings_labs_enable_threads")
}
/// Try out the rich text editor (plain text mode coming soon)
public static var settingsLabsEnableWysiwygComposer: String {
return VectorL10n.tr("Vector", "settings_labs_enable_wysiwyg_composer")
}
/// Polls
public static var settingsLabsEnabledPolls: String {
return VectorL10n.tr("Vector", "settings_labs_enabled_polls")
@@ -9155,6 +9159,34 @@ public class VectorL10n: NSObject {
public static var widgetStickerPickerNoStickerpacksAlertAddNow: String {
return VectorL10n.tr("Vector", "widget_sticker_picker_no_stickerpacks_alert_add_now")
}
/// Attachments
public static var wysiwygComposerStartActionAttachments: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_start_action_attachments")
}
/// Camera
public static var wysiwygComposerStartActionCamera: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_start_action_camera")
}
/// Location
public static var wysiwygComposerStartActionLocation: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_start_action_location")
}
/// Photo Library
public static var wysiwygComposerStartActionMediaPicker: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_start_action_media_picker")
}
/// Polls
public static var wysiwygComposerStartActionPolls: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_start_action_polls")
}
/// Stickers
public static var wysiwygComposerStartActionStickers: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_start_action_stickers")
}
/// Text Formatting
public static var wysiwygComposerStartActionTextFormatting: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_start_action_text_formatting")
}
/// Yes
public static var yes: String {
return VectorL10n.tr("Vector", "yes")
@@ -172,6 +172,10 @@ final class RiotSettings: NSObject {
@UserDefault(key: "enableClientInformationFeature", defaultValue: false, storage: defaults)
var enableClientInformationFeature
/// Flag indicating if the wysiwyg composer feature is enabled
@UserDefault(key: "enableWysiwygComposer", defaultValue: false, storage: defaults)
var enableWysiwygComposer
// MARK: Calls
/// Indicate if `allowStunServerFallback` settings has been set once.
@@ -16,6 +16,7 @@
import Foundation
import SwiftUI
import Combine
/**
UIHostingController that applies some app-level specific configuration
@@ -25,7 +26,9 @@ class VectorHostingController: UIHostingController<AnyView> {
// MARK: Private
private let forceZeroSafeAreaInsets: Bool
private var theme: Theme
private var heightSubject = CurrentValueSubject<CGFloat, Never>(0)
// MARK: Public
@@ -40,8 +43,12 @@ class VectorHostingController: UIHostingController<AnyView> {
var enableNavigationBarScrollEdgeAppearance = false
/// When non-nil, the style will be applied to the status bar.
var statusBarStyle: UIStatusBarStyle?
private let forceZeroSafeAreaInsets: Bool
/// Whether or not to publish when the height of the view changes.
var publishHeightChanges: Bool = false
/// The publisher to subscribe to if `publishHeightChanges` is enabled.
var heightPublisher: AnyPublisher<CGFloat, Never> {
return heightSubject.eraseToAnyPublisher()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
statusBarStyle ?? super.preferredStatusBarStyle
@@ -104,6 +111,10 @@ class VectorHostingController: UIHostingController<AnyView> {
if #available(iOS 15.0, *) {
self.view.invalidateIntrinsicContentSize()
}
if publishHeightChanges {
let height = sizeThatFits(in: CGSize(width: self.view.frame.width, height: UIView.layoutFittingExpandedSize.height)).height
heightSubject.send(height)
}
}
override func viewSafeAreaInsetsDidChange() {
@@ -92,6 +92,22 @@ typedef enum : NSUInteger
*/
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView sendTextMessage:(NSString*)textMessage;
/**
Tells the delegate that the user wants to send a formatted text message.
@param toolbarView the room input toolbar view.
@param formattedTextMessage the formatted message to send.
@param rawText the raw message to send.
*/
- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendFormattedTextMessage:(NSString *)formattedTextMessage withRawText:(NSString *)rawText;
/**
Tells the delegate that the user wants to display the send media actions.
@param toolbarView the room input toolbar view.
*/
- (void)roomInputToolbarViewShowSendMediaActions:(MXKRoomInputToolbarView *)toolbarView;
/**
Tells the delegate that the user wants to send an image.
@@ -222,7 +238,7 @@ typedef enum : NSUInteger
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKRoomInputToolbarView-inherited` object if successful, `nil` otherwise.
*/
+ (instancetype)roomInputToolbarView;
+ (MXKRoomInputToolbarView *)instantiateRoomInputToolbarView;
/**
The delegate notified when inputs are ready.
@@ -69,7 +69,7 @@
bundle:[NSBundle bundleForClass:[MXKRoomInputToolbarView class]]];
}
+ (instancetype)roomInputToolbarView
+ (MXKRoomInputToolbarView *)instantiateRoomInputToolbarView
{
if ([[self class] nib])
{
@@ -21,7 +21,7 @@ extension RoomDataSource {
private enum Constants {
static let emoteMessageSlashCommandPrefix = String(format: "%@ ", kMXKSlashCmdEmote)
}
// MARK: - NSAttributedString Sending
/// Send a text message to the room.
/// While sending, a fake event will be echoed in the messages list.
@@ -33,7 +33,7 @@ extension RoomDataSource {
func sendAttributedTextMessage(_ attributedText: NSAttributedString,
completion: @escaping (MXResponse<String?>) -> Void) {
var localEcho: MXEvent?
let isEmote = isAttributedTextMessageAnEmote(attributedText)
let sanitized = sanitizedAttributedMessageText(attributedText)
let rawText: String
@@ -43,7 +43,7 @@ extension RoomDataSource {
} else {
rawText = sanitized.string
}
if isEmote {
room.sendEmote(rawText,
formattedText: html,
@@ -57,13 +57,38 @@ extension RoomDataSource {
localEcho: &localEcho,
completion: completion)
}
if localEcho != nil {
self.queueEvent(forProcessing: localEcho, with: self.roomState, direction: .forwards)
self.processQueuedEvents(nil)
}
}
// MARK: - NSAttributedString Sending
/// Send a text message to the room.
/// While sending, a fake event will be echoed in the messages list.
/// Once complete, this local echo will be replaced by the event saved by the homeserver.
///
/// - Parameters:
/// - rawText: the raw text to send
/// - html: the formatted html to send
/// - completion: http operation completion block
func sendFormattedTextMessage(_ rawText: String,
html: String,
completion: @escaping (MXResponse<String?>) -> Void) {
var localEcho: MXEvent?
room.sendTextMessage(rawText,
formattedText: html,
threadId: self.threadId,
localEcho: &localEcho,
completion: completion)
if localEcho != nil {
self.queueEvent(forProcessing: localEcho, with: self.roomState, direction: .forwards)
self.processQueuedEvents(nil)
}
}
/// Send a reply to an event with text message to the room.
///
/// While sending, a fake event will be echoed in the messages list.
@@ -76,8 +101,6 @@ extension RoomDataSource {
func sendReply(to eventToReply: MXEvent,
withAttributedTextMessage attributedText: NSAttributedString,
completion: @escaping (MXResponse<String?>) -> Void) {
var localEcho: MXEvent?
let sanitized = sanitizedAttributedMessageText(attributedText)
let rawText: String
let html: String? = htmlMessageFromSanitizedAttributedText(sanitized)
@@ -86,23 +109,29 @@ extension RoomDataSource {
} else {
rawText = sanitized.string
}
let stringLocalizer: MXSendReplyEventStringLocalizerProtocol = MXKSendReplyEventStringLocalizer()
room.sendReply(to: eventToReply,
textMessage: rawText,
formattedTextMessage: html,
stringLocalizer: stringLocalizer,
threadId: self.threadId,
localEcho: &localEcho,
completion: completion)
if localEcho != nil {
self.queueEvent(forProcessing: localEcho, with: self.roomState, direction: .forwards)
self.processQueuedEvents(nil)
}
handleFormattedSendReply(to: eventToReply, rawText: rawText, html: html, completion: completion)
}
/// Send a reply to an event with a html formatted text message to the room.
///
/// While sending, a fake event will be echoed in the messages list.
/// Once complete, this local echo will be replaced by the event saved by the homeserver.
///
/// - Parameters:
/// - eventToReply: the event to reply
/// - rawText: the raw text to send
/// - htmlText: the html text to send
/// - completion: http operation completion block
func sendReply(to eventToReply: MXEvent,
rawText: String,
htmlText: String,
completion: @escaping (MXResponse<String?>) -> Void) {
handleFormattedSendReply(to: eventToReply, rawText: rawText, html: htmlText, completion: completion)
}
/// Replace a text in an event.
///
/// - Parameters:
@@ -122,29 +151,24 @@ extension RoomDataSource {
} else {
rawText = sanitized.string
}
let eventBody = event.content[kMXMessageBodyKey] as? String
let eventFormattedBody = event.content["formatted_body"] as? String
if rawText != eventBody && (eventFormattedBody == nil || html != eventFormattedBody) {
self.mxSession.aggregations.replaceTextMessageEvent(
event,
withTextMessage: rawText,
formattedText: html,
localEcho: { localEcho in
// Apply the local echo to the timeline
self.updateEvent(withReplace: localEcho)
// Integrate the replace local event into the timeline like when sending a message
// This also allows to manage read receipt on this replace event
self.queueEvent(forProcessing: localEcho, with: self.roomState, direction: .forwards)
self.processQueuedEvents(nil)
},
success: success,
failure: failure)
} else {
failure(nil)
}
handleReplaceFormattedMessage(for: event, rawText: rawText, html: html, success: success, failure: failure)
}
/// Replace a formatted html text in an event
///
/// - Parameters:
/// - event: The event to replace
/// - rawText: The new rawText
/// - html: The new html text
/// - success: A block object called when the operation succeeds. It returns the event id of the event generated on the homeserver
/// - failure: A block object called when the operation fails
func replaceFormattedTextMessage( for event: MXEvent,
rawText: String,
html: String,
success: @escaping ((String?) -> Void),
failure: @escaping ((Error?) -> Void)) {
handleReplaceFormattedMessage(for: event, rawText: rawText, html: html, success: success, failure: failure)
}
/// Retrieve editable attributed text message from an event.
@@ -197,6 +221,10 @@ extension RoomDataSource {
return editableTextMessage
}
@objc func editableHtmlTextMessage(for event: MXEvent) -> String {
event.content["formatted_body"] as? String ?? event.content["body"] as? String ?? ""
}
}
// MARK: - Private Helpers
@@ -230,4 +258,54 @@ private extension RoomDataSource {
func isAttributedTextMessageAnEmote(_ attributedText: NSAttributedString) -> Bool {
return attributedText.string.starts(with: Constants.emoteMessageSlashCommandPrefix)
}
func handleReplaceFormattedMessage(for event: MXEvent,
rawText: String,
html: String?,
success: @escaping ((String?) -> Void),
failure: @escaping ((Error?) -> Void)) {
let eventBody = event.content[kMXMessageBodyKey] as? String
let eventFormattedBody = event.content["formatted_body"] as? String
if rawText != eventBody && (eventFormattedBody == nil || html != eventFormattedBody) {
self.mxSession.aggregations.replaceTextMessageEvent(
event,
withTextMessage: rawText,
formattedText: html,
localEcho: { localEcho in
// Apply the local echo to the timeline
self.updateEvent(withReplace: localEcho)
// Integrate the replace local event into the timeline like when sending a message
// This also allows to manage read receipt on this replace event
self.queueEvent(forProcessing: localEcho, with: self.roomState, direction: .forwards)
self.processQueuedEvents(nil)
},
success: success,
failure: failure)
} else {
failure(nil)
}
}
func handleFormattedSendReply(to eventToReply: MXEvent,
rawText: String,
html: String?,
completion: @escaping (MXResponse<String?>) -> Void) {
var localEcho: MXEvent?
let stringLocalizer: MXSendReplyEventStringLocalizerProtocol = MXKSendReplyEventStringLocalizer()
room.sendReply(to: eventToReply,
textMessage: rawText,
formattedTextMessage: html,
stringLocalizer: stringLocalizer,
threadId: self.threadId,
localEcho: &localEcho,
completion: completion)
if localEcho != nil {
self.queueEvent(forProcessing: localEcho, with: self.roomState, direction: .forwards)
self.processQueuedEvents(nil)
}
}
}
+29 -27
View File
@@ -1116,7 +1116,7 @@
MXLogDebug(@"[MXKRoomVC] setRoomInputToolbarViewClass: Set inputToolbarView to class %@", roomInputToolbarViewClass);
id inputToolbarView = [roomInputToolbarViewClass roomInputToolbarView];
id inputToolbarView = [roomInputToolbarViewClass instantiateRoomInputToolbarView];
self->inputToolbarView = inputToolbarView;
self->inputToolbarView.delegate = self;
@@ -3359,32 +3359,34 @@
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView heightDidChanged:(CGFloat)height completion:(void (^)(BOOL finished))completion
{
_roomInputToolbarContainerHeightConstraint.constant = height;
// Update layout with animation
[UIView animateWithDuration:self.resizeComposerAnimationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn
animations:^{
// We will scroll to bottom if the bottom of the table is currently visible
BOOL shouldScrollToBottom = [self isBubblesTableScrollViewAtTheBottom];
CGFloat bubblesTableViewBottomConst = self->_roomInputToolbarContainerBottomConstraint.constant + self->_roomInputToolbarContainerHeightConstraint.constant + self->_roomActivitiesContainerHeightConstraint.constant;
self->_bubblesTableViewBottomConstraint.constant = bubblesTableViewBottomConst;
// Force to render the view
[self.view layoutIfNeeded];
if (shouldScrollToBottom)
{
[self scrollBubblesTableViewToBottomAnimated:NO];
}
}
completion:^(BOOL finished){
if (completion)
{
completion(finished);
}
}];
// This dispatch fixes a simultaneous accesses crash if this gets called twice quickly in succession
dispatch_async(dispatch_get_main_queue(), ^{
// Update layout with animation
[UIView animateWithDuration:self.resizeComposerAnimationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn
animations:^{
// We will scroll to bottom if the bottom of the table is currently visible
BOOL shouldScrollToBottom = [self isBubblesTableScrollViewAtTheBottom];
self->_roomInputToolbarContainerHeightConstraint.constant = height;
CGFloat bubblesTableViewBottomConst = self->_roomInputToolbarContainerBottomConstraint.constant + self->_roomInputToolbarContainerHeightConstraint.constant + self->_roomActivitiesContainerHeightConstraint.constant;
self->_bubblesTableViewBottomConstraint.constant = bubblesTableViewBottomConst;
// Force to render the view
[self.view layoutIfNeeded];
if (shouldScrollToBottom)
{
[self scrollBubblesTableViewToBottomAnimated:NO];
}
}
completion:^(BOOL finished){
if (completion)
{
completion(finished);
}
}];
});
}
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView sendTextMessage:(NSString*)textMessage
+5 -5
View File
@@ -1,10 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -59,6 +58,7 @@
<constraint firstAttribute="bottom" secondItem="nLd-BP-JAE" secondAttribute="bottom" id="kQ6-Cg-FMi"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<point key="canvasLocation" x="130" y="132"/>
</view>
</objects>
</document>
+165 -52
View File
@@ -97,7 +97,7 @@ static CGSize kThreadListBarButtonItemImageSize;
@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate,
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate>
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate, RoomInputToolbarViewDelegate, ComposerCreateActionListBridgePresenterDelegate>
{
// The preview header
@@ -195,6 +195,7 @@ static CGSize kThreadListBarButtonItemImageSize;
@property (nonatomic, strong) RoomContextualMenuPresenter *roomContextualMenuPresenter;
@property (nonatomic, strong) MXKErrorAlertPresentation *errorPresenter;
@property (nonatomic, strong) NSAttributedString *textMessageBeforeEditing;
@property (nonatomic, strong) NSString *htmlTextBeforeEditing;
@property (nonatomic, strong) EditHistoryCoordinatorBridgePresenter *editHistoryPresenter;
@property (nonatomic, strong) MXKDocumentPickerPresenter *documentPickerPresenter;
@property (nonatomic, strong) EmojiPickerCoordinatorBridgePresenter *emojiPickerCoordinatorBridgePresenter;
@@ -209,6 +210,7 @@ static CGSize kThreadListBarButtonItemImageSize;
@property (nonatomic, strong) ThreadsCoordinatorBridgePresenter *threadsBridgePresenter;
@property (nonatomic, strong) ThreadsBetaCoordinatorBridgePresenter *threadsBetaBridgePresenter;
@property (nonatomic, strong) SlidingModalPresenter *threadsNoticeModalPresenter;
@property (nonatomic, strong) ComposerCreateActionListBridgePresenter *composerCreateActionListBridgePresenter;
@property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded;
@property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden;
@property (nonatomic, getter=isMissedDiscussionsBadgeHidden) BOOL missedDiscussionsBadgeHidden;
@@ -672,9 +674,7 @@ static CGSize kThreadListBarButtonItemImageSize;
{
// Retrieve the potential message partially typed during last room display.
// Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early)
RoomInputToolbarView *inputToolbar = (RoomInputToolbarView *)self.inputToolbarView;
inputToolbar.attributedTextMessage = self.roomDataSource.partialAttributedTextMessage;
self.inputToolbarView.attributedTextMessage = self.roomDataSource.partialAttributedTextMessage;
}
}
@@ -1152,10 +1152,23 @@ static CGSize kThreadListBarButtonItemImageSize;
[self notifyDelegateOnLeaveRoomIfNecessary];
}
+ (Class) mainToolbarClass
{
if (RiotSettings.shared.enableWysiwygComposer)
{
return WysiwygInputToolbarView.class;
}
else
{
return RoomInputToolbarView.class;
}
}
// Set the input toolbar according to the current display
- (void)updateRoomInputToolbarViewClassIfNeeded
{
Class roomInputToolbarViewClass = RoomInputToolbarView.class;
Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass];
BOOL shouldDismissContextualMenu = NO;
@@ -1198,10 +1211,10 @@ static CGSize kThreadListBarButtonItemImageSize;
{
[super setRoomInputToolbarViewClass:roomInputToolbarViewClass];
// The voice message toolbar cannot be set on DisabledInputToolbarView and on new direct chat.
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class] && !self.isNewDirectChat)
{
[(RoomInputToolbarView *)self.inputToolbarView setVoiceMessageToolbarView:self.voiceMessageController.voiceMessageToolbarView];
if ([self.inputToolbarView.class conformsToProtocol:@protocol(RoomInputToolbarViewProtocol)]) {
id<RoomInputToolbarViewProtocol> inputToolbar = (id<RoomInputToolbarViewProtocol>)self.inputToolbarView;
[inputToolbar setVoiceMessageToolbarView:self.voiceMessageController.voiceMessageToolbarView];
}
[self updateInputToolBarViewHeight];
@@ -1214,9 +1227,9 @@ static CGSize kThreadListBarButtonItemImageSize;
{
CGFloat height = 0;
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class])
{
height = ((RoomInputToolbarView*)self.inputToolbarView).mainToolbarHeightConstraint.constant;
if ([self.inputToolbarView.class conformsToProtocol:@protocol(RoomInputToolbarViewProtocol)]) {
id<RoomInputToolbarViewProtocol> inputToolbar = (id<RoomInputToolbarViewProtocol>)self.inputToolbarView;
height = inputToolbar.toolbarHeight;
}
else if ([self.inputToolbarView isKindOfClass:DisabledRoomInputToolbarView.class])
{
@@ -2029,9 +2042,9 @@ static CGSize kThreadListBarButtonItemImageSize;
- (void)setInputToolBarSendMode:(RoomInputToolbarViewSendMode)sendMode forEventWithId:(NSString *)eventId
{
if (self.inputToolbarView && [self.inputToolbarView isKindOfClass:[RoomInputToolbarView class]])
if (self.inputToolbarView && [self inputToolbarConformsToToolbarViewProtocol])
{
RoomInputToolbarView *roomInputToolbarView = (RoomInputToolbarView*)self.inputToolbarView;
MXKRoomInputToolbarView <RoomInputToolbarViewProtocol> *roomInputToolbarView = (MXKRoomInputToolbarView <RoomInputToolbarViewProtocol> *) self.inputToolbarView;
if (eventId)
{
MXEvent *event = [self.roomDataSource eventWithEventId:eventId];
@@ -2165,11 +2178,9 @@ static CGSize kThreadListBarButtonItemImageSize;
UIView *sourceView;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class])
{
sourceView = roomInputToolbarView.attachMediaButton;
sourceView = ((RoomInputToolbarView*)self.inputToolbarView).attachMediaButton;
}
else
{
@@ -2241,6 +2252,7 @@ static CGSize kThreadListBarButtonItemImageSize;
}
- (void)setupActions {
if (![self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
return;
}
@@ -2432,8 +2444,7 @@ static CGSize kThreadListBarButtonItemImageSize;
*/
- (void)sendVideoAsset:(AVAsset *)videoAsset isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset
{
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (!roomInputToolbarView)
if (![self inputToolbarConformsToToolbarViewProtocol])
{
return;
}
@@ -2454,15 +2465,27 @@ static CGSize kThreadListBarButtonItemImageSize;
// Create before sending the message in case of a discussion (direct chat)
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
if (readyToSend)
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
{
[[self inputToolbarViewAsRoomInputToolbarView] sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
[self.inputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
}
// Errors are handled at the request level. This should be improved in case of code rewriting.
}];
}];
compressionPrompt.popoverPresentationController.sourceView = roomInputToolbarView.attachMediaButton;
compressionPrompt.popoverPresentationController.sourceRect = roomInputToolbarView.attachMediaButton.bounds;
UIView *sourceView;
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class])
{
sourceView = ((RoomInputToolbarView*)self.inputToolbarView).attachMediaButton;
}
else
{
sourceView = self.inputToolbarView;
}
compressionPrompt.popoverPresentationController.sourceView = sourceView;
compressionPrompt.popoverPresentationController.sourceRect = sourceView.bounds;
[self presentViewController:compressionPrompt animated:YES completion:nil];
}
@@ -2473,9 +2496,9 @@ static CGSize kThreadListBarButtonItemImageSize;
// Create before sending the message in case of a discussion (direct chat)
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
if (readyToSend)
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
{
[[self inputToolbarViewAsRoomInputToolbarView] sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
[self.inputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
}
// Errors are handled at the request level. This should be improved in case of code rewriting.
}];
@@ -4608,12 +4631,16 @@ static CGSize kThreadListBarButtonItemImageSize;
{
MXEvent *event = [self.roomDataSource eventWithEventId:eventId];
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
if ([self inputToolbarConformsToHtmlToolbarViewProtocol])
{
self.textMessageBeforeEditing = roomInputToolbarView.attributedTextMessage;
roomInputToolbarView.attributedTextMessage = [self.customizedRoomDataSource editableAttributedTextMessageFor:event];
MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *htmlInputToolBarView = (MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *) self.inputToolbarView;
self.htmlTextBeforeEditing = htmlInputToolBarView.htmlContent;
htmlInputToolBarView.htmlContent = [self.customizedRoomDataSource editableHtmlTextMessageFor:event];
}
else if ([self inputToolbarConformsToToolbarViewProtocol])
{
self.textMessageBeforeEditing = self.inputToolbarView.attributedTextMessage;
self.inputToolbarView.attributedTextMessage = [self.customizedRoomDataSource editableAttributedTextMessageFor:event];
}
[self selectEventWithId:eventId inputToolBarSendMode:RoomInputToolbarViewSendModeEdit showTimestamp:YES];
@@ -4621,26 +4648,30 @@ static CGSize kThreadListBarButtonItemImageSize;
- (void)restoreTextMessageBeforeEditing
{
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (self.textMessageBeforeEditing)
if (self.htmlTextBeforeEditing && [self inputToolbarConformsToHtmlToolbarViewProtocol])
{
roomInputToolbarView.attributedTextMessage = self.textMessageBeforeEditing;
MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *htmlInputToolBarView = (MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *) self.inputToolbarView;
htmlInputToolBarView.htmlContent = self.htmlTextBeforeEditing;
}
else if (self.textMessageBeforeEditing && [self inputToolbarConformsToToolbarViewProtocol])
{
self.inputToolbarView.attributedTextMessage = self.textMessageBeforeEditing;
}
self.textMessageBeforeEditing = nil;
self.htmlTextBeforeEditing = nil;
}
- (RoomInputToolbarView*)inputToolbarViewAsRoomInputToolbarView
- (BOOL)inputToolbarConformsToHtmlToolbarViewProtocol
{
RoomInputToolbarView *roomInputToolbarView;
if (self.inputToolbarView && [self.inputToolbarView isKindOfClass:[RoomInputToolbarView class]])
{
roomInputToolbarView = (RoomInputToolbarView*)self.inputToolbarView;
}
return roomInputToolbarView;
return [self.inputToolbarView conformsToProtocol:@protocol(HtmlRoomInputToolbarViewProtocol)];
}
- (BOOL)inputToolbarConformsToToolbarViewProtocol
{
return [self.inputToolbarView conformsToProtocol:@protocol(RoomInputToolbarViewProtocol)];
}
- (void)showDifferentURLsAlertFor:(NSURL *)url visibleURLString:(NSString *)visibleURLString
@@ -4933,7 +4964,7 @@ static CGSize kThreadListBarButtonItemImageSize;
{
if (self.roomInputToolbarContainerHeightConstraint.constant != height)
{
// Hide temporarily the placeholder to prevent its distorsion during height animation
// Hide temporarily the placeholder to prevent its distortion during height animation
if (!savedInputToolbarPlaceholder)
{
savedInputToolbarPlaceholder = toolbarView.placeholder.length ? toolbarView.placeholder : @"";
@@ -4958,7 +4989,7 @@ static CGSize kThreadListBarButtonItemImageSize;
}
}
- (void)roomInputToolbarViewDidTapCancel:(RoomInputToolbarView*)toolbarView
- (void)roomInputToolbarViewDidTapCancel:(MXKRoomInputToolbarView<RoomInputToolbarViewProtocol>*)toolbarView
{
[self cancelEventSelection];
}
@@ -4977,6 +5008,53 @@ static CGSize kThreadListBarButtonItemImageSize;
}
}
- (void)roomInputToolbarView:(RoomInputToolbarView *)toolbarView sendFormattedTextMessage:(NSString *)formattedTextMessage withRawText:(NSString *)rawText
{
// Create before sending the message in case of a discussion (direct chat)
MXWeakify(self);
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
MXStrongifyAndReturnIfNil(self);
if (readyToSend) {
[self sendFormattedTextMessage:rawText htmlMsg:formattedTextMessage];
}
// Errors are handled at the request level. This should be improved in case of code rewriting.
}];
}
- (void)roomInputToolbarViewShowSendMediaActions:(MXKRoomInputToolbarView *)toolbarView
{
NSMutableArray *actionItems = [NSMutableArray new];
if (RiotSettings.shared.roomScreenAllowMediaLibraryAction)
{
[actionItems addObject:@(ComposerCreateActionPhotoLibrary)];
}
if (RiotSettings.shared.roomScreenAllowStickerAction && !self.isNewDirectChat)
{
[actionItems addObject:@(ComposerCreateActionStickers)];
}
if (RiotSettings.shared.roomScreenAllowFilesAction)
{
[actionItems addObject:@(ComposerCreateActionAttachments)];
}
if (BuildSettings.pollsEnabled && self.displayConfiguration.sendingPollsEnabled && !self.isNewDirectChat)
{
[actionItems addObject:@(ComposerCreateActionPolls)];
}
if (BuildSettings.locationSharingEnabled && !self.isNewDirectChat)
{
[actionItems addObject:@(ComposerCreateActionLocation)];
}
if (RiotSettings.shared.roomScreenAllowCameraAction)
{
[actionItems addObject:@(ComposerCreateActionCamera)];
}
self.composerCreateActionListBridgePresenter = [[ComposerCreateActionListBridgePresenter alloc] initWithActions:actionItems];
self.composerCreateActionListBridgePresenter.delegate = self;
[self.composerCreateActionListBridgePresenter presentFrom:self animated:YES];
}
- (void)roomInputToolbarView:(RoomInputToolbarView *)toolbarView sendAttributedTextMessage:(NSAttributedString *)attributedTextMessage
{
// Create before sending the message in case of a discussion (direct chat)
@@ -5323,7 +5401,7 @@ static CGSize kThreadListBarButtonItemImageSize;
else
{
// Enable back the text input
[self setRoomInputToolbarViewClass:RoomInputToolbarView.class];
[self setRoomInputToolbarViewClass:[RoomViewController mainToolbarClass]];
[self updateInputToolBarViewHeight];
// And the extra area
@@ -7575,9 +7653,9 @@ static CGSize kThreadListBarButtonItemImageSize;
// Create before sending the message in case of a discussion (direct chat)
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
if (readyToSend)
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
{
[[self inputToolbarViewAsRoomInputToolbarView] sendSelectedImage:imageData
[self.inputToolbarView sendSelectedImage:imageData
withMimeType:MXKUTI.jpeg.mimeType
andCompressionMode:MediaCompressionHelper.defaultCompressionMode
isPhotoLibraryAsset:NO];
@@ -7610,9 +7688,9 @@ static CGSize kThreadListBarButtonItemImageSize;
// Create before sending the message in case of a discussion (direct chat)
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
if (readyToSend)
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
{
[[self inputToolbarViewAsRoomInputToolbarView] sendSelectedImage:imageData
[self.inputToolbarView sendSelectedImage:imageData
withMimeType:uti.mimeType
andCompressionMode:MediaCompressionHelper.defaultCompressionMode
isPhotoLibraryAsset:YES];
@@ -7639,9 +7717,9 @@ static CGSize kThreadListBarButtonItemImageSize;
// Create before sending the message in case of a discussion (direct chat)
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
if (readyToSend)
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
{
[[self inputToolbarViewAsRoomInputToolbarView] sendSelectedAssets:assets withCompressionMode:MediaCompressionHelper.defaultCompressionMode];
[self.inputToolbarView sendSelectedAssets:assets withCompressionMode:MediaCompressionHelper.defaultCompressionMode];
}
// Errors are handled at the request level. This should be improved in case of code rewriting.
}];
@@ -7880,4 +7958,39 @@ static CGSize kThreadListBarButtonItemImageSize;
}
}
#pragma mark - ComposerCreateActionListBridgePresenter
- (void)composerCreateActionListBridgePresenterDelegateDidComplete:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter action:(enum ComposerCreateAction)action
{
[coordinatorBridgePresenter dismissWithAnimated:true completion:^{
switch (action) {
case ComposerCreateActionPhotoLibrary:
[self showMediaPickerAnimated:YES];
break;
case ComposerCreateActionStickers:
[self roomInputToolbarViewPresentStickerPicker];
break;
case ComposerCreateActionAttachments:
[self roomInputToolbarViewDidTapFileUpload];
break;
case ComposerCreateActionPolls:
[self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self];
break;
case ComposerCreateActionLocation:
[self.delegate roomViewControllerDidRequestLocationSharingFormPresentation:self];
break;
case ComposerCreateActionCamera:
[self showCameraControllerAnimated:YES];
break;
}
self.composerCreateActionListBridgePresenter = nil;
}];
}
- (void)composerCreateActionListBridgePresenterDidDismissInteractively:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter
{
self.composerCreateActionListBridgePresenter = nil;
}
@end
@@ -52,6 +52,55 @@ extension RoomViewController {
}
/// Send the formatted text message and its raw counterpat 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 {
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
@@ -107,4 +156,8 @@ private extension RoomViewController {
var inputToolbar: RoomInputToolbarView? {
return self.inputToolbarView as? RoomInputToolbarView
}
var wysiwygInputToolbar: WysiwygInputToolbarView? {
return self.inputToolbarView as? WysiwygInputToolbarView
}
}
@@ -27,7 +27,7 @@
bundle:[NSBundle bundleForClass:[DisabledRoomInputToolbarView class]]];
}
+ (instancetype)roomInputToolbarView
+ (MXKRoomInputToolbarView *)instantiateRoomInputToolbarView
{
if ([[self class] nib])
{
@@ -33,6 +33,16 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode)
};
@protocol RoomInputToolbarViewProtocol
@property (nonatomic, strong) NSString *eventSenderDisplayName;
@property (nonatomic, assign) RoomInputToolbarViewSendMode sendMode;
- (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView;
- (CGFloat)toolbarHeight;
@end
@protocol RoomInputToolbarViewDelegate <MXKRoomInputToolbarViewDelegate>
/**
@@ -40,7 +50,7 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode)
@param toolbarView the room input toolbar view
*/
- (void)roomInputToolbarViewDidTapCancel:(RoomInputToolbarView*)toolbarView;
- (void)roomInputToolbarViewDidTapCancel:(MXKRoomInputToolbarView<RoomInputToolbarViewProtocol>*)toolbarView;
/**
Inform the delegate that the text message has changed.
@@ -30,7 +30,7 @@ static const NSTimeInterval kActionMenuAttachButtonAnimationDuration = .4;
static const NSTimeInterval kActionMenuContentAlphaAnimationDuration = .2;
static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
@interface RoomInputToolbarView() <UITextViewDelegate, RoomInputToolbarTextViewDelegate>
@interface RoomInputToolbarView() <UITextViewDelegate, RoomInputToolbarTextViewDelegate, RoomInputToolbarViewProtocol>
@property (nonatomic, weak) IBOutlet UIView *mainToolbarView;
@@ -59,7 +59,7 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
@implementation RoomInputToolbarView
@dynamic delegate;
+ (instancetype)roomInputToolbarView
+ (MXKRoomInputToolbarView *)instantiateRoomInputToolbarView
{
UINib *nib = [UINib nibWithNibName:NSStringFromClass([RoomInputToolbarView class]) bundle:nil];
return [nib instantiateWithOwner:nil options:nil].firstObject;
@@ -85,25 +85,6 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
self.textView.inputAccessoryView = inputAccessoryViewForKeyboard;
}
- (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView
{
if (voiceMessageToolbarView) {
_voiceMessageToolbarView = voiceMessageToolbarView;
self.voiceMessageToolbarView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.voiceMessageToolbarView];
[NSLayoutConstraint activateConstraints:@[[self.mainToolbarView.topAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.topAnchor],
[self.mainToolbarView.leftAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.leftAnchor],
[self.mainToolbarView.bottomAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.bottomAnchor],
[self.mainToolbarView.rightAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.rightAnchor]]];
}
else
{
[self.voiceMessageToolbarView removeFromSuperview];
_voiceMessageToolbarView = nil;
}
}
#pragma mark - Override MXKView
-(void)customizeViewRendering
@@ -543,4 +524,28 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
}];
}
#pragma mark - RoomInputToolbarViewProtocol
- (CGFloat)toolbarHeight {
return self.mainToolbarHeightConstraint.constant;
}
- (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView
{
if (voiceMessageToolbarView) {
_voiceMessageToolbarView = voiceMessageToolbarView;
self.voiceMessageToolbarView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.voiceMessageToolbarView];
[NSLayoutConstraint activateConstraints:@[[self.mainToolbarView.topAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.topAnchor],
[self.mainToolbarView.leftAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.leftAnchor],
[self.mainToolbarView.bottomAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.bottomAnchor],
[self.mainToolbarView.rightAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.rightAnchor]]];
}
else
{
[self.voiceMessageToolbarView removeFromSuperview];
_voiceMessageToolbarView = nil;
}
}
@end
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -27,7 +27,7 @@
<action selector="onTouchUpInside:" destination="iN0-l3-epB" eventType="touchUpInside" id="WbU-WH-gwL"/>
</connections>
</button>
<scrollView hidden="YES" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ESv-9w-KJF" customClass="RoomActionsBar" customModule="Riot" customModuleProvider="target">
<scrollView hidden="YES" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ESv-9w-KJF" customClass="RoomActionsBar" customModule="Element" customModuleProvider="target">
<rect key="frame" x="60" y="8" width="540" height="38"/>
<constraints>
<constraint firstAttribute="height" constant="38" id="i6C-gL-ADZ"/>
@@ -77,7 +77,7 @@
<constraint firstItem="48y-kn-7b5" firstAttribute="centerY" secondItem="dVr-ZM-kkX" secondAttribute="centerY" id="z5v-Vy-6tc"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wgb-ON-N29" customClass="RoomInputToolbarTextView" customModule="Riot" customModuleProvider="target">
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wgb-ON-N29" customClass="RoomInputToolbarTextView" customModule="Element" customModuleProvider="target">
<rect key="frame" x="5" y="33" width="474" height="4"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="GrowingTextView"/>
@@ -0,0 +1,200 @@
//
// 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 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 cancellables = Set<AnyCancellable>()
private var heightConstraint: NSLayoutConstraint!
private var hostingViewController: VectorHostingController!
private var wysiwygViewModel = WysiwygComposerViewModel()
private var viewModel: ComposerViewModelProtocol! = ComposerViewModel(initialViewState: ComposerViewState())
// MARK: Public
/// 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)
}
}
// MARK: - Setup
override class func instantiate() -> MXKRoomInputToolbarView! {
return loadFromNib()
}
private weak var toolbarViewDelegate: RoomInputToolbarViewDelegate? {
return (delegate as? RoomInputToolbarViewDelegate) ?? nil
}
override func awakeFromNib() {
super.awakeFromNib()
viewModel.callback = { [weak self] result in
guard let self = self else { return }
switch result {
case .cancel:
self.toolbarViewDelegate?.roomInputToolbarViewDidTapCancel(self)
}
}
let composer = Composer(viewModel: viewModel.context,
wysiwygViewModel: wysiwygViewModel,
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()
})
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)
hostingViewController.view.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)
})
]
update(theme: ThemeService.shared().theme)
registerThemeServiceDidChangeThemeNotification()
}
override func customizeRendering() {
super.customizeRendering()
self.backgroundColor = .clear
}
// MARK: - Private
private func updateToolbarHeight(wysiwygHeight: CGFloat) {
self.heightConstraint.constant = wysiwygHeight
toolbarViewDelegate?.roomInputToolbarView?(self, heightDidChanged: wysiwygHeight, completion: nil)
}
private func sendWysiwygMessage(content: WysiwygComposerContent) {
delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.plainText)
}
private func showSendMediaActions() {
delegate?.roomInputToolbarViewShowSendMediaActions?(self)
}
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
}
// MARK: - RoomInputToolbarViewProtocol
/// Add the voice message toolbar to the composer
/// - Parameter voiceMessageToolbarView: the voice message toolbar UIView
func setVoiceMessageToolbarView(_ voiceMessageToolbarView: UIView!) {
// TODO embed the voice messages UI
}
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
}
}
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view autoresizesSubviews="NO" contentMode="scaleToFill" id="iN0-l3-epB" customClass="WysiwygInputToolbarView" customModule="Element" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="600" height="80"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="139" y="101"/>
</view>
</objects>
</document>
+23 -1
View File
@@ -174,7 +174,8 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE)
LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS,
LABS_ENABLE_LIVE_LOCATION_SHARING,
LABS_ENABLE_NEW_SESSION_MANAGER,
LABS_ENABLE_NEW_CLIENT_INFO_FEATURE
LABS_ENABLE_NEW_CLIENT_INFO_FEATURE,
LABS_ENABLE_WYSIWYG_COMPOSER
};
typedef NS_ENUM(NSUInteger, SECURITY)
@@ -594,6 +595,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
}
[sectionLabs addRowWithTag:LABS_ENABLE_NEW_SESSION_MANAGER];
[sectionLabs addRowWithTag:LABS_ENABLE_NEW_CLIENT_INFO_FEATURE];
if (@available(iOS 15.0, *))
{
[sectionLabs addRowWithTag:LABS_ENABLE_WYSIWYG_COMPOSER];
}
sectionLabs.headerTitle = [VectorL10n settingsLabs];
if (sectionLabs.hasAnyRows)
{
@@ -2549,6 +2554,18 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableNewClientInfoFeature:) forControlEvents:UIControlEventTouchUpInside];
cell = labelAndSwitchCell;
}
else if (row == LABS_ENABLE_WYSIWYG_COMPOSER)
{
MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
labelAndSwitchCell.mxkLabel.text = [VectorL10n settingsLabsEnableWysiwygComposer];
labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.enableWysiwygComposer;
labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableWysiwygComposerFeature:) forControlEvents:UIControlEventTouchUpInside];
cell = labelAndSwitchCell;
}
}
@@ -3311,6 +3328,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
MXSDKOptions.sharedInstance.enableNewClientInformationFeature = isEnabled;
}
- (void)toggleEnableWysiwygComposerFeature:(UISwitch *)sender
{
RiotSettings.shared.enableWysiwygComposer = sender.isOn;
}
- (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender
{
RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn;
+1
View File
@@ -42,6 +42,7 @@ targets:
- package: Mapbox
- package: OrderedCollections
- package: SwiftOGG
- package: WysiwygComposer
- package: DeviceKit
configFiles: