diff --git a/CHANGES.md b/CHANGES.md
index f71a4f8c22..09063bee8f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,9 +7,11 @@ Features ✨:
Improvements 🙌:
- Sharing things to RiotX: sort list by recent room first (#771)
+ - Hide the algorithm when turning on e2e (#897)
+ - Sort room members by display names
Other changes:
- -
+ - Add support for /rainbow and /rainbowme commands (#879)
Bugfix 🐛:
-
diff --git a/README.md b/README.md
index 1848c7baba..b43bcf643c 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ RiotX is an Android Matrix Client currently in beta but in active development.
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. RiotX will become the official replacement as soon as all features are implemented.
[](https://play.google.com/store/apps/details?id=im.vector.riotx)
+[](https://f-droid.org/app/im.vector.riotx)
Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index e510d11efb..ca18c2b56e 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -10,7 +10,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath "io.realm:realm-gradle-plugin:6.0.2"
+ classpath "io.realm:realm-gradle-plugin:6.1.0"
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
index 1ceb45939c..a392ee1e86 100755
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
@@ -69,6 +69,7 @@ import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
import im.vector.matrix.android.internal.database.model.EventEntity
+import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback
@@ -515,14 +516,16 @@ internal class DefaultCryptoService @Inject constructor(
}
/**
- * Tells if a room is encrypted
+ * Tells if a room is encrypted with MXCRYPTO_ALGORITHM_MEGOLM
*
* @param roomId the room id
- * @return true if the room is encrypted
+ * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
*/
override fun isRoomEncrypted(roomId: String): Boolean {
- val encryptionEvent = monarchy.fetchCopied {
- EventEntity.where(it, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION).findFirst()
+ val encryptionEvent = monarchy.fetchCopied { realm ->
+ EventEntity.where(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
+ .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
+ .findFirst()
}
return encryptionEvent != null
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt
index bbb5feba15..e65a1eb73e 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt
@@ -23,12 +23,18 @@ import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
+import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.database.mapper.ContentMapper
-import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.EventEntity
+import im.vector.matrix.android.internal.database.model.EventEntityFields
+import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
-import im.vector.matrix.android.internal.database.query.*
+import im.vector.matrix.android.internal.database.query.getOrCreate
+import im.vector.matrix.android.internal.database.query.isEventRead
+import im.vector.matrix.android.internal.database.query.latestEvent
+import im.vector.matrix.android.internal.database.query.prev
+import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
@@ -92,7 +98,9 @@ internal class RoomSummaryUpdater @Inject constructor(
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev()
val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
- val encryptionEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ENCRYPTION).prev()
+ val encryptionEvent = EventEntity.where(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
+ .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
+ .prev()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
// avoid this call if we are sure there are unread events
diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
index 6a81b9d3c0..dae8271d66 100644
--- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
+++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
@@ -1,4 +1,8 @@
+
+ %1$s turned on end-to-end encryption.
+ %1$s turned on end-to-end encryption (unrecognised algorithm %2$s).
+
%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.
diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt b/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt
index f9e5654726..e91a2896bc 100644
--- a/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt
+++ b/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt
@@ -113,3 +113,39 @@ fun containsOnlyEmojis(str: String?): Boolean {
return res
}
+
+/**
+ * Same as split, but considering emojis
+ */
+fun CharSequence.splitEmoji(): List {
+ val result = mutableListOf()
+
+ var index = 0
+
+ while (index < length) {
+ val firstChar = get(index)
+
+ if (firstChar.toInt() == 0x200e) {
+ // Left to right mark. What should I do with it?
+ } else if (firstChar.toInt() in 0xD800..0xDBFF && index + 1 < length) {
+ // We have the start of a surrogate pair
+ val secondChar = get(index + 1)
+
+ if (secondChar.toInt() in 0xDC00..0xDFFF) {
+ // We have an emoji
+ result.add("$firstChar$secondChar")
+ index++
+ } else {
+ // Not sure what we have here...
+ result.add("$firstChar")
+ }
+ } else {
+ // Regular char
+ result.add("$firstChar")
+ }
+
+ index++
+ }
+
+ return result
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/command/Command.kt b/vector/src/main/java/im/vector/riotx/features/command/Command.kt
index 776e8385ad..5c96b7c93c 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/Command.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/Command.kt
@@ -37,6 +37,8 @@ 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),
MARKDOWN("/markdown", "", R.string.command_description_markdown),
+ RAINBOW("/rainbow", "", R.string.command_description_rainbow),
+ RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote),
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token),
SPOILER("/spoiler", "", R.string.command_description_spoiler),
SHRUG("/shrug", "", R.string.command_description_shrug),
diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
index dcdb7ad8a2..d4f5010d7e 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
@@ -80,6 +80,16 @@ object CommandParser {
ParsedCommand.SendEmote(message)
}
+ Command.RAINBOW.command -> {
+ val message = textMessage.subSequence(Command.RAINBOW.command.length, textMessage.length).trim()
+
+ ParsedCommand.SendRainbow(message)
+ }
+ Command.RAINBOW_EMOTE.command -> {
+ val message = textMessage.subSequence(Command.RAINBOW_EMOTE.command.length, textMessage.length).trim()
+
+ ParsedCommand.SendRainbowEmote(message)
+ }
Command.JOIN_ROOM.command -> {
if (messageParts.size >= 2) {
val roomAlias = messageParts[1]
diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
index b16f68c7b9..dd9fe32e09 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
@@ -34,6 +34,8 @@ sealed class ParsedCommand {
// Valid commands:
class SendEmote(val message: CharSequence) : ParsedCommand()
+ class SendRainbow(val message: CharSequence) : 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 SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand()
diff --git a/vector/src/main/java/im/vector/riotx/features/form/FormSwitchItem.kt b/vector/src/main/java/im/vector/riotx/features/form/FormSwitchItem.kt
index 2e48a8b709..1f3c7c81bb 100644
--- a/vector/src/main/java/im/vector/riotx/features/form/FormSwitchItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/form/FormSwitchItem.kt
@@ -44,6 +44,12 @@ abstract class FormSwitchItem : VectorEpoxyModel() {
var summary: String? = null
override fun bind(holder: Holder) {
+ holder.view.setOnClickListener {
+ if (enabled) {
+ holder.switchView.toggle()
+ }
+ }
+
holder.titleView.text = title
holder.summaryView.setTextOrHide(summary)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
index f6a4717c78..94dbcc8057 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
@@ -56,6 +56,7 @@ import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventConten
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
+import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
@@ -64,6 +65,7 @@ import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.core.utils.subscribeLogError
import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand
+import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.riotx.features.crypto.verification.supportedVerificationMethods
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotx.features.home.room.typing.TypingHelper
@@ -84,6 +86,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private val vectorPreferences: VectorPreferences,
private val stringProvider: StringProvider,
private val typingHelper: TypingHelper,
+ private val rainbowGenerator: RainbowGenerator,
private val session: Session
) : VectorViewModel(initialState), Timeline.Listener {
@@ -390,6 +393,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
popDraft()
}
+ is ParsedCommand.SendRainbow -> {
+ slashCommandResult.message.toString().let {
+ room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
+ }
+ _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
+ popDraft()
+ }
+ is ParsedCommand.SendRainbowEmote -> {
+ slashCommandResult.message.toString().let {
+ room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
+ }
+ _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
+ popDraft()
+ }
is ParsedCommand.SendSpoiler -> {
room.sendFormattedTextMessage(
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
@@ -423,7 +440,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
}
- }
+ }.exhaustive
}
is SendMode.EDIT -> {
// is original event a reply?
@@ -481,7 +498,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
popDraft()
}
}
- }
+ }.exhaustive
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/rainbow/RainbowGenerator.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/rainbow/RainbowGenerator.kt
new file mode 100644
index 0000000000..3868be4e2e
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/rainbow/RainbowGenerator.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2020 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.riotx.features.home.room.detail.composer.rainbow
+
+import im.vector.riotx.core.utils.splitEmoji
+import javax.inject.Inject
+import kotlin.math.abs
+import kotlin.math.roundToInt
+
+/**
+ * Inspired from React-Sdk
+ * Ref: https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/utils/colour.js
+ */
+class RainbowGenerator @Inject constructor() {
+
+ fun generate(text: String): String {
+ val split = text.splitEmoji()
+ val frequency = 360f / split.size
+
+ return split
+ .mapIndexed { idx, letter ->
+ // Do better than React-Sdk: Avoid adding font color for spaces
+ if (letter == " ") {
+ "$letter"
+ } else {
+ val dashColor = hueToRGB(idx * frequency, 1.0f, 0.5f).toDashColor()
+ "$letter"
+ }
+ }
+ .joinToString(separator = "")
+ }
+
+ private fun hueToRGB(h: Float, s: Float, l: Float): RgbColor {
+ val c = s * (1 - abs(2 * l - 1))
+ val x = c * (1 - abs((h / 60) % 2 - 1))
+ val m = l - c / 2
+
+ var r = 0f
+ var g = 0f
+ var b = 0f
+
+ when {
+ h < 60f -> {
+ r = c
+ g = x
+ }
+ h < 120f -> {
+ r = x
+ g = c
+ }
+ h < 180f -> {
+ g = c
+ b = x
+ }
+ h < 240f -> {
+ g = x
+ b = c
+ }
+ h < 300f -> {
+ r = x
+ b = c
+ }
+ else -> {
+ r = c
+ b = x
+ }
+ }
+
+ return RgbColor(
+ ((r + m) * 255).roundToInt(),
+ ((g + m) * 255).roundToInt(),
+ ((b + m) * 255).roundToInt()
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/rainbow/RgbColor.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/rainbow/RgbColor.kt
new file mode 100644
index 0000000000..bf2e808a36
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/rainbow/RgbColor.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 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.riotx.features.home.room.detail.composer.rainbow
+
+data class RgbColor(
+ val r: Int,
+ val g: Int,
+ val b: Int
+)
+
+fun RgbColor.toDashColor(): String {
+ return listOf(r, g, b)
+ .joinToString(separator = "", prefix = "#") {
+ it.toString(16).padStart(2, '0')
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
index f90dbed95e..bdfdb02be1 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
@@ -46,17 +46,14 @@ class MessageActionsEpoxyController @Inject constructor(
override fun buildModels(state: MessageActionState) {
// Message preview
- val body = state.messageBody
- if (body != null) {
- bottomSheetMessagePreviewItem {
- id("preview")
- avatarRenderer(avatarRenderer)
- matrixItem(state.informationData.matrixItem)
- movementMethod(createLinkMovementMethod(listener))
- userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) }
- body(body.linkify(listener))
- time(state.time())
- }
+ bottomSheetMessagePreviewItem {
+ id("preview")
+ avatarRenderer(avatarRenderer)
+ matrixItem(state.informationData.matrixItem)
+ movementMethod(createLinkMovementMethod(listener))
+ userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) }
+ body(state.messageBody.linkify(listener))
+ time(state.time())
}
// Send state
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index a20d17cadf..661e7fb416 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -15,7 +15,12 @@
*/
package im.vector.riotx.features.home.room.detail.timeline.action
-import com.airbnb.mvrx.*
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.FragmentViewModelContext
+import com.airbnb.mvrx.MvRxState
+import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.Uninitialized
+import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import dagger.Lazy
@@ -42,7 +47,8 @@ import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.reactions.data.EmojiDataSource
import im.vector.riotx.features.settings.VectorPreferences
import java.text.SimpleDateFormat
-import java.util.*
+import java.util.Date
+import java.util.Locale
/**
* Quick reactions state
@@ -57,7 +63,7 @@ data class MessageActionState(
val eventId: String,
val informationData: MessageInformationData,
val timelineEvent: Async = Uninitialized,
- val messageBody: CharSequence? = null,
+ val messageBody: CharSequence = "",
// For quick reactions
val quickStates: Async> = Uninitialized,
// For actions
@@ -152,13 +158,16 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun observeTimelineEventState() {
asyncSubscribe(MessageActionState::timelineEvent) { timelineEvent ->
- val computedMessage = computeMessageBody(timelineEvent)
- val actions = actionsForEvent(timelineEvent)
- setState { copy(messageBody = computedMessage, actions = actions) }
+ setState {
+ copy(
+ messageBody = computeMessageBody(timelineEvent),
+ actions = actionsForEvent(timelineEvent)
+ )
+ }
}
}
- private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence? {
+ private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence {
return when (timelineEvent.root.getClearType()) {
EventType.MESSAGE,
EventType.STICKER -> {
@@ -188,7 +197,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
noticeEventFormatter.format(timelineEvent)
}
else -> null
- }
+ } ?: ""
}
private fun actionsForEvent(timelineEvent: TimelineEvent): List {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt
index d9bed98b1f..89e21e04a2 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt
@@ -16,10 +16,13 @@
package im.vector.riotx.features.home.room.detail.timeline.factory
+import android.view.View
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
-import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
+import im.vector.riotx.R
+import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
+import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem_
@@ -28,20 +31,26 @@ import javax.inject.Inject
class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: AvatarSizeProvider,
private val avatarRenderer: AvatarRenderer,
+ private val stringProvider: StringProvider,
private val informationDataFactory: MessageInformationDataFactory) {
fun create(text: String,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): DefaultItem {
+ val attributes = DefaultItem.Attributes(
+ avatarRenderer = avatarRenderer,
+ informationData = informationData,
+ text = text,
+ itemLongClickListener = View.OnLongClickListener { view ->
+ callback?.onEventLongClicked(informationData, null, view) ?: false
+ },
+ readReceiptsCallback = callback
+ )
return DefaultItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight)
- .text(text)
- .avatarRenderer(avatarRenderer)
- .informationData(informationData)
- .baseCallback(callback)
- .readReceiptsCallback(callback)
+ .attributes(attributes)
}
fun create(event: TimelineEvent,
@@ -49,9 +58,9 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
callback: TimelineEventController.Callback?,
throwable: Throwable? = null): DefaultItem {
val text = if (throwable == null) {
- "${event.root.getClearType()} events are not yet handled"
+ stringProvider.getString(R.string.rendering_event_error_type_of_event_not_handled, event.root.getClearType())
} else {
- "an exception occurred when rendering the event ${event.root.eventId}"
+ stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId)
}
val informationData = informationDataFactory.create(event, null)
return create(text, informationData, highlight, callback)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 3cf6b46b4f..086dfe3754 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -27,7 +27,17 @@ import dagger.Lazy
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
-import im.vector.matrix.android.api.session.room.model.message.*
+import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
+import im.vector.matrix.android.api.session.room.model.message.MessageContent
+import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
+import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
+import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
+import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
+import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
+import im.vector.matrix.android.api.session.room.model.message.MessageType
+import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
+import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
+import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
@@ -41,8 +51,26 @@ import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.core.utils.containsOnlyEmojis
import im.vector.riotx.core.utils.isLocalFile
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
-import im.vector.riotx.features.home.room.detail.timeline.helper.*
-import im.vector.riotx.features.home.room.detail.timeline.item.*
+import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
+import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
+import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
+import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
+import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
+import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem
+import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem
+import im.vector.riotx.features.home.room.detail.timeline.item.MessageBlockCodeItem
+import im.vector.riotx.features.home.room.detail.timeline.item.MessageBlockCodeItem_
+import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
+import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem_
+import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
+import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem_
+import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
+import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
+import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
+import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem
+import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem_
+import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestItem
+import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestItem_
import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.riotx.features.home.room.detail.timeline.tools.linkify
import im.vector.riotx.features.html.CodeVisitor
@@ -201,7 +229,7 @@ class MessageItemFactory @Inject constructor(
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): DefaultItem? {
- val text = "${messageContent.type} message events are not yet handled"
+ val text = stringProvider.getString(R.string.rendering_event_error_type_of_message_not_handled, messageContent.type)
return defaultItemFactory.create(text, informationData, highlight, callback)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
index 0fbcb55b01..8d70279fce 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
@@ -33,6 +33,7 @@ import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
@@ -198,7 +199,11 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
private fun formatRoomEncryptionEvent(event: Event, senderName: String?): CharSequence? {
val content = event.content.toModel() ?: return null
- return sp.getString(R.string.notice_end_to_end, senderName, content.algorithm)
+ return if (content.algorithm == MXCRYPTO_ALGORITHM_MEGOLM) {
+ sp.getString(R.string.notice_end_to_end_ok, senderName)
+ } else {
+ sp.getString(R.string.notice_end_to_end_unknown_algorithm, senderName, content.algorithm)
+ }
}
private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt
index 02b7341c72..f674cfa0f4 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt
@@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View
import android.view.ViewStub
import android.widget.RelativeLayout
+import androidx.annotation.CallSuper
import androidx.annotation.IdRes
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
@@ -42,6 +43,7 @@ abstract class BaseEventItem : VectorEpoxyModel
@EpoxyAttribute
lateinit var dimensionConverter: DimensionConverter
+ @CallSuper
override fun bind(holder: H) {
super.bind(holder)
holder.leftGuideline.updateLayoutParams {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt
index dc52293292..0ccc982c4c 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt
@@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View
+import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
@@ -29,42 +30,39 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle
abstract class DefaultItem : BaseEventItem() {
@EpoxyAttribute
- lateinit var informationData: MessageInformationData
- @EpoxyAttribute
- lateinit var avatarRenderer: AvatarRenderer
- @EpoxyAttribute
- var baseCallback: TimelineEventController.BaseCallback? = null
-
- private var longClickListener = View.OnLongClickListener {
- return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true
- }
-
- @EpoxyAttribute
- var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
+ lateinit var attributes: Attributes
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
- readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts)
+ attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
})
- @EpoxyAttribute
- var text: CharSequence? = null
-
override fun bind(holder: Holder) {
- holder.messageView.text = text
- holder.view.setOnLongClickListener(longClickListener)
- holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)
+ super.bind(holder)
+ holder.messageTextView.text = attributes.text
+ attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
+ holder.view.setOnLongClickListener(attributes.itemLongClickListener)
+ holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
}
override fun getEventIds(): List {
- return listOf(informationData.eventId)
+ return listOf(attributes.informationData.eventId)
}
override fun getViewType() = STUB_ID
class Holder : BaseHolder(STUB_ID) {
- val messageView by bind(R.id.stateMessageView)
+ val avatarImageView by bind(R.id.itemDefaultAvatarView)
+ val messageTextView by bind(R.id.itemDefaultTextView)
}
+ data class Attributes(
+ val avatarRenderer: AvatarRenderer,
+ val informationData: MessageInformationData,
+ val text: CharSequence,
+ val itemLongClickListener: View.OnLongClickListener? = null,
+ val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
+ )
+
companion object {
private const val STUB_ID = R.id.messageContentDefaultStub
}
diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomAction.kt
index 333834ca3c..8986db180a 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomAction.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomAction.kt
@@ -22,5 +22,6 @@ sealed class CreateRoomAction : VectorViewModelAction {
data class SetName(val name: String) : CreateRoomAction()
data class SetIsPublic(val isPublic: Boolean) : CreateRoomAction()
data class SetIsInRoomDirectory(val isInRoomDirectory: Boolean) : CreateRoomAction()
+ data class SetIsEncrypted(val isEncrypted: Boolean) : CreateRoomAction()
object Create : CreateRoomAction()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt
index 2477e6fab0..92e178c628 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt
@@ -39,9 +39,7 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
var index = 0
override fun buildModels(viewState: CreateRoomViewState) {
- val asyncCreateRoom = viewState.asyncCreateRoomRequest
-
- when (asyncCreateRoom) {
+ when (val asyncCreateRoom = viewState.asyncCreateRoomRequest) {
is Success -> {
// Nothing to display, the screen will be closed
}
@@ -101,12 +99,24 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
listener?.setIsInRoomDirectory(value)
}
}
+ formSwitchItem {
+ id("encryption")
+ enabled(enableFormElement)
+ title(stringProvider.getString(R.string.create_room_encryption_title))
+ summary(stringProvider.getString(R.string.create_room_encryption_description))
+ switchChecked(viewState.isEncrypted)
+
+ listener { value ->
+ listener?.setIsEncrypted(value)
+ }
+ }
}
interface Listener {
fun onNameChange(newName: String)
fun setIsPublic(isPublic: Boolean)
fun setIsInRoomDirectory(isInRoomDirectory: Boolean)
+ fun setIsEncrypted(isEncrypted: Boolean)
fun retry()
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt
index aacc21916a..827db96783 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt
@@ -85,6 +85,10 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C
viewModel.handle(CreateRoomAction.SetIsInRoomDirectory(isInRoomDirectory))
}
+ override fun setIsEncrypted(isEncrypted: Boolean) {
+ viewModel.handle(CreateRoomAction.SetIsEncrypted(isEncrypted))
+ }
+
override fun retry() {
Timber.v("Retry")
viewModel.handle(CreateRoomAction.Create)
diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
index 31f4d176e4..6c750af5ac 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
@@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
+import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
@@ -62,6 +63,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
is CreateRoomAction.SetName -> setName(action)
is CreateRoomAction.SetIsPublic -> setIsPublic(action)
is CreateRoomAction.SetIsInRoomDirectory -> setIsInRoomDirectory(action)
+ is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action)
is CreateRoomAction.Create -> doCreateRoom()
}
}
@@ -72,6 +74,8 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
private fun setIsInRoomDirectory(action: CreateRoomAction.SetIsInRoomDirectory) = setState { copy(isInRoomDirectory = action.isInRoomDirectory) }
+ private fun setIsEncrypted(action: CreateRoomAction.SetIsEncrypted) = setState { copy(isEncrypted = action.isEncrypted) }
+
private fun doCreateRoom() = withState { state ->
if (state.asyncCreateRoomRequest is Loading || state.asyncCreateRoomRequest is Success) {
return@withState
@@ -87,7 +91,10 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE,
// Public room
preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT
- )
+ ).let {
+ // Encryption
+ if (state.isEncrypted) it.enableEncryptionWithAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM) else it
+ }
session.createRoom(createRoomParams, object : MatrixCallback {
override fun onSuccess(data: String) {
diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewState.kt
index 363d31edc6..810319d54f 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewState.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewState.kt
@@ -24,5 +24,6 @@ data class CreateRoomViewState(
val roomName: String = "",
val isPublic: Boolean = false,
val isInRoomDirectory: Boolean = false,
+ val isEncrypted: Boolean = false,
val asyncCreateRoomRequest: Async = Uninitialized
) : MvRxState
diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
index 8472d4a2a5..2556e3b78c 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
@@ -40,6 +40,7 @@ import io.reactivex.Observable
import io.reactivex.functions.BiFunction
class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomMemberListViewState,
+ private val roomMemberSummaryComparator: RoomMemberSummaryComparator,
private val session: Session)
: VectorViewModel(initialState) {
@@ -113,11 +114,11 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
}
return listOf(
- PowerLevelCategory.ADMIN to admins,
- PowerLevelCategory.MODERATOR to moderators,
- PowerLevelCategory.CUSTOM to customs,
- PowerLevelCategory.INVITE to invites,
- PowerLevelCategory.USER to users
+ PowerLevelCategory.ADMIN to admins.sortedWith(roomMemberSummaryComparator),
+ PowerLevelCategory.MODERATOR to moderators.sortedWith(roomMemberSummaryComparator),
+ PowerLevelCategory.CUSTOM to customs.sortedWith(roomMemberSummaryComparator),
+ PowerLevelCategory.INVITE to invites.sortedWith(roomMemberSummaryComparator),
+ PowerLevelCategory.USER to users.sortedWith(roomMemberSummaryComparator)
)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberSummaryComparator.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberSummaryComparator.kt
new file mode 100644
index 0000000000..cc1dd29d13
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberSummaryComparator.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2020 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.riotx.features.roomprofile.members
+
+import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
+import javax.inject.Inject
+
+class RoomMemberSummaryComparator @Inject constructor() : Comparator {
+
+ override fun compare(leftRoomMemberSummary: RoomMemberSummary?, rightRoomMemberSummary: RoomMemberSummary?): Int {
+ return when (leftRoomMemberSummary) {
+ null ->
+ when (rightRoomMemberSummary) {
+ null -> 0
+ else -> 1
+ }
+ else ->
+ when (rightRoomMemberSummary) {
+ null -> -1
+ else ->
+ when {
+ leftRoomMemberSummary.displayName.isNullOrBlank() ->
+ when {
+ rightRoomMemberSummary.displayName.isNullOrBlank() -> {
+ // No display names, compare ids
+ leftRoomMemberSummary.userId.compareTo(rightRoomMemberSummary.userId)
+ }
+ else -> 1
+ }
+ else ->
+ when {
+ rightRoomMemberSummary.displayName.isNullOrBlank() -> -1
+ else -> {
+ when (leftRoomMemberSummary.displayName) {
+ rightRoomMemberSummary.displayName ->
+ // Same display name, compare id
+ leftRoomMemberSummary.userId.compareTo(rightRoomMemberSummary.userId)
+ else ->
+ leftRoomMemberSummary.displayName!!.compareTo(rightRoomMemberSummary.displayName!!, true)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/vector/src/main/res/layout/item_form_switch.xml b/vector/src/main/res/layout/item_form_switch.xml
index 5757c4b853..3583ac8024 100644
--- a/vector/src/main/res/layout/item_form_switch.xml
+++ b/vector/src/main/res/layout/item_form_switch.xml
@@ -5,6 +5,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
+ android:foreground="?attr/selectableItemBackground"
android:minHeight="@dimen/item_form_min_height">
+ android:layout="@layout/item_timeline_event_default_stub"
+ tools:layout_marginTop="80dp"
+ tools:visibility="visible" />
+ android:layout="@layout/item_timeline_event_merged_header_stub"
+ tools:layout_marginTop="160dp"
+ tools:visibility="visible" />
diff --git a/vector/src/main/res/layout/item_timeline_event_default_stub.xml b/vector/src/main/res/layout/item_timeline_event_default_stub.xml
index 345bda0b7e..68c8936b32 100644
--- a/vector/src/main/res/layout/item_timeline_event_default_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_default_stub.xml
@@ -1,12 +1,31 @@
-
\ No newline at end of file
+ android:orientation="horizontal">
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_notice_stub.xml b/vector/src/main/res/layout/item_timeline_event_notice_stub.xml
index 76190062b1..9aacf357f1 100644
--- a/vector/src/main/res/layout/item_timeline_event_notice_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_notice_stub.xml
@@ -3,8 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:orientation="horizontal"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ tools:text="@string/notice_avatar_url_changed" />
\ No newline at end of file
diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
index e15f6a4149..bf6bcf40d3 100644
--- a/vector/src/main/res/values/strings_riotX.xml
+++ b/vector/src/main/res/values/strings_riotX.xml
@@ -6,6 +6,9 @@
Prepends ¯\\_(ツ)_/¯ to a plain-text message
+ "Enable encryption"
+ "Once enabled, encryption cannot be disabled."
+
Your email domain is not authorized to register on this server
Untrusted sign in
@@ -82,11 +85,18 @@
Jump to read receipt
+ "RiotX does not handle events of type '%1$s' (yet)"
+ "RiotX does not handle message of type '%1$s' (yet)"
+ "RiotX encountered an issue when rendering content of event with id '%1$s'"
+
Unignore
Recent rooms
Other rooms
+ Sends the given message colored as a rainbow
+ Sends the given emote colored as a rainbow
+
Timeline
diff --git a/vector/src/test/java/im/vector/riotx/features/home/room/detail/composer/rainbow/RainbowGeneratorTest.kt b/vector/src/test/java/im/vector/riotx/features/home/room/detail/composer/rainbow/RainbowGeneratorTest.kt
new file mode 100644
index 0000000000..5a9fdc0ab7
--- /dev/null
+++ b/vector/src/test/java/im/vector/riotx/features/home/room/detail/composer/rainbow/RainbowGeneratorTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2020 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.riotx.features.home.room.detail.composer.rainbow
+
+import im.vector.riotx.test.trimIndentOneLine
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@Suppress("SpellCheckingInspection")
+class RainbowGeneratorTest {
+
+ private val rainbowGenerator = RainbowGenerator()
+
+ @Test
+ fun testEmpty() {
+ assertEquals("", rainbowGenerator.generate(""))
+ }
+
+ @Test
+ fun testAscii1() {
+ assertEquals("""a""", rainbowGenerator.generate("a"))
+ }
+
+ @Test
+ fun testAscii2() {
+ val expected = """
+ a
+ b
+ """.trimIndentOneLine()
+
+ assertEquals(expected, rainbowGenerator.generate("ab"))
+ }
+
+ @Test
+ fun testAscii3() {
+ val expected = """
+ T
+ h
+ i
+ s
+
+ i
+ s
+
+ a
+
+ r
+ a
+ i
+ n
+ b
+ o
+ w
+ !
+ """.trimIndentOneLine()
+
+ assertEquals(expected, rainbowGenerator.generate("This is a rainbow!"))
+ }
+
+ @Test
+ fun testEmoji1() {
+ assertEquals("""🤞""", rainbowGenerator.generate("\uD83E\uDD1E")) // 🤞
+ }
+
+ @Test
+ fun testEmoji2() {
+ assertEquals("""🤞""", rainbowGenerator.generate("🤞"))
+ }
+
+ @Test
+ fun testEmoji3() {
+ val expected = """
+ 🤞
+ 🙂
+ """.trimIndentOneLine()
+
+ assertEquals(expected, rainbowGenerator.generate("🤞🙂"))
+ }
+
+ @Test
+ fun testEmojiMix1() {
+ val expected = """
+ H
+ e
+ l
+ l
+ o
+
+ 🤞
+
+ w
+ o
+ r
+ l
+ d
+ !
+ """.trimIndentOneLine()
+
+ assertEquals(expected, rainbowGenerator.generate("Hello 🤞 world!"))
+ }
+
+ @Test
+ fun testEmojiMix2() {
+ val expected = """
+ a
+ 🤞
+ """.trimIndentOneLine()
+
+ assertEquals(expected, rainbowGenerator.generate("a🤞"))
+ }
+
+ @Test
+ fun testEmojiMix3() {
+ val expected = """
+ 🤞
+ a
+ """.trimIndentOneLine()
+
+ assertEquals(expected, rainbowGenerator.generate("🤞a"))
+ }
+
+ @Test
+ fun testError1() {
+ assertEquals("\uD83E", rainbowGenerator.generate("\uD83E"))
+ }
+}
diff --git a/vector/src/test/java/im/vector/riotx/test/Extensions.kt b/vector/src/test/java/im/vector/riotx/test/Extensions.kt
new file mode 100644
index 0000000000..31781ce00e
--- /dev/null
+++ b/vector/src/test/java/im/vector/riotx/test/Extensions.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 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.riotx.test
+
+fun String.trimIndentOneLine() = trimIndent().replace("\n", "")