Merge branch 'develop' into cross_signing

This commit is contained in:
Valere 2020-01-31 14:09:40 +01:00
commit 5c547794f2
35 changed files with 623 additions and 88 deletions

View file

@ -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 🐛:
-

View file

@ -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)

View file

@ -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"
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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>

View file

@ -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
}

View file

@ -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),

View file

@ -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]

View file

@ -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()

View file

@ -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)

View file

@ -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
}
}

View file

@ -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()
)
}
}

View file

@ -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')
}
}

View file

@ -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

View file

@ -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> {

View file

@ -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)

View file

@ -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)
}

View file

@ -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 {

View file

@ -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> {

View file

@ -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
}

View file

@ -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()
}

View file

@ -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()
}
}

View file

@ -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)

View file

@ -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) {

View file

@ -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

View file

@ -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)
)
}

View file

@ -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)
}
}
}
}
}
}
}
}

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"))
}
}

View 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", "")