mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 18:35:40 +03:00
Merge branch 'develop' into cross_signing
This commit is contained in:
commit
5c547794f2
35 changed files with 623 additions and 88 deletions
|
@ -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 🐛:
|
||||
-
|
||||
|
|
|
@ -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.
|
||||
|
||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx)
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="60">](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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="notice_end_to_end_ok">%1$s turned on end-to-end encryption.</string>
|
||||
<string name="notice_end_to_end_unknown_algorithm">%1$s turned on end-to-end encryption (unrecognised algorithm %2$s).</string>
|
||||
|
||||
<string name="key_verification_request_fallback_message">%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.</string>
|
||||
</resources>
|
||||
|
|
|
@ -113,3 +113,39 @@ fun containsOnlyEmojis(str: String?): Boolean {
|
|||
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as split, but considering emojis
|
||||
*/
|
||||
fun CharSequence.splitEmoji(): List<CharSequence> {
|
||||
val result = mutableListOf<CharSequence>()
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -37,6 +37,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
|
|||
KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user),
|
||||
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick),
|
||||
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown),
|
||||
RAINBOW("/rainbow", "<message>", R.string.command_description_rainbow),
|
||||
RAINBOW_EMOTE("/rainbowme", "<message>", R.string.command_description_rainbow_emote),
|
||||
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token),
|
||||
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler),
|
||||
SHRUG("/shrug", "<message>", R.string.command_description_shrug),
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -44,6 +44,12 @@ abstract class FormSwitchItem : VectorEpoxyModel<FormSwitchItem.Holder>() {
|
|||
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)
|
||||
|
||||
|
|
|
@ -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<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
"<font color=\"$dashColor\">$letter</font>"
|
||||
}
|
||||
}
|
||||
.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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<TimelineEvent> = Uninitialized,
|
||||
val messageBody: CharSequence? = null,
|
||||
val messageBody: CharSequence = "",
|
||||
// For quick reactions
|
||||
val quickStates: Async<List<ToggleState>> = 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<EventSharedAction> {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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<EncryptionEventContent>() ?: 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 {
|
||||
|
|
|
@ -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<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||
@EpoxyAttribute
|
||||
lateinit var dimensionConverter: DimensionConverter
|
||||
|
||||
@CallSuper
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
holder.leftGuideline.updateLayoutParams<RelativeLayout.LayoutParams> {
|
||||
|
|
|
@ -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<DefaultItem.Holder>() {
|
||||
|
||||
@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<String> {
|
||||
return listOf(informationData.eventId)
|
||||
return listOf(attributes.informationData.eventId)
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
class Holder : BaseHolder(STUB_ID) {
|
||||
val messageView by bind<TextView>(R.id.stateMessageView)
|
||||
val avatarImageView by bind<ImageView>(R.id.itemDefaultAvatarView)
|
||||
val messageTextView by bind<TextView>(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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<String> {
|
||||
override fun onSuccess(data: String) {
|
||||
|
|
|
@ -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<String> = Uninitialized
|
||||
) : MvRxState
|
||||
|
|
|
@ -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<RoomMemberListViewState, RoomMemberListAction, EmptyViewEvents>(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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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<RoomMemberSummary> {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
||||
<TextView
|
||||
|
|
|
@ -36,8 +36,9 @@
|
|||
<ViewStub
|
||||
android:id="@+id/messageContentDefaultStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:inflatedId="@+id/stateMessageView"
|
||||
android:layout="@layout/item_timeline_event_default_stub" />
|
||||
android:layout="@layout/item_timeline_event_default_stub"
|
||||
tools:layout_marginTop="80dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentBlankStub"
|
||||
|
@ -49,7 +50,9 @@
|
|||
<ViewStub
|
||||
android:id="@+id/messageContentMergedHeaderStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_merged_header_stub" />
|
||||
android:layout="@layout/item_timeline_event_merged_header_stub"
|
||||
tools:layout_marginTop="160dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
|
|
@ -1,12 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/stateMessageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="8dp"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="italic"
|
||||
tools:text="Mon item" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemDefaultAvatarView"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemDefaultTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="italic"
|
||||
tools:text="@string/rendering_event_error_type_of_event_not_handled" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemNoticeAvatarView"
|
||||
|
@ -15,16 +15,16 @@
|
|||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:layout_gravity="top"
|
||||
android:id="@+id/itemNoticeTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="italic"
|
||||
tools:text="John doe changed their avatar" />
|
||||
tools:text="@string/notice_avatar_url_changed" />
|
||||
|
||||
</LinearLayout>
|
|
@ -6,6 +6,9 @@
|
|||
<string name="command_description_shrug">Prepends ¯\\_(ツ)_/¯ to a plain-text message</string>
|
||||
|
||||
|
||||
<string name="create_room_encryption_title">"Enable encryption"</string>
|
||||
<string name="create_room_encryption_description">"Once enabled, encryption cannot be disabled."</string>
|
||||
|
||||
<string name="login_error_threepid_denied">Your email domain is not authorized to register on this server</string>
|
||||
|
||||
<string name="verification_conclusion_warning">Untrusted sign in</string>
|
||||
|
@ -82,11 +85,18 @@
|
|||
|
||||
<string name="room_member_jump_to_read_receipt">Jump to read receipt</string>
|
||||
|
||||
<string name="rendering_event_error_type_of_event_not_handled">"RiotX does not handle events of type '%1$s' (yet)"</string>
|
||||
<string name="rendering_event_error_type_of_message_not_handled">"RiotX does not handle message of type '%1$s' (yet)"</string>
|
||||
<string name="rendering_event_error_exception">"RiotX encountered an issue when rendering content of event with id '%1$s'"</string>
|
||||
|
||||
<string name="unignore">Unignore</string>
|
||||
|
||||
<string name="room_list_sharing_header_recent_rooms">Recent rooms</string>
|
||||
<string name="room_list_sharing_header_other_rooms">Other rooms</string>
|
||||
|
||||
<string name="command_description_rainbow">Sends the given message colored as a rainbow</string>
|
||||
<string name="command_description_rainbow_emote">Sends the given emote colored as a rainbow</string>
|
||||
|
||||
<!-- Title for category in the settings which affect what is displayed in the timeline (ex: show read receipts, etc.) -->
|
||||
<string name="settings_category_timeline">Timeline</string>
|
||||
|
||||
|
|
|
@ -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("""<font color="#ff0000">a</font>""", rainbowGenerator.generate("a"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAscii2() {
|
||||
val expected = """
|
||||
<font color="#ff0000">a</font>
|
||||
<font color="#00ffff">b</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("ab"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAscii3() {
|
||||
val expected = """
|
||||
<font color="#ff0000">T</font>
|
||||
<font color="#ff5500">h</font>
|
||||
<font color="#ffaa00">i</font>
|
||||
<font color="#ffff00">s</font>
|
||||
|
||||
<font color="#55ff00">i</font>
|
||||
<font color="#00ff00">s</font>
|
||||
|
||||
<font color="#00ffaa">a</font>
|
||||
|
||||
<font color="#00aaff">r</font>
|
||||
<font color="#0055ff">a</font>
|
||||
<font color="#0000ff">i</font>
|
||||
<font color="#5500ff">n</font>
|
||||
<font color="#aa00ff">b</font>
|
||||
<font color="#ff00ff">o</font>
|
||||
<font color="#ff00aa">w</font>
|
||||
<font color="#ff0055">!</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("This is a rainbow!"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmoji1() {
|
||||
assertEquals("""<font color="#ff0000">🤞</font>""", rainbowGenerator.generate("\uD83E\uDD1E")) // 🤞
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmoji2() {
|
||||
assertEquals("""<font color="#ff0000">🤞</font>""", rainbowGenerator.generate("🤞"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmoji3() {
|
||||
val expected = """
|
||||
<font color="#ff0000">🤞</font>
|
||||
<font color="#00ffff">🙂</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("🤞🙂"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmojiMix1() {
|
||||
val expected = """
|
||||
<font color="#ff0000">H</font>
|
||||
<font color="#ff6d00">e</font>
|
||||
<font color="#ffdb00">l</font>
|
||||
<font color="#b6ff00">l</font>
|
||||
<font color="#49ff00">o</font>
|
||||
|
||||
<font color="#00ff92">🤞</font>
|
||||
|
||||
<font color="#0092ff">w</font>
|
||||
<font color="#0024ff">o</font>
|
||||
<font color="#4900ff">r</font>
|
||||
<font color="#b600ff">l</font>
|
||||
<font color="#ff00db">d</font>
|
||||
<font color="#ff006d">!</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("Hello 🤞 world!"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmojiMix2() {
|
||||
val expected = """
|
||||
<font color="#ff0000">a</font>
|
||||
<font color="#00ffff">🤞</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("a🤞"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmojiMix3() {
|
||||
val expected = """
|
||||
<font color="#ff0000">🤞</font>
|
||||
<font color="#00ffff">a</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("🤞a"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testError1() {
|
||||
assertEquals("<font color=\"#ff0000\">\uD83E</font>", rainbowGenerator.generate("\uD83E"))
|
||||
}
|
||||
}
|
19
vector/src/test/java/im/vector/riotx/test/Extensions.kt
Normal file
19
vector/src/test/java/im/vector/riotx/test/Extensions.kt
Normal file
|
@ -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", "")
|
Loading…
Reference in a new issue