From c0cf534845d55ca1572d95ee67f0a23ff17c3d84 Mon Sep 17 00:00:00 2001 From: Constantin Wartenburger Date: Sat, 10 Oct 2020 16:36:04 +0200 Subject: [PATCH 01/90] Added commands from element web --- .../im/vector/app/features/command/Command.kt | 7 +- .../app/features/command/CommandParser.kt | 79 ++++++++++++++++--- .../app/features/command/ParsedCommand.kt | 7 +- .../home/room/detail/RoomDetailFragment.kt | 1 + .../home/room/detail/RoomDetailViewEvents.kt | 12 +-- .../home/room/detail/RoomDetailViewModel.kt | 68 +++++++++++++--- vector/src/main/res/values/strings.xml | 5 ++ 7 files changed, 149 insertions(+), 30 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index db429f9e58..c1b30b2744 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -28,8 +28,11 @@ enum class Command(val command: String, val parameters: String, @StringRes val d EMOTE("/me", "", R.string.command_description_emote), BAN_USER("/ban", " [reason]", R.string.command_description_ban_user), UNBAN_USER("/unban", " [reason]", R.string.command_description_unban_user), + IGNORE_USER("/ignore", " [reason]", R.string.command_description_ignore_user), + UNIGNORE_USER("/unignore", "", R.string.command_description_unignore_user), SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user), RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user), + ROOM_NAME("/roomname", " [reason]", R.string.command_description_room_name), INVITE("/invite", " [reason]", R.string.command_description_invite_user), JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room), PART("/part", " [reason]", R.string.command_description_part_room), @@ -42,8 +45,10 @@ enum class Command(val command: String, val parameters: String, @StringRes val d CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token), SPOILER("/spoiler", "", R.string.command_description_spoiler), POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll), - SHRUG("/shrug", "", R.string.command_description_shrug), + SHRUG("/shrug", "[]", R.string.command_description_shrug), + LENNY("/lenny", "[]", R.string.command_description_lenny), PLAIN("/plain", "", R.string.command_description_plain), + WHOIS("/whois", "", R.string.command_description_whois), DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session); val length diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 94de6bf265..fd7d587c1c 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -102,7 +102,7 @@ object CommandParser { ParsedCommand.SendRainbowEmote(message) } - Command.JOIN_ROOM.command -> { + Command.JOIN_ROOM.command -> { if (messageParts.size >= 2) { val roomAlias = messageParts[1] @@ -120,7 +120,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.JOIN_ROOM) } } - Command.PART.command -> { + Command.PART.command -> { if (messageParts.size >= 2) { val roomAlias = messageParts[1] @@ -138,7 +138,16 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.PART) } } - Command.INVITE.command -> { + Command.ROOM_NAME.command -> { + val newRoomName = textMessage.substring(Command.ROOM_NAME.command.length).trim() + + if (newRoomName.isNotEmpty()) { + ParsedCommand.ChangeRoomName(newRoomName) + } else { + ParsedCommand.ErrorSyntax(Command.ROOM_NAME) + } + } + Command.INVITE.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -183,7 +192,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.KICK_USER) } } - Command.BAN_USER.command -> { + Command.BAN_USER.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -201,7 +210,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.BAN_USER) } } - Command.UNBAN_USER.command -> { + Command.UNBAN_USER.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -219,7 +228,33 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.UNBAN_USER) } } - Command.SET_USER_POWER_LEVEL.command -> { + Command.IGNORE_USER.command -> { + if (messageParts.size == 2) { + val userId = messageParts[1] + + if (MatrixPatterns.isUserId(userId)) { + ParsedCommand.IgnoreUser(userId) + } else { + ParsedCommand.ErrorSyntax(Command.IGNORE_USER) + } + } else { + ParsedCommand.ErrorSyntax(Command.IGNORE_USER) + } + } + Command.UNIGNORE_USER.command -> { + if (messageParts.size == 2) { + val userId = messageParts[1] + + if (MatrixPatterns.isUserId(userId)) { + ParsedCommand.UnignoreUser(userId) + } else { + ParsedCommand.ErrorSyntax(Command.UNIGNORE_USER) + } + } else { + ParsedCommand.ErrorSyntax(Command.UNIGNORE_USER) + } + } + Command.SET_USER_POWER_LEVEL.command -> { if (messageParts.size == 3) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { @@ -252,7 +287,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) } } - Command.MARKDOWN.command -> { + Command.MARKDOWN.command -> { if (messageParts.size == 2) { when { "on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true) @@ -263,23 +298,28 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.MARKDOWN) } } - Command.CLEAR_SCALAR_TOKEN.command -> { + Command.CLEAR_SCALAR_TOKEN.command -> { if (messageParts.size == 1) { ParsedCommand.ClearScalarToken } else { ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN) } } - Command.SPOILER.command -> { + Command.SPOILER.command -> { val message = textMessage.substring(Command.SPOILER.command.length).trim() ParsedCommand.SendSpoiler(message) } - Command.SHRUG.command -> { + Command.SHRUG.command -> { val message = textMessage.substring(Command.SHRUG.command.length).trim() ParsedCommand.SendShrug(message) } - Command.POLL.command -> { + Command.LENNY.command -> { + val message = textMessage.substring(Command.LENNY.command.length).trim() + + ParsedCommand.SendLenny(message) + } + Command.POLL.command -> { val rawCommand = textMessage.substring(Command.POLL.command.length).trim() val split = rawCommand.split("|").map { it.trim() } if (split.size > 2) { @@ -288,10 +328,23 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.POLL) } } - Command.DISCARD_SESSION.command -> { + Command.DISCARD_SESSION.command -> { ParsedCommand.DiscardSession } - else -> { + Command.WHOIS.command -> { + if (messageParts.size == 2) { + val userId = messageParts[1] + + if (MatrixPatterns.isUserId(userId)) { + ParsedCommand.ShowUser(userId) + } else { + ParsedCommand.ErrorSyntax(Command.WHOIS) + } + } else { + ParsedCommand.ErrorSyntax(Command.WHOIS) + } + } + else -> { // Unknown command ParsedCommand.ErrorUnknownSlashCommand(slashCommand) } diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index bdfa7779fb..54043f343a 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -41,7 +41,10 @@ sealed class ParsedCommand { class SendRainbowEmote(val message: CharSequence) : ParsedCommand() class BanUser(val userId: String, val reason: String?) : ParsedCommand() class UnbanUser(val userId: String, val reason: String?) : ParsedCommand() + class IgnoreUser(val userId: String) : ParsedCommand() + class UnignoreUser(val userId: String) : ParsedCommand() class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand() + class ChangeRoomName(val name: String) : ParsedCommand() class Invite(val userId: String, val reason: String?) : ParsedCommand() class Invite3Pid(val threePid: ThreePid) : ParsedCommand() class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand() @@ -53,6 +56,8 @@ sealed class ParsedCommand { object ClearScalarToken : ParsedCommand() class SendSpoiler(val message: String) : ParsedCommand() class SendShrug(val message: CharSequence) : ParsedCommand() + class SendLenny(val message: CharSequence) : ParsedCommand() class SendPoll(val question: String, val options: List) : ParsedCommand() - object DiscardSession: ParsedCommand() + object DiscardSession : ParsedCommand() + class ShowUser(val userId: String) : ParsedCommand() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 51aeda2aab..a9c2307566 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -359,6 +359,7 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode) RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager() + is RoomDetailViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId) is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it) is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog() is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 29ed43f17d..b747075622 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -64,14 +64,16 @@ sealed class RoomDetailViewEvents : VectorViewEvents { abstract class SendMessageResult : RoomDetailViewEvents() - object DisplayPromptForIntegrationManager: RoomDetailViewEvents() + object DisplayPromptForIntegrationManager : RoomDetailViewEvents() - object DisplayEnableIntegrationsWarning: RoomDetailViewEvents() + object DisplayEnableIntegrationsWarning : RoomDetailViewEvents() - data class OpenStickerPicker(val widget: Widget): RoomDetailViewEvents() + data class OpenRoomMemberProfile(val userId: String) : RoomDetailViewEvents() - object OpenIntegrationManager: RoomDetailViewEvents() - object OpenActiveWidgetBottomSheet: RoomDetailViewEvents() + data class OpenStickerPicker(val widget: Widget) : RoomDetailViewEvents() + + object OpenIntegrationManager : RoomDetailViewEvents() + object OpenActiveWidgetBottomSheet : RoomDetailViewEvents() data class RequestNativeWidgetPermission(val widget: Widget, val domain: String, val grantedEvents: RoomDetailViewEvents) : RoomDetailViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 1b5e928843..2e7109b5e7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -571,6 +571,10 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.MessageSent) popDraft() } + is ParsedCommand.ChangeRoomName -> { + handleChangeRoomNameSlashCommand(slashCommandResult) + popDraft() + } is ParsedCommand.Invite -> { handleInviteSlashCommand(slashCommandResult) popDraft() @@ -593,12 +597,20 @@ class RoomDetailViewModel @AssistedInject constructor( if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) popDraft() } + is ParsedCommand.BanUser -> { + handleBanSlashCommand(slashCommandResult) + popDraft() + } is ParsedCommand.UnbanUser -> { handleUnbanSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.BanUser -> { - handleBanSlashCommand(slashCommandResult) + is ParsedCommand.IgnoreUser -> { + handleIgnoreSlashCommand(slashCommandResult) + popDraft() + } + is ParsedCommand.UnignoreUser -> { + handleUnignoreSlashCommand(slashCommandResult) popDraft() } is ParsedCommand.KickUser -> { @@ -641,14 +653,12 @@ class RoomDetailViewModel @AssistedInject constructor( popDraft() } is ParsedCommand.SendShrug -> { - val sequence = buildString { - append("¯\\_(ツ)_/¯") - if (slashCommandResult.message.isNotEmpty()) { - append(" ") - append(slashCommandResult.message) - } - } - room.sendTextMessage(sequence) + sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message) + _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.SendLenny -> { + sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() } @@ -665,6 +675,11 @@ class RoomDetailViewModel @AssistedInject constructor( handleChangeDisplayNameSlashCommand(slashCommandResult) popDraft() } + is ParsedCommand.ShowUser -> { + _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) + handleWhoisSlashCommand(slashCommandResult) + popDraft() + } is ParsedCommand.DiscardSession -> { if (room.isEncrypted()) { session.cryptoService().discardOutboundSession(room.roomId) @@ -786,6 +801,12 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleChangeRoomNameSlashCommand(changeRoomName: ParsedCommand.ChangeRoomName) { + launchSlashCommandFlow { + room.updateName(changeRoomName.name, it) + } + } + private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { launchSlashCommandFlow { room.invite(invite.userId, invite.reason, it) @@ -833,6 +854,33 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleIgnoreSlashCommand(ignore: ParsedCommand.IgnoreUser) { + launchSlashCommandFlow { + session.ignoreUserIds(listOf(ignore.userId), it) + } + } + + private fun handleUnignoreSlashCommand(unignore: ParsedCommand.UnignoreUser) { + launchSlashCommandFlow { + session.unIgnoreUserIds(listOf(unignore.userId), it) + } + } + + private fun handleWhoisSlashCommand(whois: ParsedCommand.ShowUser) { + _viewEvents.post(RoomDetailViewEvents.OpenRoomMemberProfile(whois.userId)) + } + + private fun sendPrefixedMessage(prefix: String, message: CharSequence) { + val sequence = buildString { + append(prefix) + if (message.isNotEmpty()) { + append(" ") + append(message) + } + } + room.sendTextMessage(sequence) + } + private fun launchSlashCommandFlow(lambda: (MatrixCallback) -> Unit) { _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) val matrixCallback = object : MatrixCallback { diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index c025054f98..f87b45cd05 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1287,8 +1287,11 @@ Displays action Bans user with given id Unbans user with given id + Ignores a user, hiding their messages from you + Stops ignoring a user, showing their messages going forward Define the power level of a user Deops user with given id + Sets the room name Invites user with given id to current room Joins room with given alias Leave room @@ -1297,6 +1300,7 @@ Changes your display nickname On/Off markdown To fix Matrix Apps management + Displays information about a user Markdown has been enabled. Markdown has been disabled. @@ -2063,6 +2067,7 @@ Element may crash more often when an unexpected error occurs Prepends ¯\\_(ツ)_/¯ to a plain-text message + Prepends ( ͡° ͜ʖ ͡°) to a plain-text message "Enable encryption" "Once enabled, encryption cannot be disabled." From 13960561c00d6a44d8e44d7f8586be66075aeeb5 Mon Sep 17 00:00:00 2001 From: Constantin Wartenburger Date: Sat, 10 Oct 2020 18:34:31 +0200 Subject: [PATCH 02/90] Added /myroomnick command --- .../im/vector/app/features/command/Command.kt | 1 + .../app/features/command/CommandParser.kt | 23 +++++++++++++------ .../app/features/command/ParsedCommand.kt | 1 + .../home/room/detail/RoomDetailViewModel.kt | 15 ++++++++++++ vector/src/main/res/values/strings.xml | 1 + 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index c1b30b2744..1db1639b1d 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -39,6 +39,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d TOPIC("/topic", "", R.string.command_description_topic), KICK_USER("/kick", " [reason]", R.string.command_description_kick_user), CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick), + CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "", R.string.command_description_room_nick), MARKDOWN("/markdown", "", R.string.command_description_markdown), RAINBOW("/rainbow", "", R.string.command_description_rainbow), RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote), diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index fd7d587c1c..e09b6a842d 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -60,7 +60,7 @@ object CommandParser { } return when (val slashCommand = messageParts.first()) { - Command.PLAIN.command -> { + Command.PLAIN.command -> { val text = textMessage.substring(Command.PLAIN.command.length).trim() if (text.isNotEmpty()) { @@ -69,7 +69,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.PLAIN) } } - Command.CHANGE_DISPLAY_NAME.command -> { + Command.CHANGE_DISPLAY_NAME.command -> { val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim() if (newDisplayName.isNotEmpty()) { @@ -78,7 +78,16 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME) } } - Command.TOPIC.command -> { + Command.CHANGE_DISPLAY_NAME_FOR_ROOM.command -> { + val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME_FOR_ROOM.command.length).trim() + + if (newDisplayName.isNotEmpty()) { + ParsedCommand.ChangeDisplayNameForRoom(newDisplayName) + } else { + ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME_FOR_ROOM) + } + } + Command.TOPIC.command -> { val newTopic = textMessage.substring(Command.TOPIC.command.length).trim() if (newTopic.isNotEmpty()) { @@ -87,22 +96,22 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.TOPIC) } } - Command.EMOTE.command -> { + Command.EMOTE.command -> { val message = textMessage.subSequence(Command.EMOTE.command.length, textMessage.length).trim() ParsedCommand.SendEmote(message) } - Command.RAINBOW.command -> { + Command.RAINBOW.command -> { val message = textMessage.subSequence(Command.RAINBOW.command.length, textMessage.length).trim() ParsedCommand.SendRainbow(message) } - Command.RAINBOW_EMOTE.command -> { + Command.RAINBOW_EMOTE.command -> { val message = textMessage.subSequence(Command.RAINBOW_EMOTE.command.length, textMessage.length).trim() ParsedCommand.SendRainbowEmote(message) } - Command.JOIN_ROOM.command -> { + Command.JOIN_ROOM.command -> { if (messageParts.size >= 2) { val roomAlias = messageParts[1] diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index 54043f343a..60b4e1c3a2 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -52,6 +52,7 @@ sealed class ParsedCommand { class ChangeTopic(val topic: String) : ParsedCommand() class KickUser(val userId: String, val reason: String?) : ParsedCommand() class ChangeDisplayName(val displayName: String) : ParsedCommand() + class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand() class SetMarkdown(val enable: Boolean) : ParsedCommand() object ClearScalarToken : ParsedCommand() class SendSpoiler(val message: String) : ParsedCommand() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 2e7109b5e7..10942f17bf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -74,6 +74,7 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageContent @@ -675,6 +676,10 @@ class RoomDetailViewModel @AssistedInject constructor( handleChangeDisplayNameSlashCommand(slashCommandResult) popDraft() } + is ParsedCommand.ChangeDisplayNameForRoom -> { + handleChangeDisplayNameForRoomSlashCommand(slashCommandResult) + popDraft() + } is ParsedCommand.ShowUser -> { _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) handleWhoisSlashCommand(slashCommandResult) @@ -836,6 +841,16 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { + val content = room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId)) + ?.content?.toModel() + ?: RoomMemberContent(membership = Membership.JOIN) + + launchSlashCommandFlow { + room.sendStateEvent(EventType.STATE_ROOM_MEMBER, session.myUserId, content.copy(displayName = changeDisplayName.displayName).toContent(), it) + } + } + private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) { launchSlashCommandFlow { room.kick(kick.userId, kick.reason, it) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index f87b45cd05..33c648d647 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1298,6 +1298,7 @@ Set the room topic Kicks user with given id Changes your display nickname + Changes your display nickname On/Off markdown To fix Matrix Apps management Displays information about a user From 1a40b65b53677b79b3d6beb34335fdd9d3dab80a Mon Sep 17 00:00:00 2001 From: Constantin Wartenburger Date: Sun, 11 Oct 2020 18:56:13 +0200 Subject: [PATCH 03/90] Added /myroomavatar command (without upload) --- .../im/vector/app/features/command/Command.kt | 3 +- .../app/features/command/CommandParser.kt | 51 ++++++++++++------- .../app/features/command/ParsedCommand.kt | 1 + .../home/room/detail/RoomDetailViewModel.kt | 18 +++++-- vector/src/main/res/values/strings.xml | 3 +- 5 files changed, 52 insertions(+), 24 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 1db1639b1d..fd0623dc05 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -39,7 +39,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d TOPIC("/topic", "", R.string.command_description_topic), KICK_USER("/kick", " [reason]", R.string.command_description_kick_user), CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick), - CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "", R.string.command_description_room_nick), + CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "", R.string.command_description_nick_for_room), + CHANGE_AVATAR_FOR_ROOM("/myroomavatar", "", R.string.command_description_avatar_for_room), MARKDOWN("/markdown", "", R.string.command_description_markdown), RAINBOW("/rainbow", "", R.string.command_description_rainbow), RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote), diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index e09b6a842d..d5fb9a41b6 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -87,6 +87,19 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME_FOR_ROOM) } } + Command.CHANGE_AVATAR_FOR_ROOM.command -> { + if (messageParts.size == 2) { + val url = messageParts[1] + + if (url.isNotEmpty()) { + ParsedCommand.ChangeAvatarForRoom(url) + } else { + ParsedCommand.ErrorSyntax(Command.CHANGE_AVATAR_FOR_ROOM) + } + } else { + ParsedCommand.ErrorSyntax(Command.CHANGE_AVATAR_FOR_ROOM) + } + } Command.TOPIC.command -> { val newTopic = textMessage.substring(Command.TOPIC.command.length).trim() @@ -129,7 +142,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.JOIN_ROOM) } } - Command.PART.command -> { + Command.PART.command -> { if (messageParts.size >= 2) { val roomAlias = messageParts[1] @@ -147,7 +160,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.PART) } } - Command.ROOM_NAME.command -> { + Command.ROOM_NAME.command -> { val newRoomName = textMessage.substring(Command.ROOM_NAME.command.length).trim() if (newRoomName.isNotEmpty()) { @@ -156,7 +169,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.ROOM_NAME) } } - Command.INVITE.command -> { + Command.INVITE.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -183,7 +196,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.INVITE) } } - Command.KICK_USER.command -> { + Command.KICK_USER.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -201,7 +214,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.KICK_USER) } } - Command.BAN_USER.command -> { + Command.BAN_USER.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -219,7 +232,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.BAN_USER) } } - Command.UNBAN_USER.command -> { + Command.UNBAN_USER.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -237,7 +250,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.UNBAN_USER) } } - Command.IGNORE_USER.command -> { + Command.IGNORE_USER.command -> { if (messageParts.size == 2) { val userId = messageParts[1] @@ -250,7 +263,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.IGNORE_USER) } } - Command.UNIGNORE_USER.command -> { + Command.UNIGNORE_USER.command -> { if (messageParts.size == 2) { val userId = messageParts[1] @@ -263,7 +276,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.UNIGNORE_USER) } } - Command.SET_USER_POWER_LEVEL.command -> { + Command.SET_USER_POWER_LEVEL.command -> { if (messageParts.size == 3) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { @@ -283,7 +296,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) } } - Command.RESET_USER_POWER_LEVEL.command -> { + Command.RESET_USER_POWER_LEVEL.command -> { if (messageParts.size == 2) { val userId = messageParts[1] @@ -296,7 +309,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) } } - Command.MARKDOWN.command -> { + Command.MARKDOWN.command -> { if (messageParts.size == 2) { when { "on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true) @@ -307,28 +320,28 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.MARKDOWN) } } - Command.CLEAR_SCALAR_TOKEN.command -> { + Command.CLEAR_SCALAR_TOKEN.command -> { if (messageParts.size == 1) { ParsedCommand.ClearScalarToken } else { ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN) } } - Command.SPOILER.command -> { + Command.SPOILER.command -> { val message = textMessage.substring(Command.SPOILER.command.length).trim() ParsedCommand.SendSpoiler(message) } - Command.SHRUG.command -> { + Command.SHRUG.command -> { val message = textMessage.substring(Command.SHRUG.command.length).trim() ParsedCommand.SendShrug(message) } - Command.LENNY.command -> { + Command.LENNY.command -> { val message = textMessage.substring(Command.LENNY.command.length).trim() ParsedCommand.SendLenny(message) } - Command.POLL.command -> { + Command.POLL.command -> { val rawCommand = textMessage.substring(Command.POLL.command.length).trim() val split = rawCommand.split("|").map { it.trim() } if (split.size > 2) { @@ -337,10 +350,10 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.POLL) } } - Command.DISCARD_SESSION.command -> { + Command.DISCARD_SESSION.command -> { ParsedCommand.DiscardSession } - Command.WHOIS.command -> { + Command.WHOIS.command -> { if (messageParts.size == 2) { val userId = messageParts[1] @@ -353,7 +366,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.WHOIS) } } - else -> { + else -> { // Unknown command ParsedCommand.ErrorUnknownSlashCommand(slashCommand) } diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index 60b4e1c3a2..f0dcbc9663 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -53,6 +53,7 @@ sealed class ParsedCommand { class KickUser(val userId: String, val reason: String?) : ParsedCommand() class ChangeDisplayName(val displayName: String) : ParsedCommand() class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand() + class ChangeAvatarForRoom(val url: String) : ParsedCommand() class SetMarkdown(val enable: Boolean) : ParsedCommand() object ClearScalarToken : ParsedCommand() class SendSpoiler(val message: String) : ParsedCommand() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 10942f17bf..e84eb5520c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -680,6 +680,10 @@ class RoomDetailViewModel @AssistedInject constructor( handleChangeDisplayNameForRoomSlashCommand(slashCommandResult) popDraft() } + is ParsedCommand.ChangeAvatarForRoom -> { + handleChangeAvatarForRoomSlashCommand(slashCommandResult) + popDraft() + } is ParsedCommand.ShowUser -> { _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) handleWhoisSlashCommand(slashCommandResult) @@ -841,13 +845,21 @@ class RoomDetailViewModel @AssistedInject constructor( } } - private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { - val content = room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId)) + private fun getLastMemberEvent(): RoomMemberContent { + return room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId)) ?.content?.toModel() ?: RoomMemberContent(membership = Membership.JOIN) + } + private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { launchSlashCommandFlow { - room.sendStateEvent(EventType.STATE_ROOM_MEMBER, session.myUserId, content.copy(displayName = changeDisplayName.displayName).toContent(), it) + room.sendStateEvent(EventType.STATE_ROOM_MEMBER, session.myUserId, getLastMemberEvent().copy(displayName = changeDisplayName.displayName).toContent(), it) + } + } + + private fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) { + launchSlashCommandFlow { + room.sendStateEvent(EventType.STATE_ROOM_MEMBER, session.myUserId, getLastMemberEvent().copy(avatarUrl = changeAvatar.url).toContent(), it) } } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 33c648d647..823d567f20 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1298,7 +1298,8 @@ Set the room topic Kicks user with given id Changes your display nickname - Changes your display nickname + Changes your display nickname in the current room only + Changes your avatar in this current room only On/Off markdown To fix Matrix Apps management Displays information about a user From 24c67660c12d8ac402e50dee16a2c2f6ea94dae1 Mon Sep 17 00:00:00 2001 From: Constantin Wartenburger Date: Mon, 12 Oct 2020 17:43:07 +0200 Subject: [PATCH 04/90] Added /roomavatar command (not upload) --- .../im/vector/app/features/command/Command.kt | 3 ++- .../vector/app/features/command/CommandParser.kt | 15 ++++++++++++++- .../vector/app/features/command/ParsedCommand.kt | 1 + .../home/room/detail/RoomDetailViewModel.kt | 11 +++++++++++ vector/src/main/res/values/strings.xml | 1 + 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index fd0623dc05..b74b608e32 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -32,7 +32,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d UNIGNORE_USER("/unignore", "", R.string.command_description_unignore_user), SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user), RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user), - ROOM_NAME("/roomname", " [reason]", R.string.command_description_room_name), + ROOM_NAME("/roomname", "", R.string.command_description_room_name), INVITE("/invite", " [reason]", R.string.command_description_invite_user), JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room), PART("/part", " [reason]", R.string.command_description_part_room), @@ -40,6 +40,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d KICK_USER("/kick", " [reason]", R.string.command_description_kick_user), CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick), CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "", R.string.command_description_nick_for_room), + ROOM_AVATAR("/roomavatar", "", R.string.command_description_room_avatar), CHANGE_AVATAR_FOR_ROOM("/myroomavatar", "", R.string.command_description_avatar_for_room), MARKDOWN("/markdown", "", R.string.command_description_markdown), RAINBOW("/rainbow", "", R.string.command_description_rainbow), diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index d5fb9a41b6..41961c209a 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -87,11 +87,24 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME_FOR_ROOM) } } + Command.ROOM_AVATAR.command -> { + if (messageParts.size == 2) { + val url = messageParts[1] + + if (url.isNotEmpty() && url.startsWith("mxc://")) { + ParsedCommand.ChangeRoomAvatar(url) + } else { + ParsedCommand.ErrorSyntax(Command.ROOM_AVATAR) + } + } else { + ParsedCommand.ErrorSyntax(Command.ROOM_AVATAR) + } + } Command.CHANGE_AVATAR_FOR_ROOM.command -> { if (messageParts.size == 2) { val url = messageParts[1] - if (url.isNotEmpty()) { + if (url.isNotEmpty() && url.startsWith("mxc://")) { ParsedCommand.ChangeAvatarForRoom(url) } else { ParsedCommand.ErrorSyntax(Command.CHANGE_AVATAR_FOR_ROOM) diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index f0dcbc9663..16f2eaac29 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -53,6 +53,7 @@ sealed class ParsedCommand { class KickUser(val userId: String, val reason: String?) : ParsedCommand() class ChangeDisplayName(val displayName: String) : ParsedCommand() class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand() + class ChangeRoomAvatar(val url: String) : ParsedCommand() class ChangeAvatarForRoom(val url: String) : ParsedCommand() class SetMarkdown(val enable: Boolean) : ParsedCommand() object ClearScalarToken : ParsedCommand() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index e84eb5520c..737cdf61b0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -74,6 +74,7 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -680,6 +681,10 @@ class RoomDetailViewModel @AssistedInject constructor( handleChangeDisplayNameForRoomSlashCommand(slashCommandResult) popDraft() } + is ParsedCommand.ChangeRoomAvatar -> { + handleChangeRoomAvatarSlashCommand(slashCommandResult) + popDraft() + } is ParsedCommand.ChangeAvatarForRoom -> { handleChangeAvatarForRoomSlashCommand(slashCommandResult) popDraft() @@ -857,6 +862,12 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) { + launchSlashCommandFlow { + room.sendStateEvent(EventType.STATE_ROOM_AVATAR, null, RoomAvatarContent(changeAvatar.url).toContent(), it) + } + } + private fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) { launchSlashCommandFlow { room.sendStateEvent(EventType.STATE_ROOM_MEMBER, session.myUserId, getLastMemberEvent().copy(avatarUrl = changeAvatar.url).toContent(), it) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 823d567f20..909ef01b4c 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1299,6 +1299,7 @@ Kicks user with given id Changes your display nickname Changes your display nickname in the current room only + Changes the avatar of the current room Changes your avatar in this current room only On/Off markdown To fix Matrix Apps management From 5b6727408b5498aa553f7781ecb63d2b2863066b Mon Sep 17 00:00:00 2001 From: Constantin Wartenburger Date: Tue, 13 Oct 2020 15:10:57 +0200 Subject: [PATCH 05/90] Fix wrong parameter name --- .../src/main/java/im/vector/app/features/command/Command.kt | 2 +- .../main/java/im/vector/app/features/command/CommandParser.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index b74b608e32..81bbf8177d 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -32,7 +32,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d UNIGNORE_USER("/unignore", "", R.string.command_description_unignore_user), SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user), RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user), - ROOM_NAME("/roomname", "", R.string.command_description_room_name), + ROOM_NAME("/roomname", "", R.string.command_description_room_name), INVITE("/invite", " [reason]", R.string.command_description_invite_user), JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room), PART("/part", " [reason]", R.string.command_description_part_room), diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 41961c209a..29eba00490 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -91,7 +91,7 @@ object CommandParser { if (messageParts.size == 2) { val url = messageParts[1] - if (url.isNotEmpty() && url.startsWith("mxc://")) { + if (url.startsWith("mxc://")) { ParsedCommand.ChangeRoomAvatar(url) } else { ParsedCommand.ErrorSyntax(Command.ROOM_AVATAR) @@ -104,7 +104,7 @@ object CommandParser { if (messageParts.size == 2) { val url = messageParts[1] - if (url.isNotEmpty() && url.startsWith("mxc://")) { + if (url.startsWith("mxc://")) { ParsedCommand.ChangeAvatarForRoom(url) } else { ParsedCommand.ErrorSyntax(Command.CHANGE_AVATAR_FOR_ROOM) From 451c2379ec3b9ca4430eff872d48d2287871a931 Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Fri, 28 May 2021 15:16:04 +0200 Subject: [PATCH 06/90] Do not notify again for old events Resending the notification here can trigger other system components or apps that listen to new notifications, such as connected smart watches or automation tools. Fixes https://github.com/vector-im/element-android/issues/1673 --- changelog.d/1673.bugfix | 1 + .../app/features/notifications/NotificationDrawerManager.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/1673.bugfix diff --git a/changelog.d/1673.bugfix b/changelog.d/1673.bugfix new file mode 100644 index 0000000000..b0459f34b8 --- /dev/null +++ b/changelog.d/1673.bugfix @@ -0,0 +1 @@ +Avoid resending notifications that are already shown diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 37ed1e654a..fd15455391 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -457,7 +457,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context if (eventList.isEmpty() || eventList.all { it.isRedacted }) { notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) - } else { + } else if (hasNewEvent) { // FIXME roomIdToEventMap.size is not correct, this is the number of rooms val nbEvents = roomIdToEventMap.size + simpleEvents.size val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) From 8c590b50e3938abadc506bbdeb36ffccc32f4f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20V=C3=A1gner?= Date: Sun, 1 Aug 2021 14:51:00 +0200 Subject: [PATCH 07/90] Improve accessibility of voice messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Peter Vágner --- .../home/room/detail/composer/VoiceMessageRecorderView.kt | 5 +++++ .../home/room/detail/timeline/item/MessageVoiceItem.kt | 3 +++ .../src/main/res/layout/item_timeline_event_voice_stub.xml | 3 ++- vector/src/main/res/layout/view_voice_message_recorder.xml | 7 +++++-- vector/src/main/res/values/strings.xml | 6 +++--- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt index ba6e0fbae7..a90c1c4d3a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -20,6 +20,7 @@ import android.content.Context import android.text.format.DateUtils import android.util.AttributeSet import android.view.MotionEvent +import android.view.View import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isInvisible import androidx.core.view.isVisible @@ -476,12 +477,14 @@ class VoiceMessageRecorderView @JvmOverloads constructor( views.voiceMessagePlaybackTimerIndicator.isVisible = true views.voicePlaybackControlButton.isVisible = false views.voiceMessageSendButton.isVisible = true + views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES renderToast(context.getString(R.string.voice_message_tap_to_stop_toast)) } private fun showPlaybackViews() { views.voiceMessagePlaybackTimerIndicator.isVisible = false views.voicePlaybackControlButton.isVisible = true + views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO callback?.onVoiceRecordingPlaybackModeOn() } @@ -507,12 +510,14 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } is VoiceMessagePlaybackTracker.Listener.State.Playing -> { views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) + views.voicePlaybackControlButton.contentDescription = resources.getString(R.string.a11y_pause_voice_message) val formattedTimerText = DateUtils.formatElapsedTime((state.playbackTime / 1000).toLong()) views.voicePlaybackTime.text = formattedTimerText } is VoiceMessagePlaybackTracker.Listener.State.Paused, is VoiceMessagePlaybackTracker.Listener.State.Idle -> { views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) + views.voicePlaybackControlButton.contentDescription = resources.getString(R.string.a11y_play_voice_message) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt index dc204da291..fce2db2bfd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -98,16 +98,19 @@ abstract class MessageVoiceItem : AbsMessageItem() { private fun renderIdleState(holder: Holder) { holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) + holder.voicePlaybackControlButton.contentDescription = holder.view.context.resources.getString(R.string.a11y_play_voice_message) holder.voicePlaybackTime.text = formatPlaybackTime(duration) } private fun renderPlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing) { holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) + holder.voicePlaybackControlButton.contentDescription = holder.view.context.resources.getString(R.string.a11y_pause_voice_message) holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) } private fun renderPausedState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Paused) { holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) + holder.voicePlaybackControlButton.contentDescription = holder.view.context.resources.getString(R.string.a11y_play_voice_message) holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) } diff --git a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml index 21705566e9..2c8ade173a 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml @@ -53,6 +53,7 @@ android:layout_height="0dp" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" + android:importantForAccessibility="no" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/voicePlaybackTime" @@ -77,4 +78,4 @@ app:layout_constraintTop_toBottomOf="@+id/voicePlaybackLayout" tools:visibility="visible" /> - \ No newline at end of file + diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index d309761815..051928b73d 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -107,7 +107,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="28dp" - android:contentDescription="@string/a11y_lock_voice_message" + android:importantForAccessibility="no" android:src="@drawable/ic_voice_message_unlocked" android:visibility="gone" app:layout_constraintEnd_toEndOf="@id/voiceMessageMicButton" @@ -215,6 +215,8 @@ android:layout_height="0dp" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" + android:contentDescription="@string/a11y_stop_voice_message" + android:importantForAccessibility="no" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/voicePlaybackTime" @@ -231,10 +233,11 @@ android:layout_height="wrap_content" android:layout_marginBottom="84dp" android:visibility="gone" + android:accessibilityLiveRegion="polite" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" tools:text="@string/voice_message_release_to_send_toast" tools:visibility="visible" /> - \ No newline at end of file + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 8e6bf85eae..468c51bac8 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3460,13 +3460,13 @@ Sorry, an error occurred while trying to join: %s - Start Voice Message + Record Voice Message Slide to cancel - Voice Message Lock Play Voice Message Pause Voice Message + Stop Recording Recording voice message - Delete recorded voice message + Delete recording Hold to record, release to send %1$ds left Tap on your recording to stop or listen From ebe1e28689604b7bf25f04dc20c2ec9cbdf7805c Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Aug 2021 16:46:13 +0200 Subject: [PATCH 08/90] Sync: makes SyncResponse in public API --- .../session/sync/model/DeviceListResponse.kt | 5 +++-- .../sync/model/DeviceOneTimeKeysCountSyncResponse.kt | 4 ++-- .../session/sync/model/GroupSyncProfile.kt | 4 ++-- .../session/sync/model/GroupsSyncResponse.kt | 4 ++-- .../session/sync/model/InvitedGroupSync.kt | 4 ++-- .../session/sync/model/InvitedRoomSync.kt | 5 +++-- .../session/sync/model/LazyRoomSyncEphemeral.kt | 8 ++++---- .../session/sync/model/PresenceSyncResponse.kt | 4 ++-- .../session/sync/model/RoomInviteState.kt | 5 +++-- .../sdk/{internal => api}/session/sync/model/RoomSync.kt | 5 +++-- .../session/sync/model/RoomSyncAccountData.kt | 4 ++-- .../session/sync/model/RoomSyncEphemeral.kt | 4 ++-- .../{internal => api}/session/sync/model/RoomSyncState.kt | 4 ++-- .../session/sync/model/RoomSyncSummary.kt | 4 ++-- .../session/sync/model/RoomSyncTimeline.kt | 4 ++-- .../session/sync/model/RoomSyncUnreadNotifications.kt | 4 ++-- .../session/sync/model/RoomsSyncResponse.kt | 5 +++-- .../{internal => api}/session/sync/model/SyncResponse.kt | 4 ++-- .../session/sync/model/ToDeviceSyncResponse.kt | 4 ++-- .../android/sdk/internal/crypto/DefaultCryptoService.kt | 2 +- .../session/notification/ProcessEventForPushTask.kt | 2 +- .../internal/session/room/summary/RoomSummaryUpdater.kt | 4 ++-- .../sdk/internal/session/sync/CryptoSyncHandler.kt | 4 ++-- .../android/sdk/internal/session/sync/GroupSyncHandler.kt | 4 ++-- .../session/sync/RoomSyncEphemeralTemporaryStore.kt | 2 +- .../android/sdk/internal/session/sync/RoomSyncHandler.kt | 8 ++++---- .../matrix/android/sdk/internal/session/sync/SyncAPI.kt | 2 +- .../sdk/internal/session/sync/SyncResponseHandler.kt | 6 +++--- .../matrix/android/sdk/internal/session/sync/SyncTask.kt | 3 ++- .../internal/session/sync/UserAccountDataSyncHandler.kt | 2 +- .../session/sync/model/accountdata/UserAccountDataSync.kt | 2 +- .../parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt | 4 ++-- .../session/sync/parsing/InitialSyncResponseParser.kt | 2 +- .../session/sync/parsing/RoomSyncAccountDataHandler.kt | 2 +- 34 files changed, 70 insertions(+), 64 deletions(-) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/DeviceListResponse.kt (90%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt (87%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/GroupSyncProfile.kt (91%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/GroupsSyncResponse.kt (92%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/InvitedGroupSync.kt (90%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/InvitedRoomSync.kt (93%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/LazyRoomSyncEphemeral.kt (77%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/PresenceSyncResponse.kt (90%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/RoomInviteState.kt (91%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/RoomSync.kt (95%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/RoomSyncAccountData.kt (90%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/RoomSyncEphemeral.kt (91%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/RoomSyncState.kt (91%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/RoomSyncSummary.kt (95%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/RoomSyncTimeline.kt (93%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/RoomSyncUnreadNotifications.kt (92%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/RoomsSyncResponse.kt (93%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/SyncResponse.kt (95%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal => api}/session/sync/model/ToDeviceSyncResponse.kt (90%) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceListResponse.kt similarity index 90% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceListResponse.kt index bfa8c342b6..c05e1e5187 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceListResponse.kt @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model + +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.JsonClass @@ -21,7 +22,7 @@ import com.squareup.moshi.JsonClass * This class describes the device list response from a sync request */ @JsonClass(generateAdapter = true) -internal data class DeviceListResponse( +data class DeviceListResponse( // user ids list which have new crypto devices val changed: List = emptyList(), // List of user ids who are no more tracked. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt similarity index 87% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt index d5b435ac27..930cfb153f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class DeviceOneTimeKeysCountSyncResponse( +data class DeviceOneTimeKeysCountSyncResponse( @Json(name = "signed_curve25519") val signedCurve25519: Int? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupSyncProfile.kt similarity index 91% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupSyncProfile.kt index ee6aabb0a9..581e6824ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupSyncProfile.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class GroupSyncProfile( +data class GroupSyncProfile( /** * The name of the group, if any. May be nil. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupsSyncResponse.kt similarity index 92% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupsSyncResponse.kt index 4c2dce3ba8..fd8710bbda 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupsSyncResponse.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class GroupsSyncResponse( +data class GroupsSyncResponse( /** * Joined groups: An array of groups ids. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedGroupSync.kt similarity index 90% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedGroupSync.kt index 148c2aeab9..d41df9f0f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedGroupSync.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class InvitedGroupSync( +data class InvitedGroupSync( /** * The identifier of the inviter. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedRoomSync.kt similarity index 93% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedRoomSync.kt index c21a73abc2..dc63c5ba07 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedRoomSync.kt @@ -13,14 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model + +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass // InvitedRoomSync represents a room invitation during server sync v2. @JsonClass(generateAdapter = true) -internal data class InvitedRoomSync( +data class InvitedRoomSync( /** * The state of a room that the user has been invited to. These state events may only have the 'sender', 'type', 'state_key' diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/LazyRoomSyncEphemeral.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/LazyRoomSyncEphemeral.kt similarity index 77% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/LazyRoomSyncEphemeral.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/LazyRoomSyncEphemeral.kt index 83006c646b..087a5f52dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/LazyRoomSyncEphemeral.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/LazyRoomSyncEphemeral.kt @@ -1,11 +1,11 @@ /* - * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * Copyright 2020 The Matrix.org Foundation C.I.C. * * 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 + * 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, @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) -internal sealed class LazyRoomSyncEphemeral { +sealed class LazyRoomSyncEphemeral { data class Parsed(val _roomSyncEphemeral: RoomSyncEphemeral) : LazyRoomSyncEphemeral() object Stored : LazyRoomSyncEphemeral() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/PresenceSyncResponse.kt similarity index 90% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/PresenceSyncResponse.kt index 92d09aa4f5..d632552888 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/PresenceSyncResponse.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Event // PresenceSyncResponse represents the updates to the presence status of other users during server sync v2. @JsonClass(generateAdapter = true) -internal data class PresenceSyncResponse( +data class PresenceSyncResponse( /** * List of presence events (array of Event with type m.presence). diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomInviteState.kt similarity index 91% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomInviteState.kt index ded9e2a350..59b4b4fc32 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomInviteState.kt @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model + +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -21,7 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event // RoomInviteState represents the state of a room that the user has been invited to. @JsonClass(generateAdapter = true) -internal data class RoomInviteState( +data class RoomInviteState( /** * List of state events (array of MXEvent). diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSync.kt similarity index 95% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSync.kt index 9aed0d37d6..e3d07602c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSync.kt @@ -13,14 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model + +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass // RoomSync represents the response for a room during server sync v2. @JsonClass(generateAdapter = true) -internal data class RoomSync( +data class RoomSync( /** * The state updates for the room. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncAccountData.kt similarity index 90% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncAccountData.kt index a2375507d8..f2c4ed551c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncAccountData.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Event @JsonClass(generateAdapter = true) -internal data class RoomSyncAccountData( +data class RoomSyncAccountData( /** * List of account data events (array of Event). */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncEphemeral.kt similarity index 91% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncEphemeral.kt index f2135db6b7..f4d831c16f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncEphemeral.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event // RoomSyncEphemeral represents the ephemeral events in the room that aren't recorded in the timeline or state of the room (e.g. typing). @JsonClass(generateAdapter = true) -internal data class RoomSyncEphemeral( +data class RoomSyncEphemeral( /** * List of ephemeral events (array of Event). */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncState.kt similarity index 91% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncState.kt index f86f05d000..7822467564 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event // RoomSyncState represents the state updates for a room during server sync v2. @JsonClass(generateAdapter = true) -internal data class RoomSyncState( +data class RoomSyncState( /** * List of state events (array of Event). The resulting state corresponds to the *start* of the timeline. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncSummary.kt similarity index 95% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncSummary.kt index 228a71ec28..7216a0c992 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncSummary.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class RoomSyncSummary( +data class RoomSyncSummary( /** * Present only if the room has no m.room.name or m.room.canonical_alias. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncTimeline.kt similarity index 93% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncTimeline.kt index 27bbc4343f..82d29a52e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncTimeline.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event // RoomSyncTimeline represents the timeline of messages and state changes for a room during server sync v2. @JsonClass(generateAdapter = true) -internal data class RoomSyncTimeline( +data class RoomSyncTimeline( /** * List of events (array of Event). diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncUnreadNotifications.kt similarity index 92% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncUnreadNotifications.kt index f01534b884..6618bceacd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncUnreadNotifications.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -24,7 +24,7 @@ import org.matrix.android.sdk.api.session.events.model.Event * `MXRoomSyncUnreadNotifications` represents the unread counts for a room. */ @JsonClass(generateAdapter = true) -internal data class RoomSyncUnreadNotifications( +data class RoomSyncUnreadNotifications( /** * List of account data events (array of Event). */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomsSyncResponse.kt similarity index 93% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomsSyncResponse.kt index dd2f96c988..ff3ed54264 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomsSyncResponse.kt @@ -13,14 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model + +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass // RoomsSyncResponse represents the rooms list in server sync v2 response. @JsonClass(generateAdapter = true) -internal data class RoomsSyncResponse( +data class RoomsSyncResponse( /** * Joined rooms: keys are rooms ids. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt similarity index 95% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt index f2b2fb7e8f..e9863e1cd1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.session.sync.model.accountdata.UserAccoun // SyncResponse represents the request response for server sync v2. @JsonClass(generateAdapter = true) -internal data class SyncResponse( +data class SyncResponse( /** * The user private data. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/ToDeviceSyncResponse.kt similarity index 90% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/ToDeviceSyncResponse.kt index 8f3af56cde..082460cc2d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/ToDeviceSyncResponse.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Event // ToDeviceSyncResponse represents the data directly sent to one of user's devices. @JsonClass(generateAdapter = true) -internal data class ToDeviceSyncResponse( +data class ToDeviceSyncResponse( /** * List of direct-to-device events. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 563c890950..c28ccda00a 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -87,7 +87,7 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask -import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.configureWith diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt index 0ece07fc15..be89554f2f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.notification import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.internal.task.Task import timber.log.Timber import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 842c9d3aba..c626e472e6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -61,8 +61,8 @@ import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataD import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.relationship.RoomChildRelationInfo -import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary -import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications +import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary +import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications import timber.log.Timber import javax.inject.Inject import kotlin.system.measureTimeMillis diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt index 411a9c5c06..a81a7d681e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt @@ -27,8 +27,8 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService import org.matrix.android.sdk.internal.session.initsync.ProgressReporter -import org.matrix.android.sdk.internal.session.sync.model.SyncResponse -import org.matrix.android.sdk.internal.session.sync.model.ToDeviceSyncResponse +import org.matrix.android.sdk.api.session.sync.model.SyncResponse +import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse import timber.log.Timber import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt index 02362bf050..701f6314ab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt @@ -25,8 +25,8 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.mapWithProgress -import org.matrix.android.sdk.internal.session.sync.model.GroupsSyncResponse -import org.matrix.android.sdk.internal.session.sync.model.InvitedGroupSync +import org.matrix.android.sdk.api.session.sync.model.GroupsSyncResponse +import org.matrix.android.sdk.api.session.sync.model.InvitedGroupSync import javax.inject.Inject internal class GroupSyncHandler @Inject constructor() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt index c6ff71cfcf..038b92d729 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt @@ -21,7 +21,7 @@ import com.squareup.moshi.Moshi import okio.buffer import okio.source import org.matrix.android.sdk.internal.di.SessionFilesDirectory -import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral +import org.matrix.android.sdk.api.session.sync.model.RoomSyncEphemeral import org.matrix.android.sdk.internal.util.md5 import timber.log.Timber import java.io.File diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index c3586bcea7..487ccbbfc3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -58,10 +58,10 @@ import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent -import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync -import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral -import org.matrix.android.sdk.internal.session.sync.model.RoomSync -import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync +import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral +import org.matrix.android.sdk.api.session.sync.model.RoomSync +import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.internal.session.sync.parsing.RoomSyncAccountDataHandler import org.matrix.android.sdk.internal.util.computeBestChunkSize import timber.log.Timber diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt index 2616803463..86ecdf8b56 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.sync import okhttp3.ResponseBody import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.network.TimeOutInterceptor -import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Header diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index a4468a96c9..cf50f89f54 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -30,9 +30,9 @@ import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.reportSubtask import org.matrix.android.sdk.internal.session.notification.ProcessEventForPushTask -import org.matrix.android.sdk.internal.session.sync.model.GroupsSyncResponse -import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse -import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.api.session.sync.model.GroupsSyncResponse +import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index c80fbe60c1..f033fe31d7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -28,7 +28,8 @@ import org.matrix.android.sdk.internal.session.filter.FilterRepository import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask import org.matrix.android.sdk.internal.session.initsync.DefaultInitialSyncProgressService import org.matrix.android.sdk.internal.session.initsync.reportSubtask -import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral +import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.session.sync.parsing.InitialSyncResponseParser import org.matrix.android.sdk.internal.session.user.UserStore import org.matrix.android.sdk.internal.task.Task diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt index b8d987d500..d4be345b75 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt @@ -48,7 +48,7 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper -import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync +import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent import org.matrix.android.sdk.internal.session.sync.model.accountdata.DirectMessagesContent import org.matrix.android.sdk.internal.session.sync.model.accountdata.IgnoredUsersContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt index 05b50ab2c5..ffa9db06ff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt @@ -21,6 +21,6 @@ import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent @JsonClass(generateAdapter = true) -internal data class UserAccountDataSync( +data class UserAccountDataSync( @Json(name = "events") val list: List = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt index 940ea219fb..62c71d9e21 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt @@ -23,8 +23,8 @@ import com.squareup.moshi.JsonWriter import com.squareup.moshi.ToJson import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore -import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral -import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral +import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral +import org.matrix.android.sdk.api.session.sync.model.RoomSyncEphemeral import timber.log.Timber internal class DefaultLazyRoomSyncEphemeralJsonAdapter { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt index 0b44887aed..331d4cc3fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt @@ -21,7 +21,7 @@ import okio.buffer import okio.source import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore -import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import timber.log.Timber import java.io.File import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt index 8bf9ad5b90..60bc68facc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt @@ -29,7 +29,7 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.session.room.read.FullyReadContent import org.matrix.android.sdk.internal.session.sync.RoomFullyReadHandler import org.matrix.android.sdk.internal.session.sync.RoomTagHandler -import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData +import org.matrix.android.sdk.api.session.sync.model.RoomSyncAccountData import javax.inject.Inject internal class RoomSyncAccountDataHandler @Inject constructor(private val roomTagHandler: RoomTagHandler, From a968a848b0cf7ac9c0d740f9a18115c937125bfa Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Aug 2021 16:46:37 +0200 Subject: [PATCH 09/90] Sync: exposes ShareFlow from the SyncThread --- .../matrix/android/sdk/api/session/Session.kt | 7 +++++++ .../sdk/internal/session/DefaultSession.kt | 2 ++ .../sdk/internal/session/sync/SyncTask.kt | 21 ++++++++++++------- .../internal/session/sync/job/SyncThread.kt | 10 ++++++++- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 2f981ffbbe..e0b48f1e07 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session import androidx.annotation.MainThread import androidx.lifecycle.LiveData +import kotlinx.coroutines.flow.SharedFlow import okhttp3.OkHttpClient import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.failure.GlobalError @@ -57,6 +58,7 @@ import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService import org.matrix.android.sdk.api.session.typing.TypingUsersTracker import org.matrix.android.sdk.api.session.user.UserService import org.matrix.android.sdk.api.session.widgets.WidgetService +import org.matrix.android.sdk.api.session.sync.model.SyncResponse /** * This interface defines interactions with a session. @@ -143,6 +145,11 @@ interface Session : */ fun getSyncState(): SyncState + /** + * This method returns a flow of SyncResponse. New value will be pushed through the sync thread. + */ + fun syncFlow(): SharedFlow + /** * This methods return true if an initial sync has been processed */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index c2bd1e24ed..2975cc7ad0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -223,6 +223,8 @@ internal class DefaultSession @Inject constructor( override fun getSyncStateLive() = getSyncThread().liveState() + override fun syncFlow() = getSyncThread().syncFlow() + override fun getSyncState() = getSyncThread().currentState() override fun hasAlreadySynced(): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index f033fe31d7..5684c2b1d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -41,7 +41,7 @@ import java.io.File import java.net.SocketTimeoutException import javax.inject.Inject -internal interface SyncTask : Task { +internal interface SyncTask : Task { data class Params( val timeout: Long, @@ -69,13 +69,13 @@ internal class DefaultSyncTask @Inject constructor( private val workingDir = File(fileDirectory, "is") private val initialSyncStatusRepository: InitialSyncStatusRepository = FileInitialSyncStatusRepository(workingDir) - override suspend fun execute(params: SyncTask.Params) { - syncTaskSequencer.post { + override suspend fun execute(params: SyncTask.Params) : SyncResponse { + return syncTaskSequencer.post { doSync(params) } } - private suspend fun doSync(params: SyncTask.Params) { + private suspend fun doSync(params: SyncTask.Params): SyncResponse { Timber.v("Sync task started on Thread: ${Thread.currentThread().name}") val requestParams = HashMap() @@ -100,6 +100,7 @@ internal class DefaultSyncTask @Inject constructor( val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT) + var syncResponseToReturn: SyncResponse? = null if (isInitialSync) { Timber.d("INIT_SYNC with filter: ${requestParams["filter"]}") val initSyncStrategy = initialSyncStrategy @@ -108,7 +109,7 @@ internal class DefaultSyncTask @Inject constructor( roomSyncEphemeralTemporaryStore.reset() workingDir.mkdirs() val file = downloadInitSyncResponse(requestParams) - reportSubtask(initialSyncProgressService, InitSyncStep.ImportingAccount, 1, 0.7F) { + syncResponseToReturn = reportSubtask(initialSyncProgressService, InitSyncStep.ImportingAccount, 1, 0.7F) { handleSyncFile(file, initSyncStrategy) } // Delete all files @@ -122,10 +123,10 @@ internal class DefaultSyncTask @Inject constructor( ) } } - logDuration("INIT_SYNC Database insertion") { syncResponseHandler.handleResponse(syncResponse, token, initialSyncProgressService) } + syncResponseToReturn = syncResponse } } initialSyncProgressService.endAll() @@ -137,8 +138,11 @@ internal class DefaultSyncTask @Inject constructor( ) } syncResponseHandler.handleResponse(syncResponse, token, null) + syncResponseToReturn = syncResponse } Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}") + // Should throw if null as it's a mandatory value. + return syncResponseToReturn!! } private suspend fun downloadInitSyncResponse(requestParams: Map): File { @@ -195,8 +199,8 @@ internal class DefaultSyncTask @Inject constructor( } } - private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized) { - logDuration("INIT_SYNC handleSyncFile()") { + private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized): SyncResponse { + return logDuration("INIT_SYNC handleSyncFile()") { val syncResponse = logDuration("INIT_SYNC Read file and parse") { syncResponseParser.parse(initSyncStrategy, workingFile) } @@ -210,6 +214,7 @@ internal class DefaultSyncTask @Inject constructor( syncResponseHandler.handleResponse(syncResponse, null, initialSyncProgressService) } initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_SUCCESS) + syncResponse } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index de8d009892..1b34b625ff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -34,11 +34,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.internal.session.call.ActiveCallHandler import org.matrix.android.sdk.internal.session.sync.SyncPresence +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import timber.log.Timber import java.net.SocketTimeoutException import java.util.Timer @@ -72,6 +75,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } } + private val _syncFlow = MutableSharedFlow() + init { updateStateTo(SyncState.Idle) } @@ -115,6 +120,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, return liveState } + fun syncFlow(): SharedFlow = _syncFlow + override fun onConnectivityChanged() { retryNoNetworkTask?.cancel() synchronized(lock) { @@ -192,7 +199,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private suspend fun doSync(params: SyncTask.Params) { try { - syncTask.execute(params) + val syncResponse = syncTask.execute(params) + _syncFlow.emit(syncResponse) } catch (failure: Throwable) { if (failure is Failure.NetworkConnection) { canReachServer = false From fac9a19c017a8df3724803b057a6f333d228ac2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20V=C3=A1gner?= Date: Fri, 3 Sep 2021 12:34:13 +0200 Subject: [PATCH 10/90] Add back a string that has been removed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Peter Vágner --- vector/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index e9b07763d4..0431b3dd8d 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3477,6 +3477,7 @@ Record Voice Message Slide to cancel + Voice Message Lock Play Voice Message Pause Voice Message Stop Recording From 97dc07f8c9ad7797c4e774000fd722fb3795ad81 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 27 Sep 2021 09:52:54 +0200 Subject: [PATCH 11/90] Fix default encrypted for restricted + hide restricted rule if no current space selected --- changelog.d/4045.bugfix | 1 + .../createroom/CreateRoomController.kt | 6 ++++- .../createroom/CreateRoomFragment.kt | 5 +++-- .../createroom/CreateRoomViewModel.kt | 22 +++++++++++++++---- .../createroom/CreateRoomViewState.kt | 5 +++-- 5 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 changelog.d/4045.bugfix diff --git a/changelog.d/4045.bugfix b/changelog.d/4045.bugfix new file mode 100644 index 0000000000..c6798ae492 --- /dev/null +++ b/changelog.d/4045.bugfix @@ -0,0 +1 @@ +Align new room encryption default to Web \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt index 2676096b6b..0d3066e43a 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt @@ -165,7 +165,11 @@ class CreateRoomController @Inject constructor( host.stringProvider.getString(R.string.create_room_encryption_description) } ) - switchChecked(viewState.isEncrypted) + if (viewState.isEncrypted != null) { + switchChecked(viewState.isEncrypted) + } else { + switchChecked(viewState.defaultEncrypted[viewState.roomJoinRules] ?: false) + } listener { value -> host.listener?.setIsEncrypted(value) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt index 70f041bd69..ac4c9db89f 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -163,8 +163,9 @@ class CreateRoomFragment @Inject constructor( } override fun selectVisibility() = withState(viewModel) { state -> - - val allowed = if (state.supportsRestricted) { + // If restricted is supported and the user is in the context of a parent space + // then show restricted option. + val allowed = if (state.supportsRestricted && state.parentSpaceId != null) { listOf(RoomJoinRules.INVITE, RoomJoinRules.PUBLIC, RoomJoinRules.RESTRICTED) } else { listOf(RoomJoinRules.INVITE, RoomJoinRules.PUBLIC) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index 9a9812933b..45edc207ba 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -109,8 +109,13 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init setState { copy( - isEncrypted = RoomJoinRules.INVITE == roomJoinRules && adminE2EByDefault, - hsAdminHasDisabledE2E = !adminE2EByDefault + hsAdminHasDisabledE2E = !adminE2EByDefault, + defaultEncrypted = mapOf( + RoomJoinRules.INVITE to adminE2EByDefault, + RoomJoinRules.PUBLIC to false, + RoomJoinRules.RESTRICTED to adminE2EByDefault + ) + ) } } @@ -286,8 +291,17 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init disableFederation = state.disableFederation // Encryption - if (state.isEncrypted) { - enableEncryption() + // we ignore the isEncrypted for public room as the switch is hidden in this case + if (state.roomJoinRules != RoomJoinRules.PUBLIC && state.isEncrypted != null) { + // the user explicitly switch the toggle + if (state.isEncrypted) { + enableEncryption() + } + } else { + // based on default + if (state.defaultEncrypted[state.roomJoinRules] == true) { + enableEncryption() + } } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt index db56a19904..528adb6c63 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt @@ -28,7 +28,7 @@ data class CreateRoomViewState( val roomName: String = "", val roomTopic: String = "", val roomJoinRules: RoomJoinRules = RoomJoinRules.INVITE, - val isEncrypted: Boolean = false, + val isEncrypted: Boolean? = null, val showAdvanced: Boolean = false, val disableFederation: Boolean = false, val homeServerName: String = "", @@ -38,7 +38,8 @@ data class CreateRoomViewState( val parentSpaceSummary: RoomSummary? = null, val supportsRestricted: Boolean = false, val aliasLocalPart: String? = null, - val isSubSpace: Boolean = false + val isSubSpace: Boolean = false, + val defaultEncrypted: Map = emptyMap() ) : MvRxState { constructor(args: CreateRoomArgs) : this( From 9815dfe449655b275f800b696df7a1a7195681b5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 28 Sep 2021 18:54:48 +0200 Subject: [PATCH 12/90] Text composer: start extracting in a dedicated ViewModel/State/Action/Events --- .../home/room/detail/RoomDetailAction.kt | 8 - .../home/room/detail/RoomDetailFragment.kt | 102 +-- .../home/room/detail/RoomDetailViewEvents.kt | 14 - .../home/room/detail/RoomDetailViewModel.kt | 523 +-------------- .../home/room/detail/RoomDetailViewState.kt | 3 +- .../detail/composer/TextComposerAction.kt | 30 + .../detail/composer/TextComposerViewEvents.kt | 41 ++ .../detail/composer/TextComposerViewModel.kt | 603 ++++++++++++++++++ .../detail/composer/TextComposerViewState.kt | 35 + 9 files changed, 770 insertions(+), 589 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 9bb82cdc27..88f172d040 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -30,10 +30,7 @@ import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.util.MatrixItem sealed class RoomDetailAction : VectorViewModelAction { - data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction() - data class SaveDraft(val draft: String) : RoomDetailAction() data class SendSticker(val stickerContent: MessageStickerContent) : RoomDetailAction() - data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction() data class SendMedia(val attachments: List, val compressBeforeSending: Boolean) : RoomDetailAction() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction() data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction() @@ -52,11 +49,6 @@ sealed class RoomDetailAction : VectorViewModelAction { object EnterTrackingUnreadMessagesState : RoomDetailAction() object ExitTrackingUnreadMessagesState : RoomDetailAction() - data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction() - data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction() - data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction() - data class EnterRegularMode(val text: String, val fromSharing: Boolean) : RoomDetailAction() - data class ResendMessage(val eventId: String) : RoomDetailAction() data class RemoveFailedEcho(val eventId: String) : RoomDetailAction() data class CancelSend(val eventId: String, val force: Boolean) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index c6eda584ad..520c0a3784 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -133,7 +133,11 @@ import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.composer.TextComposerAction import im.vector.app.features.home.room.detail.composer.TextComposerView +import im.vector.app.features.home.room.detail.composer.TextComposerViewEvents +import im.vector.app.features.home.room.detail.composer.TextComposerViewModel +import im.vector.app.features.home.room.detail.composer.TextComposerViewState import im.vector.app.features.home.room.detail.composer.VoiceMessageRecorderView import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -235,6 +239,7 @@ class RoomDetailFragment @Inject constructor( private val permalinkHandler: PermalinkHandler, private val notificationDrawerManager: NotificationDrawerManager, val roomDetailViewModelFactory: RoomDetailViewModel.Factory, + val textComposerViewModelFactory: TextComposerViewModel.Factory, private val eventHtmlRenderer: EventHtmlRenderer, private val vectorPreferences: VectorPreferences, private val colorProvider: ColorProvider, @@ -287,6 +292,7 @@ class RoomDetailFragment @Inject constructor( autoCompleterFactory.create(roomDetailArgs.roomId) } private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() + private val textComposerViewModel: TextComposerViewModel by fragmentViewModel() private val debouncer = Debouncer(createUIHandler()) private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback @@ -379,7 +385,7 @@ class RoomDetailFragment @Inject constructor( updateJumpToReadMarkerViewVisibility() } - roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode, RoomDetailViewState::canSendMessage) { mode, canSend -> + textComposerViewModel.selectSubscribe(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage) { mode, canSend -> if (!canSend) { return@selectSubscribe } @@ -391,6 +397,7 @@ class RoomDetailFragment @Inject constructor( } } + roomDetailViewModel.selectSubscribe( RoomDetailViewState::syncState, RoomDetailViewState::incrementalSyncStatus, @@ -404,6 +411,15 @@ class RoomDetailFragment @Inject constructor( ) } + textComposerViewModel.observeViewEvents { + when(it){ + is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) + is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it) + is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message) + is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) + }.exhaustive + } + roomDetailViewModel.observeViewEvents { when (it) { is RoomDetailViewEvents.Failure -> { @@ -418,8 +434,6 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message) is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) - is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) - is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode) RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager() is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it) @@ -444,7 +458,6 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() - is RoomDetailViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) }.exhaustive } @@ -495,7 +508,7 @@ class RoomDetailFragment @Inject constructor( JoinReplacementRoomBottomSheet().show(childFragmentManager, tag) } - private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: RoomDetailViewEvents.ShowRoomUpgradeDialog) { + private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: TextComposerViewEvents.ShowRoomUpgradeDialog) { val tag = MigrateRoomBottomSheet::javaClass.name MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion) .show(parentFragmentManager, tag) @@ -753,7 +766,7 @@ class RoomDetailFragment @Inject constructor( .show() } - private fun handleJoinedToAnotherRoom(action: RoomDetailViewEvents.JoinRoomCommandSuccess) { + private fun handleJoinedToAnotherRoom(action: TextComposerViewEvents.JoinRoomCommandSuccess) { updateComposerText("") lockSendButton = false navigator.openRoom(vectorBaseActivity, action.roomId) @@ -762,7 +775,7 @@ class RoomDetailFragment @Inject constructor( private fun handleShareData() { when (val sharedData = roomDetailArgs.sharedData) { is SharedData.Text -> { - roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(sharedData.text, fromSharing = true)) + textComposerViewModel.handle(TextComposerAction.EnterRegularMode(sharedData.text, fromSharing = true)) } is SharedData.Attachments -> { // open share edition @@ -980,9 +993,7 @@ class RoomDetailFragment @Inject constructor( private fun renderRegularMode(text: String) { autoCompleter.exitSpecialMode() views.composerLayout.collapse() - views.voiceMessageRecorderView.isVisible = text.isBlank() - updateComposerText(text) views.composerLayout.views.sendButton.contentDescription = getString(R.string.send) } @@ -1077,7 +1088,7 @@ class RoomDetailFragment @Inject constructor( notificationDrawerManager.setCurrentRoom(null) - roomDetailViewModel.handle(RoomDetailAction.SaveDraft(views.composerLayout.text.toString())) + textComposerViewModel.handle(TextComposerAction.SaveDraft(views.composerLayout.text.toString())) // We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed. roomDetailViewModel.handle(RoomDetailAction.EndAllVoiceActions) @@ -1196,12 +1207,12 @@ class RoomDetailFragment @Inject constructor( override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { (model as? AbsMessageItem)?.attributes?.informationData?.let { val eventId = it.eventId - roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(eventId, views.composerLayout.text.toString())) + textComposerViewModel.handle(TextComposerAction.EnterReplyMode(eventId, views.composerLayout.text.toString())) } } override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - val canSendMessage = withState(roomDetailViewModel) { + val canSendMessage = withState(textComposerViewModel) { it.canSendMessage } if (!canSendMessage) { @@ -1303,7 +1314,7 @@ class RoomDetailFragment @Inject constructor( } override fun onCloseRelatedMessage() { - roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(views.composerLayout.text.toString(), false)) + textComposerViewModel.handle(TextComposerAction.EnterRegularMode(views.composerLayout.text.toString(), false)) } override fun onRichContentSelected(contentUri: Uri): Boolean { @@ -1320,6 +1331,7 @@ class RoomDetailFragment @Inject constructor( views.voiceMessageRecorderView.isVisible = false } } + } } @@ -1332,7 +1344,7 @@ class RoomDetailFragment @Inject constructor( // We collapse ASAP, if not there will be a slight annoying delay views.composerLayout.collapse(true) lockSendButton = true - roomDetailViewModel.handle(RoomDetailAction.SendMessage(text, vectorPreferences.isMarkdownEnabled())) + textComposerViewModel.handle(TextComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled())) emojiPopup.dismiss() } } @@ -1344,7 +1356,7 @@ class RoomDetailFragment @Inject constructor( .map { it.isNotEmpty() } .subscribe { Timber.d("Typing: User is typing: $it") - roomDetailViewModel.handle(RoomDetailAction.UserIsTyping(it)) + textComposerViewModel.handle(TextComposerAction.UserIsTyping(it)) } .disposeOnDestroyView() @@ -1364,24 +1376,24 @@ class RoomDetailFragment @Inject constructor( return isHandled } - override fun invalidate() = withState(roomDetailViewModel) { state -> + override fun invalidate() = withState(roomDetailViewModel, textComposerViewModel) { mainState, textComposerState -> invalidateOptionsMenu() - val summary = state.asyncRoomSummary() - renderToolbar(summary, state.typingMessage) - views.removeJitsiWidgetView.render(state) - if (state.hasFailedSending) { + val summary = mainState.asyncRoomSummary() + renderToolbar(summary, mainState.formattedTypingUsers) + views.removeJitsiWidgetView.render(mainState) + if (mainState.hasFailedSending) { lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true } else { lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = false)?.isVisible = false } - val inviter = state.asyncInviter() + val inviter = mainState.asyncInviter() if (summary?.membership == Membership.JOIN) { views.jumpToBottomView.count = summary.notificationCount views.jumpToBottomView.drawBadge = summary.hasUnreadMessages - timelineEventController.update(state) + timelineEventController.update(mainState) lazyLoadedViews.inviteView(false)?.isVisible = false - if (state.tombstoneEvent == null) { - if (state.canSendMessage) { + if (mainState.tombstoneEvent == null) { + if (textComposerState.canSendMessage) { if (!views.voiceMessageRecorderView.isActive()) { views.composerLayout.isVisible = true views.voiceMessageRecorderView.isVisible = views.composerLayout.text?.isBlank().orFalse() @@ -1390,30 +1402,32 @@ class RoomDetailFragment @Inject constructor( views.composerLayout.alwaysShowSendButton = false } } else { - views.composerLayout.isVisible = false - views.voiceMessageRecorderView.isVisible = false + views.hideComposerViews() views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost) } } else { - views.composerLayout.isVisible = false - views.voiceMessageRecorderView.isVisible = false - views.notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) + views.hideComposerViews() + views.notificationAreaView.render(NotificationAreaView.State.Tombstone(mainState.tombstoneEvent)) } } else if (summary?.membership == Membership.INVITE && inviter != null) { - views.composerLayout.isVisible = false - views.voiceMessageRecorderView.isVisible = false + views.hideComposerViews() lazyLoadedViews.inviteView(true)?.apply { callback = this@RoomDetailFragment isVisible = true - render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState) + render(inviter, VectorInviteView.Mode.LARGE, mainState.changeMembershipState) setOnClickListener { } } Unit - } else if (state.asyncInviter.complete) { + } else if (mainState.asyncInviter.complete) { vectorBaseActivity.finish() } } + private fun FragmentRoomDetailBinding.hideComposerViews() { + composerLayout.isVisible = false + voiceMessageRecorderView.isVisible = false + } + private fun renderToolbar(roomSummary: RoomSummary?, typingMessage: String?) { if (roomSummary == null) { views.roomToolbarContentView.isClickable = false @@ -1442,24 +1456,24 @@ class RoomDetailFragment @Inject constructor( } } - private fun renderSendMessageResult(sendMessageResult: RoomDetailViewEvents.SendMessageResult) { + private fun renderSendMessageResult(sendMessageResult: TextComposerViewEvents.SendMessageResult) { when (sendMessageResult) { - is RoomDetailViewEvents.SlashCommandHandled -> { + is TextComposerViewEvents.SlashCommandHandled -> { sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } } - is RoomDetailViewEvents.SlashCommandError -> { + is TextComposerViewEvents.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) } - is RoomDetailViewEvents.SlashCommandUnknown -> { + is TextComposerViewEvents.SlashCommandUnknown -> { displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } - is RoomDetailViewEvents.SlashCommandResultOk -> { + is TextComposerViewEvents.SlashCommandResultOk -> { updateComposerText("") } - is RoomDetailViewEvents.SlashCommandResultError -> { + is TextComposerViewEvents.SlashCommandResultError -> { displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable)) } - is RoomDetailViewEvents.SlashCommandNotImplemented -> { + is TextComposerViewEvents.SlashCommandNotImplemented -> { displayCommandError(getString(R.string.not_implemented)) } } // .exhaustive @@ -1938,17 +1952,17 @@ class RoomDetailFragment @Inject constructor( } is EventSharedAction.Edit -> { if (!views.voiceMessageRecorderView.isActive()) { - roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) + textComposerViewModel.handle(TextComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } } is EventSharedAction.Quote -> { - roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString())) + textComposerViewModel.handle(TextComposerAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString())) } is EventSharedAction.Reply -> { if (!views.voiceMessageRecorderView.isActive()) { - roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString())) + textComposerViewModel.handle(TextComposerAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString())) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } @@ -2160,7 +2174,7 @@ class RoomDetailFragment @Inject constructor( override fun onContactAttachmentReady(contactAttachment: ContactAttachment) { super.onContactAttachmentReady(contactAttachment) val formattedContact = contactAttachment.toHumanReadable() - roomDetailViewModel.handle(RoomDetailAction.SendMessage(formattedContact, false)) + textComposerViewModel.handle(TextComposerAction.SendMessage(formattedContact, false)) } private fun onViewWidgetsClicked() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 2802ee2f83..abd9a52e6b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -66,8 +66,6 @@ sealed class RoomDetailViewEvents : VectorViewEvents { val mimeType: String? ) : RoomDetailViewEvents() - abstract class SendMessageResult : RoomDetailViewEvents() - data class DisplayAndAcceptCall(val call: WebRtcCall): RoomDetailViewEvents() object DisplayPromptForIntegrationManager : RoomDetailViewEvents() @@ -82,19 +80,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { val domain: String, val grantedEvents: RoomDetailViewEvents) : RoomDetailViewEvents() - object MessageSent : SendMessageResult() - data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() - class SlashCommandError(val command: Command) : SendMessageResult() - class SlashCommandUnknown(val command: String) : SendMessageResult() - data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult() - object SlashCommandResultOk : SendMessageResult() - class SlashCommandResultError(val throwable: Throwable) : SendMessageResult() - - // TODO Remove - object SlashCommandNotImplemented : SendMessageResult() - data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents() object RoomReplacementStarted : RoomDetailViewEvents() - data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean): RoomDetailViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index cacf9b8902..013ace7aed 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -44,8 +44,6 @@ import im.vector.app.features.call.conference.JitsiActiveConferenceHolder import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.lookup.CallProtocolsChecker import im.vector.app.features.call.webrtc.WebRtcCallManager -import im.vector.app.features.command.CommandParser -import im.vector.app.features.command.ParsedCommand import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider @@ -67,9 +65,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.commonmark.parser.Parser -import org.commonmark.renderer.html.HtmlRenderer -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue @@ -86,22 +81,14 @@ import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.api.session.room.model.message.OptionItem import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.read.ReadService -import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent -import org.matrix.android.sdk.api.session.room.timeline.getRelationContent -import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent -import org.matrix.android.sdk.api.session.space.CreateSpaceParams import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode @@ -181,7 +168,6 @@ class RoomDetailViewModel @AssistedInject constructor( observeSyncState() observeDataStore() observeEventDisplayedActions() - loadDraftIfAny() observeUnreadState() observeMyRoomMember() observeActiveRoomWidgets() @@ -235,13 +221,11 @@ class RoomDetailViewModel @AssistedInject constructor( private fun observePowerLevel() { PowerLevelsObservableFactory(room).createObservable() .subscribe { - val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId) val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId) val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE) setState { copy( - canSendMessage = canSendMessage, canInvite = canInvite, isAllowedToManageWidgets = isAllowedToManageWidgets, isAllowedToStartWebRTCCall = isAllowedToStartWebRTCCall @@ -300,10 +284,7 @@ class RoomDetailViewModel @AssistedInject constructor( override fun handle(action: RoomDetailAction) { when (action) { - is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) - is RoomDetailAction.SaveDraft -> handleSaveDraft(action) - is RoomDetailAction.SendMessage -> handleSendMessage(action) is RoomDetailAction.SendMedia -> handleSendMedia(action) is RoomDetailAction.SendSticker -> handleSendSticker(action) is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) @@ -315,10 +296,6 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.RedactAction -> handleRedactEvent(action) is RoomDetailAction.UndoReaction -> handleUndoReact(action) is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action) - is RoomDetailAction.EnterEditMode -> handleEditAction(action) - is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) is RoomDetailAction.JoinAndOpenReplacementRoom -> handleJoinAndOpenReplacementRoom() @@ -590,70 +567,6 @@ class RoomDetailViewModel @AssistedInject constructor( return room.getRoomMember(userId) } - /** - * Convert a send mode to a draft and save the draft - */ - private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState { - session.coroutineScope.launch { - when { - it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { - setState { copy(sendMode = it.sendMode.copy(action.draft)) } - room.saveDraft(UserDraft.REGULAR(action.draft)) - } - it.sendMode is SendMode.REPLY -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) - } - it.sendMode is SendMode.QUOTE -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) - } - it.sendMode is SendMode.EDIT -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) - } - } - } - } - - private fun loadDraftIfAny() { - val currentDraft = room.getDraft() - setState { - copy( - // Create a sendMode from a draft and retrieve the TimelineEvent - sendMode = when (currentDraft) { - is UserDraft.REGULAR -> SendMode.REGULAR(currentDraft.text, false) - is UserDraft.QUOTE -> { - room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, currentDraft.text) - } - } - is UserDraft.REPLY -> { - room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, currentDraft.text) - } - } - is UserDraft.EDIT -> { - room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, currentDraft.text) - } - } - else -> null - } ?: SendMode.REGULAR("", fromSharing = false) - ) - } - } - - private fun handleUserIsTyping(action: RoomDetailAction.UserIsTyping) { - if (vectorPreferences.sendTypingNotifs()) { - if (action.isTyping) { - room.userIsTyping() - } else { - room.userStopsTyping() - } - } - } - private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) { // Ensure outbound session keys if (OutboundSessionKeySharingStrategy.WhenTyping == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) { @@ -766,417 +679,7 @@ class RoomDetailViewModel @AssistedInject constructor( } } -// PRIVATE METHODS ***************************************************************************** - - private fun handleSendMessage(action: RoomDetailAction.SendMessage) { - withState { state -> - when (state.sendMode) { - is SendMode.REGULAR -> { - when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) { - is ParsedCommand.ErrorNotACommand -> { - // Send the text message to the room - room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) - _viewEvents.post(RoomDetailViewEvents.MessageSent) - popDraft() - } - is ParsedCommand.ErrorSyntax -> { - _viewEvents.post(RoomDetailViewEvents.SlashCommandError(slashCommandResult.command)) - } - is ParsedCommand.ErrorEmptySlashCommand -> { - _viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown("/")) - } - is ParsedCommand.ErrorUnknownSlashCommand -> { - _viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand)) - } - is ParsedCommand.SendPlainText -> { - // Send the text message to the room, without markdown - room.sendTextMessage(slashCommandResult.message, autoMarkdown = false) - _viewEvents.post(RoomDetailViewEvents.MessageSent) - popDraft() - } - is ParsedCommand.Invite -> { - handleInviteSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.Invite3Pid -> { - handleInvite3pidSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.SetUserPowerLevel -> { - handleSetUserPowerLevel(slashCommandResult) - popDraft() - } - is ParsedCommand.ClearScalarToken -> { - // TODO - _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) - } - is ParsedCommand.SetMarkdown -> { - vectorPreferences.setMarkdownEnabled(slashCommandResult.enable) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled( - if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) - popDraft() - } - is ParsedCommand.UnbanUser -> { - handleUnbanSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.BanUser -> { - handleBanSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.KickUser -> { - handleKickSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.JoinRoom -> { - handleJoinToAnotherRoomSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.PartRoom -> { - // TODO - _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) - } - is ParsedCommand.SendEmote -> { - room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendRainbow -> { - slashCommandResult.message.toString().let { - room.sendFormattedTextMessage(it, rainbowGenerator.generate(it)) - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendRainbowEmote -> { - slashCommandResult.message.toString().let { - room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE) - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendSpoiler -> { - room.sendFormattedTextMessage( - "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})", - "${slashCommandResult.message}" - ) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendShrug -> { - val sequence = buildString { - append("¯\\_(ツ)_/¯") - if (slashCommandResult.message.isNotEmpty()) { - append(" ") - append(slashCommandResult.message) - } - } - room.sendTextMessage(sequence) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendChatEffect -> { - sendChatEffect(slashCommandResult) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendPoll -> { - room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") }) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.ChangeTopic -> { - handleChangeTopicSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.ChangeDisplayName -> { - handleChangeDisplayNameSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.DiscardSession -> { - if (room.isEncrypted()) { - session.cryptoService().discardOutboundSession(room.roomId) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } else { - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - _viewEvents.post( - RoomDetailViewEvents - .ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled)) - ) - } - } - is ParsedCommand.CreateSpace -> { - viewModelScope.launch(Dispatchers.IO) { - try { - val params = CreateSpaceParams().apply { - name = slashCommandResult.name - invitedUserIds.addAll(slashCommandResult.invitees) - } - val spaceId = session.spaceService().createSpace(params) - session.spaceService().getSpace(spaceId) - ?.addChildren( - state.roomId, - null, - null, - true - ) - } catch (failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - } - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.AddToSpace -> { - viewModelScope.launch(Dispatchers.IO) { - try { - session.spaceService().getSpace(slashCommandResult.spaceId) - ?.addChildren( - room.roomId, - null, - null, - false - ) - } catch (failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - } - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.JoinSpace -> { - viewModelScope.launch(Dispatchers.IO) { - try { - session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias) - } catch (failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - } - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.LeaveRoom -> { - viewModelScope.launch(Dispatchers.IO) { - try { - session.getRoom(slashCommandResult.roomId)?.leave(null) - } catch (failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - } - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.UpgradeRoom -> { - _viewEvents.post( - RoomDetailViewEvents.ShowRoomUpgradeDialog( - slashCommandResult.newVersion, - room.roomSummary()?.isPublic ?: false - ) - ) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - }.exhaustive - } - is SendMode.EDIT -> { - // is original event a reply? - val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId - if (inReplyTo != null) { - // TODO check if same content? - room.getTimeLineEvent(inReplyTo)?.let { - room.editReply(state.sendMode.timelineEvent, it, action.text.toString()) - } - } else { - val messageContent = state.sendMode.timelineEvent.getLastMessageContent() - val existingBody = messageContent?.body ?: "" - if (existingBody != action.text) { - room.editTextMessage(state.sendMode.timelineEvent, - messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, - action.text, - action.autoMarkdown) - } else { - Timber.w("Same message content, do not send edition") - } - } - _viewEvents.post(RoomDetailViewEvents.MessageSent) - popDraft() - } - is SendMode.QUOTE -> { - val messageContent = state.sendMode.timelineEvent.getLastMessageContent() - val textMsg = messageContent?.body - - val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) - - // TODO check for pills? - - // TODO Refactor this, just temporary for quotes - val parser = Parser.builder().build() - val document = parser.parse(finalText) - val renderer = HtmlRenderer.builder().build() - val htmlText = renderer.render(document) - if (finalText == htmlText) { - room.sendTextMessage(finalText) - } else { - room.sendFormattedTextMessage(finalText, htmlText) - } - _viewEvents.post(RoomDetailViewEvents.MessageSent) - popDraft() - } - is SendMode.REPLY -> { - state.sendMode.timelineEvent.let { - room.replyToMessage(it, action.text.toString(), action.autoMarkdown) - _viewEvents.post(RoomDetailViewEvents.MessageSent) - popDraft() - } - } - }.exhaustive - } - } - - private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { - // If message is blank, convert to an emote, with default message - if (sendChatEffect.message.isBlank()) { - val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) { - ChatEffect.CONFETTI -> R.string.default_message_emote_confetti - ChatEffect.SNOWFALL -> R.string.default_message_emote_snow - }) - room.sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE) - } else { - room.sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType()) - } - } - - private fun popDraft() = withState { - if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) { - // If we were sharing, we want to get back our last value from draft - loadDraftIfAny() - } else { - // Otherwise we clear the composer and remove the draft from db - setState { copy(sendMode = SendMode.REGULAR("", false)) } - viewModelScope.launch { - room.deleteDraft() - } - } - } - - private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { - viewModelScope.launch { - try { - session.joinRoom(command.roomAlias, command.reason, emptyList()) - } catch (failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - return@launch - } - session.getRoomSummary(command.roomAlias) - ?.roomId - ?.let { - _viewEvents.post(RoomDetailViewEvents.JoinRoomCommandSuccess(it)) - } - } - } - - private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { - val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() - return buildString { - if (messageParagraphs != null) { - for (i in messageParagraphs.indices) { - if (messageParagraphs[i].isNotBlank()) { - append("> ") - append(messageParagraphs[i]) - } - - if (i != messageParagraphs.lastIndex) { - append("\n\n") - } - } - } - append("\n\n") - append(myText) - } - } - - private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { - launchSlashCommandFlowSuspendable { - room.updateTopic(changeTopic.topic) - } - } - - private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { - launchSlashCommandFlowSuspendable { - room.invite(invite.userId, invite.reason) - } - } - - private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) { - launchSlashCommandFlowSuspendable { - room.invite3pid(invite.threePid) - } - } - - private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { - val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) - ?.content - ?.toModel() - ?.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel) - ?.toContent() - ?: return - - launchSlashCommandFlowSuspendable { - room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent) - } - } - - private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) { - launchSlashCommandFlowSuspendable { - session.setDisplayName(session.myUserId, changeDisplayName.displayName) - } - } - - private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) { - launchSlashCommandFlowSuspendable { - room.kick(kick.userId, kick.reason) - } - } - - private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { - launchSlashCommandFlowSuspendable { - room.ban(ban.userId, ban.reason) - } - } - - private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { - launchSlashCommandFlowSuspendable { - room.unban(unban.userId, unban.reason) - } - } - - private fun launchSlashCommandFlow(lambda: (MatrixCallback) -> Unit) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - val matrixCallback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk) - } - - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - } - } - lambda.invoke(matrixCallback) - } - - private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - viewModelScope.launch { - val event = try { - block() - RoomDetailViewEvents.SlashCommandResultOk - } catch (failure: Exception) { - RoomDetailViewEvents.SlashCommandResultError(failure) - } - _viewEvents.post(event) - } - } + // PRIVATE METHODS ***************************************************************************** private fun handleSendReaction(action: RoomDetailAction.SendReaction) { room.sendReaction(action.targetEventId, action.reaction) @@ -1246,28 +749,6 @@ class RoomDetailViewModel @AssistedInject constructor( } } - private fun handleEditAction(action: RoomDetailAction.EnterEditMode) { - room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> - setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent() ?: "")) } - } - } - - private fun handleQuoteAction(action: RoomDetailAction.EnterQuoteMode) { - room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> - setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) } - } - } - - private fun handleReplyAction(action: RoomDetailAction.EnterReplyMode) { - room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> - setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) } - } - } - - private fun handleEnterRegularMode(action: RoomDetailAction.EnterRegularMode) = setState { - copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing)) - } - private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { val mxcUrl = action.messageFileContent.getFileUrl() ?: return val isLocalSendingFile = action.senderId == session.myUserId @@ -1604,7 +1085,7 @@ class RoomDetailViewModel @AssistedInject constructor( setState { val typingMessage = typingHelper.getTypingMessage(summary.typingUsers) copy( - typingMessage = typingMessage, + formattedTypingUsers = typingMessage, hasFailedSending = summary.hasFailedSending ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 8f4ad97b72..a1eac83fc8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -73,7 +73,7 @@ data class RoomDetailViewState( val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, val activeRoomWidgets: Async> = Uninitialized, - val typingMessage: String? = null, + val formattedTypingUsers: String? = null, val sendMode: SendMode = SendMode.REGULAR("", false), val tombstoneEvent: Event? = null, val joinUpgradedRoomAsync: Async = Uninitialized, @@ -84,7 +84,6 @@ data class RoomDetailViewState( val unreadState: UnreadState = UnreadState.Unknown, val canShowJumpToReadMarker: Boolean = true, val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown, - val canSendMessage: Boolean = true, val canInvite: Boolean = true, val isAllowedToManageWidgets: Boolean = false, val isAllowedToStartWebRTCCall: Boolean = true, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt new file mode 100644 index 0000000000..7896a009c1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 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. + */ + +package im.vector.app.features.home.room.detail.composer + +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.home.room.detail.RoomDetailAction + +sealed class TextComposerAction : VectorViewModelAction { + data class SaveDraft(val draft: String) : TextComposerAction() + data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : TextComposerAction() + data class EnterEditMode(val eventId: String, val text: String) : TextComposerAction() + data class EnterQuoteMode(val eventId: String, val text: String) : TextComposerAction() + data class EnterReplyMode(val eventId: String, val text: String) : TextComposerAction() + data class EnterRegularMode(val text: String, val fromSharing: Boolean) : TextComposerAction() + data class UserIsTyping(val isTyping: Boolean) : TextComposerAction() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt new file mode 100644 index 0000000000..cce33a5b63 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 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. + */ + +package im.vector.app.features.home.room.detail.composer + +import androidx.annotation.StringRes +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.command.Command + +sealed class TextComposerViewEvents : VectorViewEvents { + + data class ShowMessage(val message: String) : TextComposerViewEvents() + + abstract class SendMessageResult : TextComposerViewEvents() + + object MessageSent : SendMessageResult() + data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() + class SlashCommandError(val command: Command) : SendMessageResult() + class SlashCommandUnknown(val command: String) : SendMessageResult() + data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult() + object SlashCommandResultOk : SendMessageResult() + class SlashCommandResultError(val throwable: Throwable) : SendMessageResult() + + // TODO Remove + object SlashCommandNotImplemented : SendMessageResult() + + data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : TextComposerViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt new file mode 100644 index 0000000000..870f5682e0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -0,0 +1,603 @@ +/* + * Copyright (c) 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. + */ + +package im.vector.app.features.home.room.detail.composer + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.BuildConfig +import im.vector.app.R +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.command.CommandParser +import im.vector.app.features.command.ParsedCommand +import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy +import im.vector.app.features.home.room.detail.ChatEffect +import im.vector.app.features.home.room.detail.RoomDetailAction +import im.vector.app.features.home.room.detail.RoomDetailFragment +import im.vector.app.features.home.room.detail.SendMode +import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator +import im.vector.app.features.home.room.detail.toMessageType +import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.session.coroutineScope +import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.OptionItem +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.api.session.room.timeline.getRelationContent +import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent +import org.matrix.android.sdk.api.session.space.CreateSpaceParams +import timber.log.Timber + +class TextComposerViewModel @AssistedInject constructor( + @Assisted initialState: TextComposerViewState, + private val session: Session, + private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences, + private val rainbowGenerator: RainbowGenerator +) : VectorViewModel(initialState) { + + private val room = session.getRoom(initialState.roomId)!! + + init { + loadDraftIfAny() + observePowerLevel() + } + + override fun handle(action: TextComposerAction) { + Timber.v("Handle action: $action") + when (action) { + is TextComposerAction.EnterEditMode -> handleEnterEditMode(action) + is TextComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) + is TextComposerAction.EnterRegularMode -> handleEnterRegularMode(action) + is TextComposerAction.EnterReplyMode -> handleEnterReplyMode(action) + is TextComposerAction.SaveDraft -> handleSaveDraft(action) + is TextComposerAction.SendMessage -> handleSendMessage(action) + is TextComposerAction.UserIsTyping -> handleUserIsTyping(action) + } + } + + private fun handleEnterRegularMode(action: TextComposerAction.EnterRegularMode) = setState { + copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing)) + } + + private fun handleEnterEditMode(action: TextComposerAction.EnterEditMode) { + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent() ?: "")) } + } + } + + private fun observePowerLevel() { + PowerLevelsObservableFactory(room).createObservable() + .subscribe { + val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) + setState { + copy(canSendMessage = canSendMessage) + } + } + .disposeOnClear() + } + + private fun handleEnterQuoteMode(action: TextComposerAction.EnterQuoteMode) { + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) } + } + } + + private fun handleEnterReplyMode(action: TextComposerAction.EnterReplyMode) { + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) } + } + } + + private fun handleSendMessage(action: TextComposerAction.SendMessage) { + withState { state -> + when (state.sendMode) { + is SendMode.REGULAR -> { + when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) { + is ParsedCommand.ErrorNotACommand -> { + // Send the text message to the room + room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() + } + is ParsedCommand.ErrorSyntax -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandError(slashCommandResult.command)) + } + is ParsedCommand.ErrorEmptySlashCommand -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandUnknown("/")) + } + is ParsedCommand.ErrorUnknownSlashCommand -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand)) + } + is ParsedCommand.SendPlainText -> { + // Send the text message to the room, without markdown + room.sendTextMessage(slashCommandResult.message, autoMarkdown = false) + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() + } + is ParsedCommand.Invite -> { + handleInviteSlashCommand(slashCommandResult) + popDraft() + } + is ParsedCommand.Invite3Pid -> { + handleInvite3pidSlashCommand(slashCommandResult) + popDraft() + } + is ParsedCommand.SetUserPowerLevel -> { + handleSetUserPowerLevel(slashCommandResult) + popDraft() + } + is ParsedCommand.ClearScalarToken -> { + // TODO + _viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented) + } + is ParsedCommand.SetMarkdown -> { + vectorPreferences.setMarkdownEnabled(slashCommandResult.enable) + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled( + if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) + popDraft() + } + is ParsedCommand.UnbanUser -> { + handleUnbanSlashCommand(slashCommandResult) + popDraft() + } + is ParsedCommand.BanUser -> { + handleBanSlashCommand(slashCommandResult) + popDraft() + } + is ParsedCommand.KickUser -> { + handleKickSlashCommand(slashCommandResult) + popDraft() + } + is ParsedCommand.JoinRoom -> { + handleJoinToAnotherRoomSlashCommand(slashCommandResult) + popDraft() + } + is ParsedCommand.PartRoom -> { + // TODO + _viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented) + } + is ParsedCommand.SendEmote -> { + room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.SendRainbow -> { + slashCommandResult.message.toString().let { + room.sendFormattedTextMessage(it, rainbowGenerator.generate(it)) + } + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.SendRainbowEmote -> { + slashCommandResult.message.toString().let { + room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE) + } + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.SendSpoiler -> { + room.sendFormattedTextMessage( + "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})", + "${slashCommandResult.message}" + ) + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.SendShrug -> { + val sequence = buildString { + append("¯\\_(ツ)_/¯") + if (slashCommandResult.message.isNotEmpty()) { + append(" ") + append(slashCommandResult.message) + } + } + room.sendTextMessage(sequence) + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.SendChatEffect -> { + sendChatEffect(slashCommandResult) + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.SendPoll -> { + room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") }) + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.ChangeTopic -> { + handleChangeTopicSlashCommand(slashCommandResult) + popDraft() + } + is ParsedCommand.ChangeDisplayName -> { + handleChangeDisplayNameSlashCommand(slashCommandResult) + popDraft() + } + is ParsedCommand.DiscardSession -> { + if (room.isEncrypted()) { + session.cryptoService().discardOutboundSession(room.roomId) + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } else { + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + _viewEvents.post( + TextComposerViewEvents + .ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled)) + ) + } + } + is ParsedCommand.CreateSpace -> { + viewModelScope.launch(Dispatchers.IO) { + try { + val params = CreateSpaceParams().apply { + name = slashCommandResult.name + invitedUserIds.addAll(slashCommandResult.invitees) + } + val spaceId = session.spaceService().createSpace(params) + session.spaceService().getSpace(spaceId) + ?.addChildren( + state.roomId, + null, + null, + true + ) + } catch (failure: Throwable) { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) + } + } + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.AddToSpace -> { + viewModelScope.launch(Dispatchers.IO) { + try { + session.spaceService().getSpace(slashCommandResult.spaceId) + ?.addChildren( + room.roomId, + null, + null, + false + ) + } catch (failure: Throwable) { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) + } + } + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.JoinSpace -> { + viewModelScope.launch(Dispatchers.IO) { + try { + session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias) + } catch (failure: Throwable) { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) + } + } + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.LeaveRoom -> { + viewModelScope.launch(Dispatchers.IO) { + try { + session.getRoom(slashCommandResult.roomId)?.leave(null) + } catch (failure: Throwable) { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) + } + } + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.UpgradeRoom -> { + _viewEvents.post( + TextComposerViewEvents.ShowRoomUpgradeDialog( + slashCommandResult.newVersion, + room.roomSummary()?.isPublic ?: false + ) + ) + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + popDraft() + } + }.exhaustive + } + is SendMode.EDIT -> { + // is original event a reply? + val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId + if (inReplyTo != null) { + // TODO check if same content? + room.getTimeLineEvent(inReplyTo)?.let { + room.editReply(state.sendMode.timelineEvent, it, action.text.toString()) + } + } else { + val messageContent = state.sendMode.timelineEvent.getLastMessageContent() + val existingBody = messageContent?.body ?: "" + if (existingBody != action.text) { + room.editTextMessage(state.sendMode.timelineEvent, + messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) + } else { + Timber.w("Same message content, do not send edition") + } + } + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() + } + is SendMode.QUOTE -> { + val messageContent = state.sendMode.timelineEvent.getLastMessageContent() + val textMsg = messageContent?.body + + val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) + + // TODO check for pills? + + // TODO Refactor this, just temporary for quotes + val parser = Parser.builder().build() + val document = parser.parse(finalText) + val renderer = HtmlRenderer.builder().build() + val htmlText = renderer.render(document) + if (finalText == htmlText) { + room.sendTextMessage(finalText) + } else { + room.sendFormattedTextMessage(finalText, htmlText) + } + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() + } + is SendMode.REPLY -> { + state.sendMode.timelineEvent.let { + room.replyToMessage(it, action.text.toString(), action.autoMarkdown) + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() + } + } + }.exhaustive + } + } + + private fun popDraft() = withState { + if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) { + // If we were sharing, we want to get back our last value from draft + loadDraftIfAny() + } else { + // Otherwise we clear the composer and remove the draft from db + setState { copy(sendMode = SendMode.REGULAR("", false)) } + viewModelScope.launch { + room.deleteDraft() + } + } + } + + private fun loadDraftIfAny() { + val currentDraft = room.getDraft() + setState { + copy( + // Create a sendMode from a draft and retrieve the TimelineEvent + sendMode = when (currentDraft) { + is UserDraft.REGULAR -> SendMode.REGULAR(currentDraft.text, false) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, currentDraft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, currentDraft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, currentDraft.text) + } + } + else -> null + } ?: SendMode.REGULAR("", fromSharing = false) + ) + } + } + + private fun handleUserIsTyping(action: TextComposerAction.UserIsTyping) { + if (vectorPreferences.sendTypingNotifs()) { + if (action.isTyping) { + room.userIsTyping() + } else { + room.userStopsTyping() + } + } + } + + + private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { + // If message is blank, convert to an emote, with default message + if (sendChatEffect.message.isBlank()) { + val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) { + ChatEffect.CONFETTI -> R.string.default_message_emote_confetti + ChatEffect.SNOWFALL -> R.string.default_message_emote_snow + }) + room.sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE) + } else { + room.sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType()) + } + } + + private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { + viewModelScope.launch { + try { + session.joinRoom(command.roomAlias, command.reason, emptyList()) + } catch (failure: Throwable) { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) + return@launch + } + session.getRoomSummary(command.roomAlias) + ?.roomId + ?.let { + _viewEvents.post(TextComposerViewEvents.JoinRoomCommandSuccess(it)) + } + } + } + + private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { + val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() + return buildString { + if (messageParagraphs != null) { + for (i in messageParagraphs.indices) { + if (messageParagraphs[i].isNotBlank()) { + append("> ") + append(messageParagraphs[i]) + } + + if (i != messageParagraphs.lastIndex) { + append("\n\n") + } + } + } + append("\n\n") + append(myText) + } + } + + private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { + launchSlashCommandFlowSuspendable { + room.updateTopic(changeTopic.topic) + } + } + + private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { + launchSlashCommandFlowSuspendable { + room.invite(invite.userId, invite.reason) + } + } + + private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) { + launchSlashCommandFlowSuspendable { + room.invite3pid(invite.threePid) + } + } + + private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { + val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + ?.content + ?.toModel() + ?.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel) + ?.toContent() + ?: return + + launchSlashCommandFlowSuspendable { + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent) + } + } + + private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) { + launchSlashCommandFlowSuspendable { + session.setDisplayName(session.myUserId, changeDisplayName.displayName) + } + } + + private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) { + launchSlashCommandFlowSuspendable { + room.kick(kick.userId, kick.reason) + } + } + + private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { + launchSlashCommandFlowSuspendable { + room.ban(ban.userId, ban.reason) + } + } + + private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { + launchSlashCommandFlowSuspendable { + room.unban(unban.userId, unban.reason) + } + } + + /** + * Convert a send mode to a draft and save the draft + */ + private fun handleSaveDraft(action: TextComposerAction.SaveDraft) = withState { + session.coroutineScope.launch { + when { + it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { + setState { copy(sendMode = it.sendMode.copy(action.draft)) } + room.saveDraft(UserDraft.REGULAR(action.draft)) + } + it.sendMode is SendMode.REPLY -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + it.sendMode is SendMode.QUOTE -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + it.sendMode is SendMode.EDIT -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + } + } + } + + + private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) { + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) + viewModelScope.launch { + val event = try { + block() + TextComposerViewEvents.SlashCommandResultOk + } catch (failure: Exception) { + TextComposerViewEvents.SlashCommandResultError(failure) + } + _viewEvents.post(event) + } + } + + @AssistedFactory + interface Factory { + fun create(initialState: TextComposerViewState): TextComposerViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel { + val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.textComposerViewModelFactory.create(state) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt new file mode 100644 index 0000000000..1e118ef38a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 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. + */ + +package im.vector.app.features.home.room.detail.composer + +import com.airbnb.mvrx.MvRxState +import im.vector.app.features.home.room.detail.RoomDetailArgs +import im.vector.app.features.home.room.detail.SendMode + +data class TextComposerViewState( + val roomId: String, + val canSendMessage: Boolean = true, + val isSendButtonVisible: Boolean = false, + val isRecordingVoiceMessage: Boolean = false, + val sendMode: SendMode = SendMode.REGULAR("", false), +) : MvRxState { + + constructor(args: RoomDetailArgs) : this( + roomId = args.roomId, + ) +} + From d24f448c70931b937aaf7e8bebd03cb4afb3da56 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 29 Sep 2021 10:50:17 +0200 Subject: [PATCH 13/90] App doesn't take you to a Space after choosing to Join it --- changelog.d/3933.bugfix | 1 + .../app/features/matrixto/MatrixToBottomSheet.kt | 14 ++++++-------- 2 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 changelog.d/3933.bugfix diff --git a/changelog.d/3933.bugfix b/changelog.d/3933.bugfix new file mode 100644 index 0000000000..3396bd75e7 --- /dev/null +++ b/changelog.d/3933.bugfix @@ -0,0 +1 @@ +App doesn't take you to a Space after choosing to Join it \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt index 3e75b96c32..55c83992b8 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt @@ -36,7 +36,6 @@ import im.vector.app.databinding.BottomSheetMatrixToCardBinding import im.vector.app.features.home.AvatarRenderer import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.permalinks.PermalinkData -import java.lang.ref.WeakReference import javax.inject.Inject import kotlin.reflect.KClass @@ -57,13 +56,7 @@ class MatrixToBottomSheet : injector.inject(this) } - private var weakReference = WeakReference(null) - - var interactionListener: InteractionListener? - set(value) { - weakReference = WeakReference(value) - } - get() = weakReference.get() + var interactionListener: InteractionListener? = null override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetMatrixToCardBinding { return BottomSheetMatrixToCardBinding.inflate(inflater, container, false) @@ -76,6 +69,11 @@ class MatrixToBottomSheet : fun switchToSpace(spaceId: String) {} } + override fun onDestroyView() { + interactionListener = null + super.onDestroyView() + } + override fun invalidate() = withState(viewModel) { state -> super.invalidate() when (state.linkType) { From cdc6b7e7d507f78073285f12aa4d96f42fa7fa5e Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 29 Sep 2021 14:40:18 +0200 Subject: [PATCH 14/90] Remove listener use fragmentCallback --- .../vector/app/features/home/HomeActivity.kt | 50 ++++++++++++------- .../home/room/detail/RoomDetailActivity.kt | 37 ++++++++++++-- .../features/matrixto/MatrixToBottomSheet.kt | 16 ++---- .../features/navigation/DefaultNavigator.kt | 12 ++--- .../features/spaces/SpaceExploreActivity.kt | 21 +++++--- .../app/features/usercode/UserCodeActivity.kt | 29 ++++++++++- 6 files changed, 114 insertions(+), 51 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 4b12237bdf..2d57539abc 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -27,6 +27,8 @@ import android.view.MenuItem import androidx.core.view.GravityCompat import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.viewModel import com.google.android.material.appbar.MaterialToolbar @@ -93,7 +95,8 @@ class HomeActivity : UnreadMessagesSharedViewModel.Factory, PromoteRestrictedViewModel.Factory, NavigationInterceptor, - SpaceInviteBottomSheet.InteractionListener { + SpaceInviteBottomSheet.InteractionListener, + MatrixToBottomSheet.InteractionListener { private lateinit var sharedActionViewModel: HomeSharedActionViewModel @@ -142,6 +145,22 @@ class HomeActivity : } } + private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = this@HomeActivity + } + super.onFragmentResumed(fm, f) + } + + override fun onFragmentPaused(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = null + } + super.onFragmentPaused(fm, f) + } + } + private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { hideKeyboard() @@ -170,6 +189,7 @@ class HomeActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java) views.drawerLayout.addDrawerListener(drawerListener) @@ -445,6 +465,7 @@ class HomeActivity : override fun onDestroy() { views.drawerLayout.removeDrawerListener(drawerListener) + supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks) super.onDestroy() } @@ -526,30 +547,15 @@ class HomeActivity : } override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean { - val listener = object : MatrixToBottomSheet.InteractionListener { - override fun navigateToRoom(roomId: String) { - navigator.openRoom(this@HomeActivity, roomId) - } - } // TODO check if there is already one?? - MatrixToBottomSheet.withLink(deepLink.toString(), listener) + MatrixToBottomSheet.withLink(deepLink.toString()) .show(supportFragmentManager, "HA#MatrixToBottomSheet") return true } override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { if (roomId == null) return false - val listener = object : MatrixToBottomSheet.InteractionListener { - override fun navigateToRoom(roomId: String) { - navigator.openRoom(this@HomeActivity, roomId) - } - - override fun switchToSpace(spaceId: String) { - navigator.switchToSpace(this@HomeActivity, spaceId, Navigator.PostSwitchSpaceAction.None) - } - } - - MatrixToBottomSheet.withLink(deepLink.toString(), listener) + MatrixToBottomSheet.withLink(deepLink.toString()) .show(supportFragmentManager, "HA#MatrixToBottomSheet") return true } @@ -586,4 +592,12 @@ class HomeActivity : } override fun create(initialState: ActiveSpaceViewState) = promoteRestrictedViewModelFactory.create(initialState) + + override fun mxToBottomSheetNavigateToRoom(roomId: String) { + navigator.openRoom(this, roomId) + } + + override fun mxToBottomSheetSwitchToSpace(spaceId: String) { + navigator.switchToSpace(this, spaceId, Navigator.PostSwitchSpaceAction.None) + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt index 25428bbfbf..76c3816ce6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt @@ -20,10 +20,12 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Toast -import com.google.android.material.appbar.MaterialToolbar import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.viewModel +import com.google.android.material.appbar.MaterialToolbar import im.vector.app.R import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.hideKeyboard @@ -32,25 +34,44 @@ import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityRoomDetailBinding import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment +import im.vector.app.features.matrixto.MatrixToBottomSheet +import im.vector.app.features.navigation.Navigator import im.vector.app.features.room.RequireActiveMembershipAction import im.vector.app.features.room.RequireActiveMembershipViewEvents import im.vector.app.features.room.RequireActiveMembershipViewModel import im.vector.app.features.room.RequireActiveMembershipViewState import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewModel import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewState - import javax.inject.Inject class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable, RequireActiveMembershipViewModel.Factory, - RoomWidgetPermissionViewModel.Factory { + RoomWidgetPermissionViewModel.Factory, + MatrixToBottomSheet.InteractionListener { override fun getBinding(): ActivityRoomDetailBinding { return ActivityRoomDetailBinding.inflate(layoutInflater) } + private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { + + override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = this@RoomDetailActivity + } + super.onFragmentResumed(fm, f) + } + + override fun onFragmentPaused(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = null + } + super.onFragmentPaused(fm, f) + } + } + override fun getCoordinatorLayout() = views.coordinatorLayout private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel @@ -79,6 +100,7 @@ class RoomDetailActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) waitingView = views.waitingView.waitingView val roomDetailArgs: RoomDetailArgs? = if (intent?.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) { RoomDetailArgs(roomId = intent?.extras?.getString(EXTRA_ROOM_ID)!!) @@ -130,6 +152,7 @@ class RoomDetailActivity : } override fun onDestroy() { + supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks) views.drawerLayout.removeDrawerListener(drawerListener) super.onDestroy() } @@ -182,4 +205,12 @@ class RoomDetailActivity : } } } + + override fun mxToBottomSheetNavigateToRoom(roomId: String) { + navigator.openRoom(this, roomId) + } + + override fun mxToBottomSheetSwitchToSpace(spaceId: String) { + navigator.switchToSpace(this, spaceId, Navigator.PostSwitchSpaceAction.None) + } } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt index 55c83992b8..bf3e7a5d78 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt @@ -65,13 +65,8 @@ class MatrixToBottomSheet : private val viewModel by fragmentViewModel(MatrixToBottomSheetViewModel::class) interface InteractionListener { - fun navigateToRoom(roomId: String) - fun switchToSpace(spaceId: String) {} - } - - override fun onDestroyView() { - interactionListener = null - super.onDestroyView() + fun mxToBottomSheetNavigateToRoom(roomId: String) + fun mxToBottomSheetSwitchToSpace(spaceId: String) } override fun invalidate() = withState(viewModel) { state -> @@ -110,12 +105,12 @@ class MatrixToBottomSheet : viewModel.observeViewEvents { when (it) { is MatrixToViewEvents.NavigateToRoom -> { - interactionListener?.navigateToRoom(it.roomId) + interactionListener?.mxToBottomSheetNavigateToRoom(it.roomId) dismiss() } MatrixToViewEvents.Dismiss -> dismiss() is MatrixToViewEvents.NavigateToSpace -> { - interactionListener?.switchToSpace(it.spaceId) + interactionListener?.mxToBottomSheetSwitchToSpace(it.spaceId) dismiss() } is MatrixToViewEvents.ShowModalError -> { @@ -129,14 +124,13 @@ class MatrixToBottomSheet : } companion object { - fun withLink(matrixToLink: String, listener: InteractionListener?): MatrixToBottomSheet { + fun withLink(matrixToLink: String): MatrixToBottomSheet { return MatrixToBottomSheet().apply { arguments = Bundle().apply { putParcelable(MvRx.KEY_ARG, MatrixToArgs( matrixToLink = matrixToLink )) } - interactionListener = listener } } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index fd163f7a34..5ecbe4900f 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -257,17 +257,11 @@ class DefaultNavigator @Inject constructor( override fun openMatrixToBottomSheet(context: Context, link: String) { if (context is AppCompatActivity) { - val listener = object : MatrixToBottomSheet.InteractionListener { - override fun navigateToRoom(roomId: String) { - openRoom(context, roomId) - } - - override fun switchToSpace(spaceId: String) { - this@DefaultNavigator.switchToSpace(context, spaceId, Navigator.PostSwitchSpaceAction.None) - } + if (context !is MatrixToBottomSheet.InteractionListener) { + fatalError("Caller context should implement MatrixToBottomSheet.InteractionListener", vectorPreferences.failFast()) } // TODO check if there is already one?? - MatrixToBottomSheet.withLink(link, listener) + MatrixToBottomSheet.withLink(link) .show(context.supportFragmentManager, "HA#MatrixToBottomSheet") } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt index dbe92d4d93..cee945e202 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt @@ -29,6 +29,7 @@ import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.matrixto.MatrixToBottomSheet +import im.vector.app.features.navigation.Navigator import im.vector.app.features.spaces.explore.SpaceDirectoryArgs import im.vector.app.features.spaces.explore.SpaceDirectoryFragment import im.vector.app.features.spaces.explore.SpaceDirectoryState @@ -51,18 +52,18 @@ class SpaceExploreActivity : VectorBaseActivity(), SpaceD val sharedViewModel: SpaceDirectoryViewModel by viewModel() private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { - override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) { + override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { if (f is MatrixToBottomSheet) { f.interactionListener = this@SpaceExploreActivity } - super.onFragmentAttached(fm, f, context) + super.onFragmentResumed(fm, f) } - override fun onFragmentDetached(fm: FragmentManager, f: Fragment) { + override fun onFragmentPaused(fm: FragmentManager, f: Fragment) { if (f is MatrixToBottomSheet) { f.interactionListener = null } - super.onFragmentDetached(fm, f) + super.onFragmentPaused(fm, f) } } @@ -86,14 +87,14 @@ class SpaceExploreActivity : VectorBaseActivity(), SpaceD sharedViewModel.observeViewEvents { when (it) { - SpaceDirectoryViewEvents.Dismiss -> { + SpaceDirectoryViewEvents.Dismiss -> { finish() } - is SpaceDirectoryViewEvents.NavigateToRoom -> { + is SpaceDirectoryViewEvents.NavigateToRoom -> { navigator.openRoom(this, it.roomId) } is SpaceDirectoryViewEvents.NavigateToMxToBottomSheet -> { - MatrixToBottomSheet.withLink(it.link, this).show(supportFragmentManager, "ShowChild") + MatrixToBottomSheet.withLink(it.link).show(supportFragmentManager, "ShowChild") } } } @@ -115,7 +116,11 @@ class SpaceExploreActivity : VectorBaseActivity(), SpaceD override fun create(initialState: SpaceDirectoryState): SpaceDirectoryViewModel = spaceDirectoryViewModelFactory.create(initialState) - override fun navigateToRoom(roomId: String) { + override fun mxToBottomSheetNavigateToRoom(roomId: String) { navigator.openRoom(this, roomId) } + + override fun mxToBottomSheetSwitchToSpace(spaceId: String) { + navigator.switchToSpace(this, spaceId, Navigator.PostSwitchSpaceAction.None) + } } diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt index 0771a5d238..5e8145168b 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt @@ -24,6 +24,7 @@ import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState @@ -60,8 +61,25 @@ class UserCodeActivity : VectorBaseActivity(), injector.inject(this) } + private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = this@UserCodeActivity + } + super.onFragmentResumed(fm, f) + } + + override fun onFragmentPaused(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = null + } + super.onFragmentPaused(fm, f) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) if (isFirstCreation()) { // should be there early for shared element transition @@ -74,7 +92,7 @@ class UserCodeActivity : VectorBaseActivity(), UserCodeState.Mode.SCAN -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY) is UserCodeState.Mode.RESULT -> { showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) - MatrixToBottomSheet.withLink(mode.rawLink, this).show(supportFragmentManager, "MatrixToBottomSheet") + MatrixToBottomSheet.withLink(mode.rawLink).show(supportFragmentManager, "MatrixToBottomSheet") } } } @@ -97,6 +115,11 @@ class UserCodeActivity : VectorBaseActivity(), } } + override fun onDestroy() { + supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks) + super.onDestroy() + } + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { supportFragmentManager.commitTransaction { @@ -110,10 +133,12 @@ class UserCodeActivity : VectorBaseActivity(), } } - override fun navigateToRoom(roomId: String) { + override fun mxToBottomSheetNavigateToRoom(roomId: String) { navigator.openRoom(this, roomId) } + override fun mxToBottomSheetSwitchToSpace(spaceId: String) {} + override fun onBackPressed() = withState(sharedViewModel) { when (it.mode) { UserCodeState.Mode.SHOW -> super.onBackPressed() From 6b3a407b79f19106cb9e085e2d47bf45493c722c Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 29 Sep 2021 19:21:11 +0200 Subject: [PATCH 15/90] TextComposer: continue reworking. WIP --- .../im/vector/app/core/extensions/EditText.kt | 36 ++++++++ .../home/room/detail/RoomDetailFragment.kt | 83 +++++++++---------- .../home/room/detail/RoomDetailViewState.kt | 21 ----- .../room/detail/composer/ComposerEditText.kt | 9 +- .../detail/composer/TextComposerAction.kt | 2 + .../room/detail/composer/TextComposerView.kt | 35 +++----- .../detail/composer/TextComposerViewEvents.kt | 2 + .../detail/composer/TextComposerViewModel.kt | 58 +++++++++---- .../detail/composer/TextComposerViewState.kt | 30 ++++++- ...composer_layout_constraint_set_compact.xml | 2 +- ...omposer_layout_constraint_set_expanded.xml | 2 +- 11 files changed, 160 insertions(+), 120 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt index 05b70def3d..0eb9dcdaf9 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt @@ -18,6 +18,7 @@ package im.vector.app.core.extensions import android.text.Editable import android.text.InputType +import android.text.Spanned import android.view.MotionEvent import android.view.View import android.view.inputmethod.EditorInfo @@ -57,3 +58,38 @@ fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_searc return@OnTouchListener false }) } + +fun EditText.setTextIfDifferent(newText: CharSequence?): Boolean { + if (!isTextDifferent(newText, text)) { + // Previous text is the same. No op + return false + } + setText(newText) + // Since the text changed we move the cursor to the end of the new text. + // This allows us to fill in text programmatically with a different value, + // but if the user is typing and the view is rebound we won't lose their cursor position. + setSelection(newText?.length ?: 0) + return true +} + +private fun isTextDifferent(str1: CharSequence?, str2: CharSequence?): Boolean { + if (str1 === str2) { + return false + } + if (str1 == null || str2 == null) { + return true + } + val length = str1.length + if (length != str2.length) { + return true + } + if (str1 is Spanned) { + return str1 != str2 + } + for (i in 0 until length) { + if (str1[i] != str2[i]) { + return true + } + } + return false +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 520c0a3784..973dca0496 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -133,6 +133,7 @@ import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.composer.SendMode import im.vector.app.features.home.room.detail.composer.TextComposerAction import im.vector.app.features.home.room.detail.composer.TextComposerView import im.vector.app.features.home.room.detail.composer.TextComposerViewEvents @@ -191,7 +192,6 @@ import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.commonmark.parser.Parser -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.toModel @@ -381,11 +381,11 @@ class RoomDetailFragment @Inject constructor( invalidateOptionsMenu() } - roomDetailViewModel.selectSubscribe(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ -> + roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ -> updateJumpToReadMarkerViewVisibility() } - textComposerViewModel.selectSubscribe(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage) { mode, canSend -> + textComposerViewModel.selectSubscribe(this, TextComposerViewState::sendMode, TextComposerViewState::canSendMessage) { mode, canSend -> if (!canSend) { return@selectSubscribe } @@ -397,8 +397,8 @@ class RoomDetailFragment @Inject constructor( } } - roomDetailViewModel.selectSubscribe( + this, RoomDetailViewState::syncState, RoomDetailViewState::incrementalSyncStatus, RoomDetailViewState::pushCounter @@ -412,11 +412,12 @@ class RoomDetailFragment @Inject constructor( } textComposerViewModel.observeViewEvents { - when(it){ - is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) - is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it) - is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message) - is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) + when (it) { + is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) + is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it) + is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message) + is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) + is TextComposerViewEvents.OnSendButtonVisibilityChanged -> handleOnSendButtonVisibilityChanged(it) }.exhaustive } @@ -467,6 +468,21 @@ class RoomDetailFragment @Inject constructor( } } + private fun handleOnSendButtonVisibilityChanged(event: TextComposerViewEvents.OnSendButtonVisibilityChanged) { + Timber.v("Handle on SendButtonVisibility: $event") + if (event.isVisible) { + views.voiceMessageRecorderView.isVisible = false + views.composerLayout.views.sendButton.alpha = 0f + views.composerLayout.views.sendButton.isVisible = true + views.composerLayout.views.sendButton.animate().alpha(1f).setDuration(150).start() + } else { + views.composerLayout.views.sendButton.isInvisible = true + views.voiceMessageRecorderView.alpha = 0f + views.voiceMessageRecorderView.isVisible = true + views.voiceMessageRecorderView.animate().alpha(1f).setDuration(150).start() + } + } + private fun setupRemoveJitsiWidgetView() { views.removeJitsiWidgetView.onCompleteSliding = { withState(roomDetailViewModel) { @@ -669,8 +685,8 @@ class RoomDetailFragment @Inject constructor( views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback { override fun onVoiceRecordingStarted(): Boolean { return if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) { - views.composerLayout.isInvisible = true roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage) + textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingStateChanged(true)) vibrate(requireContext()) true } else { @@ -680,8 +696,8 @@ class RoomDetailFragment @Inject constructor( } override fun onVoiceRecordingEnded(isCancelled: Boolean) { - views.composerLayout.isInvisible = false roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled)) + textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingStateChanged(false)) } override fun onVoiceRecordingPlaybackModeOn() { @@ -767,7 +783,7 @@ class RoomDetailFragment @Inject constructor( } private fun handleJoinedToAnotherRoom(action: TextComposerViewEvents.JoinRoomCommandSuccess) { - updateComposerText("") + views.composerLayout.setTextIfDifferent("") lockSendButton = false navigator.openRoom(vectorBaseActivity, action.roomId) } @@ -993,8 +1009,7 @@ class RoomDetailFragment @Inject constructor( private fun renderRegularMode(text: String) { autoCompleter.exitSpecialMode() views.composerLayout.collapse() - views.voiceMessageRecorderView.isVisible = text.isBlank() - updateComposerText(text) + views.composerLayout.setTextIfDifferent(text) views.composerLayout.views.sendButton.contentDescription = getString(R.string.send) } @@ -1033,7 +1048,7 @@ class RoomDetailFragment @Inject constructor( false } - updateComposerText(defaultContent) + views.composerLayout.setTextIfDifferent(defaultContent) views.composerLayout.views.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) views.composerLayout.views.sendButton.contentDescription = getString(descriptionRes) @@ -1045,21 +1060,11 @@ class RoomDetailFragment @Inject constructor( // need to do it here also when not using quick reply focusComposerAndShowKeyboard() views.composerLayout.views.composerRelatedMessageImage.isVisible = isImageVisible - views.voiceMessageRecorderView.isVisible = false } } focusComposerAndShowKeyboard() } - private fun updateComposerText(text: String) { - // Do not update if this is the same text to avoid the cursor to move - if (text != views.composerLayout.text.toString()) { - // Ignore update to avoid saving a draft - views.composerLayout.views.composerEditText.setText(text) - views.composerLayout.views.composerEditText.setSelection(views.composerLayout.text?.length ?: 0) - } - } - override fun onResume() { super.onResume() notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId) @@ -1321,17 +1326,9 @@ class RoomDetailFragment @Inject constructor( return sendUri(contentUri) } - override fun onTextBlankStateChanged(isBlank: Boolean) { - if (!views.composerLayout.views.sendButton.isVisible) { - // Animate alpha to prevent overlapping with the animation of the send button - views.voiceMessageRecorderView.alpha = 0f - views.voiceMessageRecorderView.isVisible = true - views.voiceMessageRecorderView.animate().alpha(1f).setDuration(300).start() - } else { - views.voiceMessageRecorderView.isVisible = false - } + override fun onTextChanged(text: CharSequence) { + textComposerViewModel.handle(TextComposerAction.OnTextChanged(text)) } - } } @@ -1393,16 +1390,14 @@ class RoomDetailFragment @Inject constructor( timelineEventController.update(mainState) lazyLoadedViews.inviteView(false)?.isVisible = false if (mainState.tombstoneEvent == null) { + views.composerLayout.isInvisible = !textComposerState.isComposerVisible + views.voiceMessageRecorderView.isVisible = !textComposerState.isSendButtonVisible + views.composerLayout.views.sendButton.isInvisible = !textComposerState.isSendButtonVisible + views.composerLayout.setRoomEncrypted(summary.isEncrypted) + //views.composerLayout.alwaysShowSendButton = false if (textComposerState.canSendMessage) { - if (!views.voiceMessageRecorderView.isActive()) { - views.composerLayout.isVisible = true - views.voiceMessageRecorderView.isVisible = views.composerLayout.text?.isBlank().orFalse() - views.composerLayout.setRoomEncrypted(summary.isEncrypted) - views.notificationAreaView.render(NotificationAreaView.State.Hidden) - views.composerLayout.alwaysShowSendButton = false - } + views.notificationAreaView.render(NotificationAreaView.State.Hidden) } else { - views.hideComposerViews() views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost) } } else { @@ -1468,7 +1463,7 @@ class RoomDetailFragment @Inject constructor( displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } is TextComposerViewEvents.SlashCommandResultOk -> { - updateComposerText("") + views.composerLayout.setTextIfDifferent("") } is TextComposerViewEvents.SlashCommandResultError -> { displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index a1eac83fc8..d63ba0b973 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -30,26 +30,6 @@ import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType -/** - * Describes the current send mode: - * REGULAR: sends the text as a regular message - * QUOTE: User is currently quoting a message - * EDIT: User is currently editing an existing message - * - * Depending on the state the bottom toolbar will change (icons/preview/actions...) - */ -sealed class SendMode(open val text: String) { - data class REGULAR( - override val text: String, - val fromSharing: Boolean, - // This is necessary for forcing refresh on selectSubscribe - private val ts: Long = System.currentTimeMillis() - ) : SendMode(text) - - data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) - data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) - data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) -} sealed class UnreadState { object Unknown : UnreadState() @@ -74,7 +54,6 @@ data class RoomDetailViewState( val asyncRoomSummary: Async = Uninitialized, val activeRoomWidgets: Async> = Uninitialized, val formattedTypingUsers: String? = null, - val sendMode: SendMode = SendMode.REGULAR("", false), val tombstoneEvent: Event? = null, val joinUpgradedRoomAsync: Async = Uninitialized, val syncState: SyncState = SyncState.Idle, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt index 79ff7be441..48fa13e139 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt @@ -37,11 +37,10 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib interface Callback { fun onRichContentSelected(contentUri: Uri): Boolean - fun onTextBlankStateChanged(isBlank: Boolean) + fun onTextChanged(text: CharSequence) } var callback: Callback? = null - private var isBlankText = true override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? { val ic = super.onCreateInputConnection(editorInfo) ?: return null @@ -95,11 +94,7 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib } spanToRemove = null } - // Report blank status of EditText to be able to arrange other elements of the composer - if (s.isBlank() != isBlankText) { - isBlankText = !isBlankText - callback?.onTextBlankStateChanged(isBlankText) - } + callback?.onTextChanged(s.toString()) } } ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt index 7896a009c1..e51d9219e6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt @@ -27,4 +27,6 @@ sealed class TextComposerAction : VectorViewModelAction { data class EnterReplyMode(val eventId: String, val text: String) : TextComposerAction() data class EnterRegularMode(val text: String, val fromSharing: Boolean) : TextComposerAction() data class UserIsTyping(val isTyping: Boolean) : TextComposerAction() + data class OnTextChanged(val text: CharSequence) : TextComposerAction() + data class OnVoiceRecordingStateChanged(val isRecording: Boolean) : TextComposerAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt index eb935f9e75..a033e7366e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt @@ -25,16 +25,14 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.text.toSpannable import androidx.core.view.isInvisible -import androidx.core.view.isVisible -import androidx.transition.AutoTransition import androidx.transition.ChangeBounds import androidx.transition.Fade import androidx.transition.Transition import androidx.transition.TransitionManager import androidx.transition.TransitionSet import im.vector.app.R +import im.vector.app.core.extensions.setTextIfDifferent import im.vector.app.databinding.ComposerLayoutBinding -import org.matrix.android.sdk.api.extensions.orFalse /** * Encapsulate the timeline composer UX. @@ -61,13 +59,6 @@ class TextComposerView @JvmOverloads constructor( val text: Editable? get() = views.composerEditText.text - var alwaysShowSendButton = false - set(value) { - field = value - val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || text?.isNotBlank().orFalse() || value - views.sendButton.isInvisible = !shouldShowSendButton - } - init { inflate(context, R.layout.composer_layout, this) views = ComposerLayoutBinding.bind(this) @@ -79,17 +70,8 @@ class TextComposerView @JvmOverloads constructor( return callback?.onRichContentSelected(contentUri) ?: false } - override fun onTextBlankStateChanged(isBlank: Boolean) { - val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isBlank || alwaysShowSendButton - TransitionManager.endTransitions(this@TextComposerView) - if (views.sendButton.isVisible != shouldShowSendButton) { - TransitionManager.beginDelayedTransition( - this@TextComposerView, - AutoTransition().also { it.duration = 150 } - ) - views.sendButton.isInvisible = !shouldShowSendButton - } - callback?.onTextBlankStateChanged(isBlank) + override fun onTextChanged(text: CharSequence) { + callback?.onTextChanged(text) } } views.composerRelatedMessageCloseButton.setOnClickListener { @@ -114,9 +96,6 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_compact applyNewConstraintSet(animate, transitionComplete) - - val shouldShowSendButton = !views.composerEditText.text.isNullOrEmpty() || alwaysShowSendButton - views.sendButton.isInvisible = !shouldShowSendButton } fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -126,10 +105,14 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) - views.sendButton.isInvisible = false + } + + fun setTextIfDifferent(text: CharSequence?): Boolean{ + return views.composerEditText.setTextIfDifferent(text) } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { + //val wasSendButtonInvisible = views.sendButton.isInvisible if (animate) { configureAndBeginTransition(transitionComplete) } @@ -137,6 +120,8 @@ class TextComposerView @JvmOverloads constructor( it.clone(context, currentConstraintSetId) it.applyTo(this) } + // Might be updated by view state just after, but avoid blinks + //views.sendButton.isInvisible = wasSendButtonInvisible } private fun configureAndBeginTransition(transitionComplete: (() -> Unit)? = null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt index cce33a5b63..8ef1cca2da 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt @@ -22,6 +22,8 @@ import im.vector.app.features.command.Command sealed class TextComposerViewEvents : VectorViewEvents { + data class OnSendButtonVisibilityChanged(val isVisible: Boolean): TextComposerViewEvents() + data class ShowMessage(val message: String) : TextComposerViewEvents() abstract class SendMessageResult : TextComposerViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt index 870f5682e0..8d4dd78c29 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -17,29 +17,20 @@ package im.vector.app.features.home.room.detail.composer import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand -import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.home.room.detail.ChatEffect -import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailFragment -import im.vector.app.features.home.room.detail.SendMode import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.toMessageType import im.vector.app.features.powerlevel.PowerLevelsObservableFactory @@ -74,24 +65,57 @@ class TextComposerViewModel @AssistedInject constructor( private val room = session.getRoom(initialState.roomId)!! + // Keep it out of state to avoid invalidate being called + private var currentComposerText: CharSequence = "" + init { loadDraftIfAny() observePowerLevel() + subscribeToStateInternal() } override fun handle(action: TextComposerAction) { Timber.v("Handle action: $action") when (action) { - is TextComposerAction.EnterEditMode -> handleEnterEditMode(action) - is TextComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) - is TextComposerAction.EnterRegularMode -> handleEnterRegularMode(action) - is TextComposerAction.EnterReplyMode -> handleEnterReplyMode(action) - is TextComposerAction.SaveDraft -> handleSaveDraft(action) - is TextComposerAction.SendMessage -> handleSendMessage(action) - is TextComposerAction.UserIsTyping -> handleUserIsTyping(action) + is TextComposerAction.EnterEditMode -> handleEnterEditMode(action) + is TextComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) + is TextComposerAction.EnterRegularMode -> handleEnterRegularMode(action) + is TextComposerAction.EnterReplyMode -> handleEnterReplyMode(action) + is TextComposerAction.SaveDraft -> handleSaveDraft(action) + is TextComposerAction.SendMessage -> handleSendMessage(action) + is TextComposerAction.UserIsTyping -> handleUserIsTyping(action) + is TextComposerAction.OnTextChanged -> handleOnTextChanged(action) + is TextComposerAction.OnVoiceRecordingStateChanged -> handleOnVoiceRecordingStateChanged(action) } } + private fun handleOnVoiceRecordingStateChanged(action: TextComposerAction.OnVoiceRecordingStateChanged) = setState { + copy(isVoiceRecording = action.isRecording) + } + + private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) { + setState { + // Makes sure currentComposerText is upToDate when accessing further setState + currentComposerText = action.text + this + } + updateIsSendButtonVisibility() + } + + private fun subscribeToStateInternal() { + selectSubscribe(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage, TextComposerViewState::isVoiceRecording) { _, _, _ -> + updateIsSendButtonVisibility() + } + } + + private fun updateIsSendButtonVisibility() = setState { + val isSendButtonVisible = isComposerVisible && (sendMode !is SendMode.REGULAR || currentComposerText.isNotBlank()) + if (this.isSendButtonVisible != isSendButtonVisible) { + _viewEvents.post(TextComposerViewEvents.OnSendButtonVisibilityChanged(isSendButtonVisible)) + } + copy(isSendButtonVisible = isSendButtonVisible) + } + private fun handleEnterRegularMode(action: TextComposerAction.EnterRegularMode) = setState { copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing)) } @@ -442,7 +466,6 @@ class TextComposerViewModel @AssistedInject constructor( } } - private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { // If message is blank, convert to an emote, with default message if (sendChatEffect.message.isBlank()) { @@ -573,7 +596,6 @@ class TextComposerViewModel @AssistedInject constructor( } } - private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) { _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) viewModelScope.launch { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt index 1e118ef38a..2fe9b58b21 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt @@ -18,16 +18,40 @@ package im.vector.app.features.home.room.detail.composer import com.airbnb.mvrx.MvRxState import im.vector.app.features.home.room.detail.RoomDetailArgs -import im.vector.app.features.home.room.detail.SendMode +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +/** + * Describes the current send mode: + * REGULAR: sends the text as a regular message + * QUOTE: User is currently quoting a message + * EDIT: User is currently editing an existing message + * + * Depending on the state the bottom toolbar will change (icons/preview/actions...) + */ +sealed class SendMode(open val text: String) { + data class REGULAR( + override val text: String, + val fromSharing: Boolean, + // This is necessary for forcing refresh on selectSubscribe + private val ts: Long = System.currentTimeMillis() + ) : SendMode(text) + + data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) + data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) + data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) +} data class TextComposerViewState( val roomId: String, val canSendMessage: Boolean = true, - val isSendButtonVisible: Boolean = false, - val isRecordingVoiceMessage: Boolean = false, + val isVoiceRecording: Boolean = false, + val isSendButtonVisible : Boolean = false, val sendMode: SendMode = SendMode.REGULAR("", false), ) : MvRxState { + val isComposerVisible: Boolean + get() = canSendMessage && !isVoiceRecording + constructor(args: RoomDetailArgs) : this( roomId = args.roomId, ) diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml index eae8457121..f42551d39a 100644 --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml @@ -172,7 +172,7 @@ android:contentDescription="@string/send" android:scaleType="center" android:src="@drawable/ic_send" - android:visibility="gone" + android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" tools:ignore="MissingPrefix" diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml index c09b95f6f7..d6a5b57884 100644 --- a/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml @@ -184,7 +184,7 @@ android:contentDescription="@string/send" android:scaleType="center" android:src="@drawable/ic_send" - android:visibility="gone" + android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier" From 2605433a3de7a92081047f44bb12aafe233f9c6e Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 30 Sep 2021 09:15:13 +0200 Subject: [PATCH 16/90] Code review --- .../createroom/CreateRoomController.kt | 8 +++----- .../createroom/CreateRoomViewModel.kt | 19 ++++++++----------- .../createroom/CreateRoomViewState.kt | 4 ++-- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt index 0d3066e43a..a6799ce730 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt @@ -29,6 +29,7 @@ import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditableAvatarItem import im.vector.app.features.form.formSubmitButtonItem import im.vector.app.features.form.formSwitchItem +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import javax.inject.Inject @@ -165,11 +166,8 @@ class CreateRoomController @Inject constructor( host.stringProvider.getString(R.string.create_room_encryption_description) } ) - if (viewState.isEncrypted != null) { - switchChecked(viewState.isEncrypted) - } else { - switchChecked(viewState.defaultEncrypted[viewState.roomJoinRules] ?: false) - } + + switchChecked(viewState.isEncrypted ?: viewState.defaultEncrypted[viewState.roomJoinRules].orFalse()) listener { value -> host.listener?.setIsEncrypted(value) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index 45edc207ba..8ddb6d7a09 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -36,6 +36,7 @@ import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns.getDomain +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session @@ -291,17 +292,13 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init disableFederation = state.disableFederation // Encryption - // we ignore the isEncrypted for public room as the switch is hidden in this case - if (state.roomJoinRules != RoomJoinRules.PUBLIC && state.isEncrypted != null) { - // the user explicitly switch the toggle - if (state.isEncrypted) { - enableEncryption() - } - } else { - // based on default - if (state.defaultEncrypted[state.roomJoinRules] == true) { - enableEncryption() - } + val shouldEncrypt = when (state.roomJoinRules) { + // we ignore the isEncrypted for public room as the switch is hidden in this case + RoomJoinRules.PUBLIC -> false + else -> state.isEncrypted ?: state.defaultEncrypted[state.roomJoinRules].orFalse() + } + if (shouldEncrypt) { + enableEncryption() } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt index 528adb6c63..95edd1efbe 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt @@ -29,6 +29,7 @@ data class CreateRoomViewState( val roomTopic: String = "", val roomJoinRules: RoomJoinRules = RoomJoinRules.INVITE, val isEncrypted: Boolean? = null, + val defaultEncrypted: Map = emptyMap(), val showAdvanced: Boolean = false, val disableFederation: Boolean = false, val homeServerName: String = "", @@ -38,8 +39,7 @@ data class CreateRoomViewState( val parentSpaceSummary: RoomSummary? = null, val supportsRestricted: Boolean = false, val aliasLocalPart: String? = null, - val isSubSpace: Boolean = false, - val defaultEncrypted: Map = emptyMap() + val isSubSpace: Boolean = false ) : MvRxState { constructor(args: CreateRoomArgs) : this( From a171f1912a234b573aa7930c1f6f88fdf94bd12c Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 30 Sep 2021 11:57:57 +0200 Subject: [PATCH 17/90] TextComposer: makes animation ok --- .../features/home/room/detail/RoomDetailFragment.kt | 7 +++---- .../room/detail/composer/TextComposerViewEvents.kt | 2 +- .../home/room/detail/composer/TextComposerViewModel.kt | 10 +++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 973dca0496..c9ec12c4c3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -416,8 +416,8 @@ class RoomDetailFragment @Inject constructor( is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it) is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message) - is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) - is TextComposerViewEvents.OnSendButtonVisibilityChanged -> handleOnSendButtonVisibilityChanged(it) + is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) + is TextComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it) }.exhaustive } @@ -468,8 +468,7 @@ class RoomDetailFragment @Inject constructor( } } - private fun handleOnSendButtonVisibilityChanged(event: TextComposerViewEvents.OnSendButtonVisibilityChanged) { - Timber.v("Handle on SendButtonVisibility: $event") + private fun handleSendButtonVisibilityChanged(event: TextComposerViewEvents.AnimateSendButtonVisibility) { if (event.isVisible) { views.voiceMessageRecorderView.isVisible = false views.composerLayout.views.sendButton.alpha = 0f diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt index 8ef1cca2da..8d0429ee06 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt @@ -22,7 +22,7 @@ import im.vector.app.features.command.Command sealed class TextComposerViewEvents : VectorViewEvents { - data class OnSendButtonVisibilityChanged(val isVisible: Boolean): TextComposerViewEvents() + data class AnimateSendButtonVisibility(val isVisible: Boolean): TextComposerViewEvents() data class ShowMessage(val message: String) : TextComposerViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt index 8d4dd78c29..988e5b697e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -99,19 +99,19 @@ class TextComposerViewModel @AssistedInject constructor( currentComposerText = action.text this } - updateIsSendButtonVisibility() + updateIsSendButtonVisibility(true) } private fun subscribeToStateInternal() { selectSubscribe(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage, TextComposerViewState::isVoiceRecording) { _, _, _ -> - updateIsSendButtonVisibility() + updateIsSendButtonVisibility(false) } } - private fun updateIsSendButtonVisibility() = setState { + private fun updateIsSendButtonVisibility(triggerAnimation: Boolean) = setState { val isSendButtonVisible = isComposerVisible && (sendMode !is SendMode.REGULAR || currentComposerText.isNotBlank()) - if (this.isSendButtonVisible != isSendButtonVisible) { - _viewEvents.post(TextComposerViewEvents.OnSendButtonVisibilityChanged(isSendButtonVisible)) + if (this.isSendButtonVisible != isSendButtonVisible && triggerAnimation) { + _viewEvents.post(TextComposerViewEvents.AnimateSendButtonVisibility(isSendButtonVisible)) } copy(isSendButtonVisible = isSendButtonVisible) } From 4880df35550d233b8002c81b64637d32146605d1 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 30 Sep 2021 11:56:07 +0200 Subject: [PATCH 18/90] Change call to action in filter room when space selected --- changelog.d/3048.bugfix | 1 + .../home/room/filtered/FilteredRoomFooterItem.kt | 11 +++++++++++ .../home/room/list/RoomListFooterController.kt | 2 ++ .../src/main/res/layout/item_room_filter_footer.xml | 2 +- 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3048.bugfix diff --git a/changelog.d/3048.bugfix b/changelog.d/3048.bugfix new file mode 100644 index 0000000000..81fbe7b65e --- /dev/null +++ b/changelog.d/3048.bugfix @@ -0,0 +1 @@ +Room filter no results bad CTA in space mode when a space selected \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt index 5e45004579..4c163f2f56 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.filtered import android.widget.Button +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -33,11 +34,21 @@ abstract class FilteredRoomFooterItem : VectorEpoxyModel { diff --git a/vector/src/main/res/layout/item_room_filter_footer.xml b/vector/src/main/res/layout/item_room_filter_footer.xml index 8df6c53032..91790b3811 100644 --- a/vector/src/main/res/layout/item_room_filter_footer.xml +++ b/vector/src/main/res/layout/item_room_filter_footer.xml @@ -12,6 +12,7 @@ android:layout_gravity="center" android:layout_marginTop="56dp" android:text="@string/room_filtering_footer_title" + android:layout_marginBottom="@dimen/layout_vertical_margin" android:textColor="?vctr_content_secondary" />