Merge pull request #6831 from vector-im/langleyd/6830_wysiwyg_core_formatting
Wysiwyg: Core Formatting
|
@ -18,6 +18,15 @@
|
|||
"version" : "5.12.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "matrix-wysiwyg-composer-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "532183124d973b8432694f29bce3619d184fe1a7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "ogg-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
BIN
Riot/Assets/Images.xcassets/Composer/Bold.imageset/Bold.png
vendored
Normal file
After Width: | Height: | Size: 470 B |
BIN
Riot/Assets/Images.xcassets/Composer/Bold.imageset/Bold@2x.png
vendored
Normal file
After Width: | Height: | Size: 737 B |
BIN
Riot/Assets/Images.xcassets/Composer/Bold.imageset/Bold@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
23
Riot/Assets/Images.xcassets/Composer/Bold.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/Code.imageset/Code.png
vendored
Normal file
After Width: | Height: | Size: 591 B |
BIN
Riot/Assets/Images.xcassets/Composer/Code.imageset/Code@2x.png
vendored
Normal file
After Width: | Height: | Size: 894 B |
BIN
Riot/Assets/Images.xcassets/Composer/Code.imageset/Code@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
23
Riot/Assets/Images.xcassets/Composer/Code.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
6
Riot/Assets/Images.xcassets/Composer/Contents.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
23
Riot/Assets/Images.xcassets/Composer/Indent_increase.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/Indent_increase.imageset/Indent increase.png
vendored
Normal file
After Width: | Height: | Size: 275 B |
BIN
Riot/Assets/Images.xcassets/Composer/Indent_increase.imageset/Indent increase@2x.png
vendored
Normal file
After Width: | Height: | Size: 445 B |
BIN
Riot/Assets/Images.xcassets/Composer/Indent_increase.imageset/Indent increase@3x.png
vendored
Normal file
After Width: | Height: | Size: 584 B |
23
Riot/Assets/Images.xcassets/Composer/Italic.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/Italic.imageset/Italic.png
vendored
Normal file
After Width: | Height: | Size: 435 B |
BIN
Riot/Assets/Images.xcassets/Composer/Italic.imageset/Italic@2x.png
vendored
Normal file
After Width: | Height: | Size: 660 B |
BIN
Riot/Assets/Images.xcassets/Composer/Italic.imageset/Italic@3x.png
vendored
Normal file
After Width: | Height: | Size: 940 B |
23
Riot/Assets/Images.xcassets/Composer/Link.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/Link.imageset/Link.png
vendored
Normal file
After Width: | Height: | Size: 682 B |
BIN
Riot/Assets/Images.xcassets/Composer/Link.imageset/Link@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
Riot/Assets/Images.xcassets/Composer/Link.imageset/Link@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.6 KiB |
23
Riot/Assets/Images.xcassets/Composer/Numbered list.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/Numbered list.imageset/Numbered list.png
vendored
Normal file
After Width: | Height: | Size: 287 B |
BIN
Riot/Assets/Images.xcassets/Composer/Numbered list.imageset/Numbered list@2x.png
vendored
Normal file
After Width: | Height: | Size: 465 B |
BIN
Riot/Assets/Images.xcassets/Composer/Numbered list.imageset/Numbered list@3x.png
vendored
Normal file
After Width: | Height: | Size: 719 B |
23
Riot/Assets/Images.xcassets/Composer/Quote.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/Quote.imageset/Quote.png
vendored
Normal file
After Width: | Height: | Size: 474 B |
BIN
Riot/Assets/Images.xcassets/Composer/Quote.imageset/Quote@2x.png
vendored
Normal file
After Width: | Height: | Size: 848 B |
BIN
Riot/Assets/Images.xcassets/Composer/Quote.imageset/Quote@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
23
Riot/Assets/Images.xcassets/Composer/Strikethrough.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/Strikethrough.imageset/Strikethrough.png
vendored
Normal file
After Width: | Height: | Size: 585 B |
BIN
Riot/Assets/Images.xcassets/Composer/Strikethrough.imageset/Strikethrough@2x.png
vendored
Normal file
After Width: | Height: | Size: 962 B |
BIN
Riot/Assets/Images.xcassets/Composer/Strikethrough.imageset/Strikethrough@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.3 KiB |
23
Riot/Assets/Images.xcassets/Composer/Underlined.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/Underlined.imageset/Underlined.png
vendored
Normal file
After Width: | Height: | Size: 410 B |
BIN
Riot/Assets/Images.xcassets/Composer/Underlined.imageset/Underlined@2x.png
vendored
Normal file
After Width: | Height: | Size: 662 B |
BIN
Riot/Assets/Images.xcassets/Composer/Underlined.imageset/Underlined@3x.png
vendored
Normal file
After Width: | Height: | Size: 980 B |
BIN
Riot/Assets/Images.xcassets/Composer/bullet_list.imageset/Bullet list.png
vendored
Normal file
After Width: | Height: | Size: 217 B |
BIN
Riot/Assets/Images.xcassets/Composer/bullet_list.imageset/Bullet list@2x.png
vendored
Normal file
After Width: | Height: | Size: 347 B |
BIN
Riot/Assets/Images.xcassets/Composer/bullet_list.imageset/Bullet list@3x.png
vendored
Normal file
After Width: | Height: | Size: 586 B |
23
Riot/Assets/Images.xcassets/Composer/bullet_list.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
23
Riot/Assets/Images.xcassets/Composer/indent_decrease.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/indent_decrease.imageset/Indent decrease.png
vendored
Normal file
After Width: | Height: | Size: 274 B |
BIN
Riot/Assets/Images.xcassets/Composer/indent_decrease.imageset/Indent decrease@2x.png
vendored
Normal file
After Width: | Height: | Size: 449 B |
BIN
Riot/Assets/Images.xcassets/Composer/indent_decrease.imageset/Indent decrease@3x.png
vendored
Normal file
After Width: | Height: | Size: 597 B |
23
Riot/Assets/Images.xcassets/Composer/maximise_composer.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/maximise_composer.imageset/maximise_composer.png
vendored
Normal file
After Width: | Height: | Size: 320 B |
BIN
Riot/Assets/Images.xcassets/Composer/maximise_composer.imageset/maximise_composer@2x.png
vendored
Normal file
After Width: | Height: | Size: 450 B |
BIN
Riot/Assets/Images.xcassets/Composer/maximise_composer.imageset/maximise_composer@3x.png
vendored
Normal file
After Width: | Height: | Size: 553 B |
23
Riot/Assets/Images.xcassets/Composer/minimise_composer.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/minimise_composer.imageset/minimise_composer.png
vendored
Normal file
After Width: | Height: | Size: 348 B |
BIN
Riot/Assets/Images.xcassets/Composer/minimise_composer.imageset/minimise_composer@2x.png
vendored
Normal file
After Width: | Height: | Size: 488 B |
BIN
Riot/Assets/Images.xcassets/Composer/minimise_composer.imageset/minimise_composer@3x.png
vendored
Normal file
After Width: | Height: | Size: 638 B |
23
Riot/Assets/Images.xcassets/Composer/start_compose_module.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Composer/start_compose_module.imageset/start_compose_module.png
vendored
Normal file
After Width: | Height: | Size: 292 B |
BIN
Riot/Assets/Images.xcassets/Composer/start_compose_module.imageset/start_compose_module@2x.png
vendored
Normal file
After Width: | Height: | Size: 372 B |
BIN
Riot/Assets/Images.xcassets/Composer/start_compose_module.imageset/start_compose_module@3x.png
vendored
Normal file
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:
|
||||
|
|
|
@ -68,6 +68,8 @@ enum MockAppScreens {
|
|||
MockTemplateUserProfileScreenState.self,
|
||||
MockTemplateRoomListScreenState.self,
|
||||
MockTemplateRoomChatScreenState.self,
|
||||
MockSpaceSelectorScreenState.self
|
||||
MockSpaceSelectorScreenState.self,
|
||||
MockComposerScreenState.self,
|
||||
MockComposerCreateActionListScreenState.self
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
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
|
||||
|
||||
@objc protocol ComposerCreateActionListBridgePresenterDelegate {
|
||||
func composerCreateActionListBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter, action: ComposerCreateAction)
|
||||
func composerCreateActionListBridgePresenterDidDismissInteractively(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter)
|
||||
}
|
||||
|
||||
/// ComposerCreateActionListBridgePresenter enables to start ComposerCreateActionList from a view controller.
|
||||
/// This bridge is used while waiting for global usage of coordinator pattern.
|
||||
/// **WARNING**: This class breaks the Coordinator abstraction and it has been introduced for **Objective-C compatibility only**
|
||||
/// (mainly for integration in legacy view controllers). Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator.
|
||||
@objcMembers
|
||||
final class ComposerCreateActionListBridgePresenter: NSObject {
|
||||
// MARK: - Constants
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let actions: [ComposerCreateAction]
|
||||
private var coordinator: ComposerCreateActionListCoordinator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
weak var delegate: ComposerCreateActionListBridgePresenterDelegate?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(actions: [Int]) {
|
||||
self.actions = actions.compactMap {
|
||||
ComposerCreateAction(rawValue: $0)
|
||||
}
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
// NOTE: Default value feature is not compatible with Objective-C.
|
||||
// func present(from viewController: UIViewController, animated: Bool) {
|
||||
// self.present(from: viewController, animated: animated)
|
||||
// }
|
||||
|
||||
func present(from viewController: UIViewController, animated: Bool) {
|
||||
let composerCreateActionListCoordinator = ComposerCreateActionListCoordinator(actions: actions)
|
||||
composerCreateActionListCoordinator.callback = { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
switch action {
|
||||
case .done(let composeAction):
|
||||
self.delegate?.composerCreateActionListBridgePresenterDelegateDidComplete(self, action: composeAction)
|
||||
case .cancel:
|
||||
self.delegate?.composerCreateActionListBridgePresenterDidDismissInteractively(self)
|
||||
}
|
||||
}
|
||||
let presentable = composerCreateActionListCoordinator.toPresentable()
|
||||
viewController.present(presentable, animated: animated, completion: nil)
|
||||
composerCreateActionListCoordinator.start()
|
||||
|
||||
coordinator = composerCreateActionListCoordinator
|
||||
}
|
||||
|
||||
func dismiss(animated: Bool, completion: (() -> Void)?) {
|
||||
guard let coordinator = coordinator else {
|
||||
return
|
||||
}
|
||||
// Dismiss modal
|
||||
coordinator.toPresentable().dismiss(animated: animated) {
|
||||
self.coordinator = nil
|
||||
|
||||
if let completion = completion {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
/// Actions returned by the coordinator callback
|
||||
enum ComposerCreateActionListCoordinatorAction {
|
||||
case done(ComposerCreateAction)
|
||||
case cancel
|
||||
}
|
||||
|
||||
final class ComposerCreateActionListCoordinator: NSObject, Coordinator, Presentable, UISheetPresentationControllerDelegate {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let hostingController: UIViewController
|
||||
private var view: ComposerCreateActionList
|
||||
private var viewModel: ComposerCreateActionListViewModel
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: ((ComposerCreateActionListCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(actions: [ComposerCreateAction]) {
|
||||
viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: actions))
|
||||
view = ComposerCreateActionList(viewModel: viewModel.context)
|
||||
let hostingVC = VectorHostingController(rootView: view)
|
||||
hostingVC.bottomSheetPreferences = VectorHostingBottomSheetPreferences(detents: [.medium])
|
||||
hostingController = hostingVC
|
||||
super.init()
|
||||
hostingVC.presentationController?.delegate = self
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("[ComposerCreateActionListCoordinator] did start.")
|
||||
viewModel.callback = { result in
|
||||
switch result {
|
||||
case .done(let action):
|
||||
self.callback?(.done(action))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
hostingController
|
||||
}
|
||||
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
callback?(.cancel)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
enum MockComposerCreateActionListScreenState: MockScreenState, CaseIterable {
|
||||
case partialList
|
||||
case fullList
|
||||
|
||||
var screenType: Any.Type {
|
||||
ComposerCreateActionList.self
|
||||
}
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
let actions: [ComposerCreateAction]
|
||||
switch self {
|
||||
case .partialList:
|
||||
actions = [.photoLibrary, .polls]
|
||||
case .fullList:
|
||||
actions = ComposerCreateAction.allCases
|
||||
}
|
||||
let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: actions))
|
||||
|
||||
return (
|
||||
[viewModel],
|
||||
AnyView(ComposerCreateActionList(viewModel: viewModel.context))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum ComposerCreateActionListViewAction {
|
||||
// The user selected an action
|
||||
case selectAction(ComposerCreateAction)
|
||||
}
|
||||
|
||||
enum ComposerCreateActionListViewModelResult: Equatable {
|
||||
// The user selected an action and is done with the screen
|
||||
case done(ComposerCreateAction)
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct ComposerCreateActionListViewState: BindableState {
|
||||
/// The list of composer create actions to display to the user
|
||||
let actions: [ComposerCreateAction]
|
||||
}
|
||||
|
||||
@objc enum ComposerCreateAction: Int {
|
||||
/// Upload a photo/video from the media library
|
||||
case photoLibrary
|
||||
/// Add a sticker
|
||||
case stickers
|
||||
/// Upload an attachment
|
||||
case attachments
|
||||
/// Create a Poll
|
||||
case polls
|
||||
/// Add a location
|
||||
case location
|
||||
/// Upload a photo or video from the camera
|
||||
case camera
|
||||
}
|
||||
|
||||
extension ComposerCreateAction: Equatable, CaseIterable, Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
extension ComposerCreateAction {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .photoLibrary:
|
||||
return VectorL10n.wysiwygComposerStartActionMediaPicker
|
||||
case .stickers:
|
||||
return VectorL10n.wysiwygComposerStartActionStickers
|
||||
case .attachments:
|
||||
return VectorL10n.wysiwygComposerStartActionAttachments
|
||||
case .polls:
|
||||
return VectorL10n.wysiwygComposerStartActionPolls
|
||||
case .location:
|
||||
return VectorL10n.wysiwygComposerStartActionLocation
|
||||
case .camera:
|
||||
return VectorL10n.wysiwygComposerStartActionCamera
|
||||
}
|
||||
}
|
||||
|
||||
var accessibilityIdentifier: String {
|
||||
switch self {
|
||||
case .photoLibrary:
|
||||
return "photoLibraryAction"
|
||||
case .stickers:
|
||||
return "stickersAction"
|
||||
case .attachments:
|
||||
return "attachmentsAction"
|
||||
case .polls:
|
||||
return "pollsAction"
|
||||
case .location:
|
||||
return "locationAction"
|
||||
case .camera:
|
||||
return "cameraAction"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .photoLibrary:
|
||||
return Asset.Images.actionMediaLibrary.name
|
||||
case .stickers:
|
||||
return Asset.Images.actionSticker.name
|
||||
case .attachments:
|
||||
return Asset.Images.actionFile.name
|
||||
case .polls:
|
||||
return Asset.Images.actionPoll.name
|
||||
case .location:
|
||||
return Asset.Images.actionLocation.name
|
||||
case .camera:
|
||||
return Asset.Images.actionCamera.name
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// 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 RiotSwiftUI
|
||||
import XCTest
|
||||
|
||||
class ComposerCreateActionListUITests: MockScreenTestCase {
|
||||
func testFullList() throws {
|
||||
app.goToScreenWithIdentifier(MockComposerCreateActionListScreenState.fullList.title)
|
||||
|
||||
XCTAssert(app.staticTexts[ComposerCreateAction.photoLibrary.accessibilityIdentifier].exists)
|
||||
XCTAssert(app.staticTexts[ComposerCreateAction.location.accessibilityIdentifier].exists)
|
||||
}
|
||||
|
||||
func testPartialList() throws {
|
||||
app.goToScreenWithIdentifier(MockComposerCreateActionListScreenState.partialList.title)
|
||||
|
||||
XCTAssert(app.staticTexts[ComposerCreateAction.photoLibrary.accessibilityIdentifier].exists)
|
||||
XCTAssertFalse(app.staticTexts[ComposerCreateAction.location.accessibilityIdentifier].exists)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
class ComposerCreateActionListTests: XCTestCase {
|
||||
var viewModel: ComposerCreateActionListViewModel!
|
||||
var context: ComposerCreateActionListViewModel.Context!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: ComposerCreateAction.allCases))
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testSelection() throws {
|
||||
let actionToSelect: ComposerCreateAction = .attachments
|
||||
var result: ComposerCreateActionListViewModelResult?
|
||||
viewModel.callback = { callbackResult in
|
||||
result = callbackResult
|
||||
}
|
||||
|
||||
viewModel.context.send(viewAction: .selectAction(actionToSelect))
|
||||
|
||||
XCTAssertEqual(result, .done(actionToSelect))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct ComposerCreateActionList: View {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: ComposerCreateActionListViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(viewModel.viewState.actions) { action in
|
||||
HStack(spacing: 16) {
|
||||
Image(action.icon)
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(theme.colors.accent)
|
||||
Text(action.title)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.font(theme.fonts.body)
|
||||
.accessibilityIdentifier(action.accessibilityIdentifier)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
viewModel.send(viewAction: .selectAction(action))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
}
|
||||
.padding(.top, 16)
|
||||
Spacer()
|
||||
}.background(theme.colors.background.ignoresSafeArea())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct ComposerCreateActionList_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockComposerCreateActionListScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
protocol ComposerCreateActionListViewModelProtocol {
|
||||
var callback: ((ComposerCreateActionListViewModelResult) -> Void)? { get set }
|
||||
var context: ComposerCreateActionListViewModelType.Context { get }
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// Copyright 2021 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 SwiftUI
|
||||
|
||||
typealias ComposerCreateActionListViewModelType = StateStoreViewModel<ComposerCreateActionListViewState, ComposerCreateActionListViewAction>
|
||||
|
||||
class ComposerCreateActionListViewModel: ComposerCreateActionListViewModelType, ComposerCreateActionListViewModelProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: ((ComposerCreateActionListViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: ComposerCreateActionListViewAction) {
|
||||
switch viewAction {
|
||||
case .selectAction(let action):
|
||||
callback?(.done(action))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
import WysiwygComposer
|
||||
|
||||
enum MockComposerScreenState: MockScreenState, CaseIterable {
|
||||
case send
|
||||
case edit
|
||||
case reply
|
||||
|
||||
var screenType: Any.Type {
|
||||
Composer.self
|
||||
}
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: ComposerViewModel
|
||||
|
||||
switch self {
|
||||
case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState())
|
||||
case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit))
|
||||
case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", sendMode: .reply))
|
||||
}
|
||||
|
||||
let wysiwygviewModel = WysiwygComposerViewModel(minHeight: 20, maxHeight: 360)
|
||||
|
||||
viewModel.callback = { [weak viewModel, weak wysiwygviewModel] result in
|
||||
guard let viewModel = viewModel else { return }
|
||||
if viewModel.sendMode == .edit {
|
||||
wysiwygviewModel?.setHtmlContent("")
|
||||
}
|
||||
switch result {
|
||||
case .cancel: viewModel.sendMode = .send
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
[viewModel, wysiwygviewModel],
|
||||
AnyView(VStack {
|
||||
Spacer()
|
||||
Composer(viewModel: viewModel.context, wysiwygViewModel: wysiwygviewModel, sendMessageAction: { _ in }, showSendMediaActions: { })
|
||||
}.frame(
|
||||
minWidth: 0,
|
||||
maxWidth: .infinity,
|
||||
minHeight: 0,
|
||||
maxHeight: .infinity,
|
||||
alignment: .topLeading
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
125
RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift
Normal file
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
import WysiwygComposer
|
||||
|
||||
// MARK: View
|
||||
|
||||
/// An item in the toolbar
|
||||
struct FormatItem {
|
||||
/// The type of the item
|
||||
let type: FormatType
|
||||
/// Whether it is active(highlighted)
|
||||
let active: Bool
|
||||
/// Whether it is disabled or enabled
|
||||
let disabled: Bool
|
||||
}
|
||||
|
||||
/// The types of formatting actions
|
||||
enum FormatType {
|
||||
case bold
|
||||
case italic
|
||||
case strikethrough
|
||||
case underline
|
||||
}
|
||||
|
||||
extension FormatType: CaseIterable, Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
extension FormatItem: Identifiable {
|
||||
var id: FormatType { type }
|
||||
}
|
||||
|
||||
extension FormatItem {
|
||||
/// The icon for the item
|
||||
var icon: String {
|
||||
switch type {
|
||||
case .bold:
|
||||
return Asset.Images.bold.name
|
||||
case .italic:
|
||||
return Asset.Images.italic.name
|
||||
case .strikethrough:
|
||||
return Asset.Images.strikethrough.name
|
||||
case .underline:
|
||||
return Asset.Images.underlined.name
|
||||
}
|
||||
}
|
||||
|
||||
var accessibilityIdentifier: String {
|
||||
switch type {
|
||||
case .bold:
|
||||
return "boldButton"
|
||||
case .italic:
|
||||
return "italicButton"
|
||||
case .strikethrough:
|
||||
return "strikethroughButton"
|
||||
case .underline:
|
||||
return "underlineButton"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FormatType {
|
||||
/// Convenience method to map it to the external ViewModel action
|
||||
var action: WysiwygAction {
|
||||
switch self {
|
||||
case .bold:
|
||||
return .bold
|
||||
case .italic:
|
||||
return .italic
|
||||
case .strikethrough:
|
||||
return .strikeThrough
|
||||
case .underline:
|
||||
return .underline
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We probably don't need to expose this, clean up.
|
||||
|
||||
/// Convenience method to map it to the external rust binging action
|
||||
var composerAction: ComposerAction {
|
||||
switch self {
|
||||
case .bold:
|
||||
return .bold
|
||||
case .italic:
|
||||
return .italic
|
||||
case .strikethrough:
|
||||
return .strikeThrough
|
||||
case .underline:
|
||||
return .underline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ComposerSendMode: Equatable {
|
||||
case send
|
||||
case edit
|
||||
case reply
|
||||
case createDM
|
||||
}
|
||||
|
||||
enum ComposerViewAction {
|
||||
case cancel
|
||||
}
|
||||
|
||||
enum ComposerViewModelResult {
|
||||
case cancel
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
struct ComposerViewState: BindableState {
|
||||
var eventSenderDisplayName: String?
|
||||
var sendMode: ComposerSendMode = .send
|
||||
}
|
||||
|
||||
extension ComposerViewState {
|
||||
var shouldDisplayContext: Bool {
|
||||
return sendMode == .edit || sendMode == .reply
|
||||
}
|
||||
|
||||
var contextDescription: String? {
|
||||
switch sendMode {
|
||||
case .reply:
|
||||
guard let eventSenderDisplayName = eventSenderDisplayName else { return nil }
|
||||
return VectorL10n.roomMessageReplyingTo(eventSenderDisplayName)
|
||||
case .edit: return VectorL10n.roomMessageEditing
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var contextImageName: String? {
|
||||
switch sendMode {
|
||||
case .edit: return Asset.Images.inputEditIcon.name
|
||||
case .reply: return Asset.Images.inputReplyIcon.name
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// 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 RiotSwiftUI
|
||||
import XCTest
|
||||
|
||||
final class ComposerUITests: MockScreenTestCase {
|
||||
func testSendMode() throws {
|
||||
app.goToScreenWithIdentifier(MockComposerScreenState.send.title)
|
||||
|
||||
XCTAssertFalse(app.buttons["cancelButton"].exists)
|
||||
let wysiwygTextView = app.textViews.allElementsBoundByIndex[0]
|
||||
XCTAssertTrue(wysiwygTextView.exists)
|
||||
let sendButton = app.buttons["sendButton"]
|
||||
XCTAssertFalse(sendButton.exists)
|
||||
wysiwygTextView.tap()
|
||||
wysiwygTextView.typeText("test")
|
||||
XCTAssertTrue(sendButton.exists)
|
||||
XCTAssertFalse(app.buttons["editButton"].exists)
|
||||
}
|
||||
|
||||
func testReplyMode() throws {
|
||||
app.goToScreenWithIdentifier(MockComposerScreenState.reply.title)
|
||||
|
||||
let wysiwygTextView = app.textViews.allElementsBoundByIndex[0]
|
||||
XCTAssertTrue(wysiwygTextView.exists)
|
||||
let sendButton = app.buttons["sendButton"]
|
||||
XCTAssertFalse(sendButton.exists)
|
||||
|
||||
let cancelButton = app.buttons["cancelButton"]
|
||||
XCTAssertTrue(cancelButton.exists)
|
||||
|
||||
let contextDescription = app.staticTexts["contextDescription"]
|
||||
XCTAssertTrue(contextDescription.exists)
|
||||
XCTAssert(contextDescription.label == VectorL10n.roomMessageReplyingTo("TestUser"))
|
||||
|
||||
wysiwygTextView.tap()
|
||||
wysiwygTextView.typeText("test")
|
||||
XCTAssertTrue(sendButton.exists)
|
||||
XCTAssertFalse(app.buttons["editButton"].exists)
|
||||
|
||||
cancelButton.tap()
|
||||
let textViewContent = wysiwygTextView.value as! String
|
||||
XCTAssertFalse(textViewContent.isEmpty)
|
||||
XCTAssertFalse(cancelButton.exists)
|
||||
}
|
||||
|
||||
func testEditMode() throws {
|
||||
app.goToScreenWithIdentifier(MockComposerScreenState.edit.title)
|
||||
|
||||
let wysiwygTextView = app.textViews.allElementsBoundByIndex[0]
|
||||
XCTAssertTrue(wysiwygTextView.exists)
|
||||
let editButton = app.buttons["editButton"]
|
||||
XCTAssert(!editButton.exists)
|
||||
|
||||
let cancelButton = app.buttons["cancelButton"]
|
||||
XCTAssertTrue(cancelButton.exists)
|
||||
|
||||
let contextDescription = app.staticTexts["contextDescription"]
|
||||
XCTAssertTrue(contextDescription.exists)
|
||||
XCTAssert(contextDescription.label == VectorL10n.roomMessageEditing)
|
||||
|
||||
wysiwygTextView.tap()
|
||||
wysiwygTextView.typeText("test")
|
||||
XCTAssertTrue(editButton.exists)
|
||||
XCTAssertFalse(app.buttons["sendButton"].exists)
|
||||
|
||||
cancelButton.tap()
|
||||
let textViewContent = wysiwygTextView.value as! String
|
||||
XCTAssertTrue(textViewContent.isEmpty)
|
||||
XCTAssertFalse(cancelButton.exists)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
final class ComposerViewModelTests: XCTestCase {
|
||||
var viewModel: ComposerViewModel!
|
||||
var context: ComposerViewModel.Context!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
viewModel = ComposerViewModel(initialViewState: ComposerViewState())
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testSendState() {
|
||||
viewModel.sendMode = .send
|
||||
XCTAssert(context.viewState.sendMode == .send)
|
||||
XCTAssert(context.viewState.shouldDisplayContext == false)
|
||||
XCTAssert(context.viewState.eventSenderDisplayName == nil)
|
||||
XCTAssert(context.viewState.contextImageName == nil)
|
||||
XCTAssert(context.viewState.contextDescription == nil)
|
||||
}
|
||||
|
||||
func testEditState() {
|
||||
viewModel.sendMode = .edit
|
||||
XCTAssert(context.viewState.sendMode == .edit)
|
||||
XCTAssert(context.viewState.shouldDisplayContext == true)
|
||||
XCTAssert(context.viewState.eventSenderDisplayName == nil)
|
||||
XCTAssert(context.viewState.contextImageName == Asset.Images.inputEditIcon.name)
|
||||
XCTAssert(context.viewState.contextDescription == VectorL10n.roomMessageEditing)
|
||||
}
|
||||
|
||||
func testReplyState() {
|
||||
viewModel.eventSenderDisplayName = "TestUser"
|
||||
viewModel.sendMode = .reply
|
||||
XCTAssert(context.viewState.sendMode == .reply)
|
||||
XCTAssert(context.viewState.shouldDisplayContext == true)
|
||||
XCTAssert(context.viewState.eventSenderDisplayName == "TestUser")
|
||||
XCTAssert(context.viewState.contextImageName == Asset.Images.inputReplyIcon.name)
|
||||
XCTAssert(context.viewState.contextDescription == VectorL10n.roomMessageReplyingTo("TestUser"))
|
||||
}
|
||||
|
||||
func testCancelTapped() {
|
||||
var result: ComposerViewModelResult!
|
||||
viewModel.callback = { value in
|
||||
result = value
|
||||
}
|
||||
context.send(viewAction: .cancel)
|
||||
XCTAssert(result == .cancel)
|
||||
}
|
||||
}
|
192
RiotSwiftUI/Modules/Room/Composer/View/Composer.swift
Normal file
|
@ -0,0 +1,192 @@
|
|||
//
|
||||
// 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 DSBottomSheet
|
||||
import SwiftUI
|
||||
import WysiwygComposer
|
||||
|
||||
struct Composer: View {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
@State private var focused = false
|
||||
@State private var showSendButton = false
|
||||
|
||||
private let horizontalPadding: CGFloat = 12
|
||||
private let borderHeight: CGFloat = 44
|
||||
private let minTextViewHeight: CGFloat = 20
|
||||
private var verticalPadding: CGFloat {
|
||||
(borderHeight - minTextViewHeight) / 2
|
||||
}
|
||||
|
||||
private var topPadding: CGFloat {
|
||||
viewModel.viewState.shouldDisplayContext ? 0 : verticalPadding
|
||||
}
|
||||
|
||||
private var cornerRadius: CGFloat {
|
||||
viewModel.viewState.shouldDisplayContext ? 14 : borderHeight / 2
|
||||
}
|
||||
|
||||
private var actionButtonAccessibilityIdentifier: String {
|
||||
viewModel.viewState.sendMode == .edit ? "editButton" : "sendButton"
|
||||
}
|
||||
|
||||
private var formatItems: [FormatItem] {
|
||||
FormatType.allCases.map { type in
|
||||
FormatItem(
|
||||
type: type,
|
||||
active: wysiwygViewModel.reversedActions.contains(type.composerAction),
|
||||
disabled: wysiwygViewModel.disabledActions.contains(type.composerAction)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: ComposerViewModelType.Context
|
||||
@ObservedObject var wysiwygViewModel: WysiwygComposerViewModel
|
||||
|
||||
let sendMessageAction: (WysiwygComposerContent) -> Void
|
||||
let showSendMediaActions: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
let rect = RoundedRectangle(cornerRadius: cornerRadius)
|
||||
// TODO: Fix maximise animation bugs before re-enabling
|
||||
// ZStack(alignment: .topTrailing) {
|
||||
VStack(spacing: 12) {
|
||||
if viewModel.viewState.shouldDisplayContext {
|
||||
HStack {
|
||||
if let imageName = viewModel.viewState.contextImageName {
|
||||
Image(imageName)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
if let contextDescription = viewModel.viewState.contextDescription {
|
||||
Text(contextDescription)
|
||||
.accessibilityIdentifier("contextDescription")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
viewModel.send(viewAction: .cancel)
|
||||
} label: {
|
||||
Image(Asset.Images.inputCloseIcon.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
.accessibilityIdentifier("cancelButton")
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
}
|
||||
WysiwygComposerView(
|
||||
content: wysiwygViewModel.content,
|
||||
replaceText: wysiwygViewModel.replaceText,
|
||||
select: wysiwygViewModel.select,
|
||||
didUpdateText: wysiwygViewModel.didUpdateText
|
||||
)
|
||||
.textColor(theme.colors.primaryContent)
|
||||
.frame(height: wysiwygViewModel.idealHeight)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.onAppear {
|
||||
wysiwygViewModel.setup()
|
||||
}
|
||||
// Button {
|
||||
// withAnimation(.easeInOut(duration: 0.25)) {
|
||||
// viewModel.maximised.toggle()
|
||||
// }
|
||||
// } label: {
|
||||
// Image(viewModel.maximised ? Asset.Images.minimiseComposer.name : Asset.Images.maximiseComposer.name)
|
||||
// .foregroundColor(theme.colors.tertiaryContent)
|
||||
// }
|
||||
// .padding(.top, 4)
|
||||
// .padding(.trailing, 12)
|
||||
// }
|
||||
.padding(.top, topPadding)
|
||||
.padding(.bottom, verticalPadding)
|
||||
}
|
||||
.clipShape(rect)
|
||||
.overlay(rect.stroke(theme.colors.quinaryContent, lineWidth: 2))
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 4)
|
||||
.onTapGesture {
|
||||
if !focused {
|
||||
focused = true
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Button {
|
||||
showSendMediaActions()
|
||||
} label: {
|
||||
Image(Asset.Images.startComposeModule.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
.padding(11)
|
||||
.background(Circle().fill(theme.colors.system))
|
||||
}
|
||||
FormattingToolbar(formatItems: formatItems) { type in
|
||||
wysiwygViewModel.apply(type.action)
|
||||
}
|
||||
Spacer()
|
||||
ZStack {
|
||||
// TODO: Add support for voice messages
|
||||
// Button {
|
||||
//
|
||||
// } label: {
|
||||
// Image(Asset.Images.voiceMessageRecordButtonDefault.name)
|
||||
// .foregroundColor(theme.colors.tertiaryContent)
|
||||
// }
|
||||
// .isHidden(showSendButton)
|
||||
// .isHidden(true)
|
||||
Button {
|
||||
sendMessageAction(wysiwygViewModel.content)
|
||||
wysiwygViewModel.clearContent()
|
||||
} label: {
|
||||
if viewModel.viewState.sendMode == .edit {
|
||||
Image(Asset.Images.saveIcon.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
} else {
|
||||
Image(Asset.Images.sendIcon.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier(actionButtonAccessibilityIdentifier)
|
||||
.isHidden(!showSendButton)
|
||||
}
|
||||
.onChange(of: wysiwygViewModel.isContentEmpty) { empty in
|
||||
withAnimation(.easeInOut(duration: 0.25)) {
|
||||
showSendButton = !empty
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 4)
|
||||
.animation(.none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Previews
|
||||
|
||||
struct Composer_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockComposerScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
import WysiwygComposer
|
||||
|
||||
struct FormattingToolbar: View {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
/// The list of items to render in the toolbar
|
||||
var formatItems: [FormatItem]
|
||||
/// The action when an item is selected
|
||||
var formatAction: (FormatType) -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
ForEach(formatItems) { item in
|
||||
Button {
|
||||
formatAction(item.type)
|
||||
} label: {
|
||||
Image(item.icon)
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(item.active ? theme.colors.accent : theme.colors.tertiaryContent)
|
||||
}
|
||||
.disabled(item.disabled)
|
||||
.background(item.active ? theme.colors.accent.opacity(0.1) : theme.colors.background)
|
||||
.cornerRadius(8)
|
||||
.accessibilityIdentifier(item.accessibilityIdentifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct FormattingToolbar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
FormattingToolbar(formatItems: [
|
||||
FormatItem(type: .bold, active: true, disabled: false),
|
||||
FormatItem(type: .italic, active: false, disabled: false),
|
||||
FormatItem(type: .strikethrough, active: true, disabled: false),
|
||||
FormatItem(type: .underline, active: false, disabled: true)
|
||||
], formatAction: { _ in })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
typealias ComposerViewModelType = StateStoreViewModel<ComposerViewState, ComposerViewAction>
|
||||
|
||||
final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: ((ComposerViewModelResult) -> Void)?
|
||||
|
||||
var sendMode: ComposerSendMode {
|
||||
get {
|
||||
state.sendMode
|
||||
}
|
||||
set {
|
||||
state.sendMode = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var eventSenderDisplayName: String? {
|
||||
get {
|
||||
state.eventSenderDisplayName
|
||||
}
|
||||
set {
|
||||
state.eventSenderDisplayName = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: ComposerViewAction) {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
callback?(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
protocol ComposerViewModelProtocol {
|
||||
var context: ComposerViewModelType.Context { get }
|
||||
var callback: ((ComposerViewModelResult) -> Void)? { get set }
|
||||
var sendMode: ComposerSendMode { get set }
|
||||
var eventSenderDisplayName: String? { get set }
|
||||
}
|
|
@ -35,6 +35,7 @@ targets:
|
|||
dependencies:
|
||||
- target: DesignKit
|
||||
- package: Mapbox
|
||||
- package: WysiwygComposer
|
||||
sources:
|
||||
- path: .
|
||||
excludes:
|
||||
|
|
|
@ -35,7 +35,8 @@ targets:
|
|||
|
||||
dependencies:
|
||||
- target: RiotSwiftUI
|
||||
|
||||
- package: WysiwygComposer
|
||||
|
||||
settings:
|
||||
base:
|
||||
TEST_TARGET_NAME: RiotSwiftUI
|
||||
|
|
|
@ -51,6 +51,9 @@ packages:
|
|||
SwiftOGG:
|
||||
url: https://github.com/vector-im/swift-ogg
|
||||
branch: main
|
||||
WysiwygComposer:
|
||||
url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift
|
||||
branch: main
|
||||
DeviceKit:
|
||||
url: https://github.com/devicekit/DeviceKit
|
||||
majorVersion: 4.7.0
|
||||
|
|