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. [Get it on Google Play](https://play.google.com/store/apps/details?id=im.vector.riotx) +[Get it on F-Droid](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", "")