Merge pull request #6831 from vector-im/langleyd/6830_wysiwyg_core_formatting
Wysiwyg: Core Formatting
|
After Width: | Height: | Size: 470 B |
|
After Width: | Height: | Size: 737 B |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 591 B |
|
After Width: | Height: | Size: 894 B |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 275 B |
|
After Width: | Height: | Size: 445 B |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 660 B |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 287 B |
|
After Width: | Height: | Size: 465 B |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 474 B |
|
After Width: | Height: | Size: 848 B |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 585 B |
|
After Width: | Height: | Size: 962 B |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 410 B |
|
After Width: | Height: | Size: 662 B |
|
After Width: | Height: | Size: 980 B |
|
After Width: | Height: | Size: 217 B |
|
After Width: | Height: | Size: 347 B |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 274 B |
|
After Width: | Height: | Size: 449 B |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 320 B |
|
After Width: | Height: | Size: 450 B |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 348 B |
|
After Width: | Height: | Size: 488 B |
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 292 B |
|
After Width: | Height: | Size: 372 B |
|
After Width: | Height: | Size: 502 B |
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -42,6 +42,7 @@ targets:
|
||||
- package: Mapbox
|
||||
- package: OrderedCollections
|
||||
- package: SwiftOGG
|
||||
- package: WysiwygComposer
|
||||
- package: DeviceKit
|
||||
|
||||
configFiles:
|
||||
|
||||