Merge branch 'develop' into feature/ktlint_upgrade
This commit is contained in:
commit
fa0e07e146
|
@ -5,10 +5,10 @@ Features ✨:
|
|||
-
|
||||
|
||||
Improvements 🙌:
|
||||
-
|
||||
- Add support for `/plain` command (#12)
|
||||
|
||||
Bugfix 🐛:
|
||||
-
|
||||
- Fix crash on attachment preview screen (#1088)
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
@ -21,6 +21,7 @@ Build 🧱:
|
|||
|
||||
Other changes:
|
||||
- Restore availability to Chromebooks (#932)
|
||||
- Add a [documentation](./docs/integration_tests.md) to run integration tests
|
||||
|
||||
Changes in RiotX 0.17.0 (2020-02-27)
|
||||
===================================================
|
||||
|
|
|
@ -82,6 +82,8 @@ Make sure the following commands execute without any error:
|
|||
RiotX is currently supported on Android KitKat (API 19+): please test your change on an Android device (or Android emulator) running with API 19. Many issues can happen (including crashes) on older devices.
|
||||
Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient.
|
||||
|
||||
You should consider adding Unit tests with your PR, and also integration tests (AndroidTest). Please refer to [this document](./docs/integration_tests.md) to install and run the integration test environment.
|
||||
|
||||
### Internationalisation
|
||||
|
||||
When adding new string resources, please only add new entries in file `value/strings.xml`. Translations will be added later by the community of translators with a specific tool named [Weblate](https://translate.riot.im/projects/riot-android/).
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
# Integration tests
|
||||
|
||||
Integration tests are useful to ensure that the code works well for any use cases.
|
||||
|
||||
They can also be used as sample on how to use the Matrix SDK.
|
||||
|
||||
In a ideal world, every API of the SDK should be covered by integration tests. For the moment, we have test mainly for the Crypto part, which is the tricky part. But it covers quite a lot of features: accounts creation, login to existing account, send encrypted messages, keys backup, verification, etc.
|
||||
|
||||
The Matrix SDK is able to open multiple sessions, for the same user, of for different users. This way we can test communication between several sessions on a single device.
|
||||
|
||||
## Pre requirements
|
||||
|
||||
Integration tests need a homeserver running on localhost.
|
||||
|
||||
The documentation describes what we do to have one, using [Synapse](https://github.com/matrix-org/synapse/), which is the Matrix reference homeserver.
|
||||
|
||||
## Install and run Synapse
|
||||
|
||||
Steps:
|
||||
|
||||
- Install virtualenv
|
||||
|
||||
```bash
|
||||
python3 -m pip install virtualenv
|
||||
```
|
||||
|
||||
- Clone Synapse repository
|
||||
|
||||
```bash
|
||||
git clone -b develop https://github.com/matrix-org/synapse.git
|
||||
```
|
||||
or
|
||||
```bash
|
||||
git clone -b develop git@github.com:matrix-org/synapse.git
|
||||
```
|
||||
|
||||
You should have the develop branch cloned by default.
|
||||
|
||||
- Run synapse, from the Synapse folder you just cloned
|
||||
|
||||
```bash
|
||||
virtualenv -p python3 env
|
||||
source env/bin/activate
|
||||
pip install -e .
|
||||
demo/start.sh --no-rate-limit
|
||||
```
|
||||
|
||||
Alternatively, to install the latest Synapse release package (and not a cloned branch) you can run the following instead of `pip install -e .`:
|
||||
|
||||
```bash
|
||||
pip install matrix-synapse
|
||||
```
|
||||
|
||||
You should now have 3 running federated Synapse instances 🎉, at http://127.0.0.1:8080/, http://127.0.0.1:8081/ and http://127.0.0.1:8082/, which should display a "It Works! Synapse is running" message.
|
||||
|
||||
## Run the test
|
||||
|
||||
It's recommended to run tests using an Android Emulator and not a real device. First reason for that is that the tests will use http://10.0.2.2:8080 to connect to Synapse, which run locally on your machine.
|
||||
|
||||
You can run all the tests in the `androidTest` folders.
|
||||
|
||||
## Stop Synapse
|
||||
|
||||
To stop Synapse, you can run the following commands:
|
||||
|
||||
```bash
|
||||
./demo/stop.sh
|
||||
```
|
||||
|
||||
And you can deactivate the virtualenv:
|
||||
|
||||
```bash
|
||||
deactivate
|
||||
```
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
You'll need python3 to be able to run synapse
|
||||
|
||||
### Android Emulator does cannot reach the homeserver
|
||||
|
||||
Try on the Emulator browser to open "http://10.0.2.2:8080". You should see the "Synapse is running" message.
|
||||
|
||||
### virtualenv command fails
|
||||
|
||||
You can try using
|
||||
```bash
|
||||
python3 -m venv env
|
||||
```
|
||||
or
|
||||
```bash
|
||||
python3 -m virtualenv env
|
||||
```
|
||||
instead of
|
||||
```bash
|
||||
virtualenv -p python3 env
|
||||
```
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 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.matrix.android.internal.crypto.crosssigning
|
||||
|
||||
import org.amshove.kluent.shouldBeNull
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.junit.Test
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
class ExtensionsKtTest {
|
||||
|
||||
@Test
|
||||
fun testComparingBase64StringWithOrWithoutPadding() {
|
||||
// Without padding
|
||||
"NMJyumnhMic".fromBase64().contentEquals("NMJyumnhMic".fromBase64()).shouldBeTrue()
|
||||
// With padding
|
||||
"NMJyumnhMic".fromBase64().contentEquals("NMJyumnhMic=".fromBase64()).shouldBeTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBadBase64() {
|
||||
"===".fromBase64Safe().shouldBeNull()
|
||||
}
|
||||
}
|
|
@ -28,5 +28,7 @@ sealed class SharedSecretStorageError(message: String?) : Throwable(message) {
|
|||
object BadKeyFormat : SharedSecretStorageError("Bad Key Format")
|
||||
object ParsingError : SharedSecretStorageError("parsing Error")
|
||||
object BadMac : SharedSecretStorageError("Bad mac")
|
||||
object BadCipherText : SharedSecretStorageError("Bad cipher text")
|
||||
|
||||
data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage)
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
|
||||
cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo ->
|
||||
privateKeysInfo.master
|
||||
?.fromBase64NoPadding()
|
||||
?.fromBase64()
|
||||
?.let { privateKeySeed ->
|
||||
val pkSigning = OlmPkSigning()
|
||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
||||
|
@ -93,7 +93,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
}
|
||||
}
|
||||
privateKeysInfo.user
|
||||
?.fromBase64NoPadding()
|
||||
?.fromBase64()
|
||||
?.let { privateKeySeed ->
|
||||
val pkSigning = OlmPkSigning()
|
||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
||||
|
@ -106,7 +106,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
}
|
||||
}
|
||||
privateKeysInfo.selfSigned
|
||||
?.fromBase64NoPadding()
|
||||
?.fromBase64()
|
||||
?.let { privateKeySeed ->
|
||||
val pkSigning = OlmPkSigning()
|
||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
||||
|
@ -307,7 +307,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
var userKeyIsTrusted = false
|
||||
var selfSignedKeyIsTrusted = false
|
||||
|
||||
masterKeyPrivateKey?.fromBase64NoPadding()
|
||||
masterKeyPrivateKey?.fromBase64()
|
||||
?.let { privateKeySeed ->
|
||||
val pkSigning = OlmPkSigning()
|
||||
try {
|
||||
|
@ -324,7 +324,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
uskKeyPrivateKey?.fromBase64NoPadding()
|
||||
uskKeyPrivateKey?.fromBase64()
|
||||
?.let { privateKeySeed ->
|
||||
val pkSigning = OlmPkSigning()
|
||||
try {
|
||||
|
@ -341,7 +341,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
sskPrivateKey?.fromBase64NoPadding()
|
||||
sskPrivateKey?.fromBase64()
|
||||
?.let { privateKeySeed ->
|
||||
val pkSigning = OlmPkSigning()
|
||||
try {
|
||||
|
@ -450,7 +450,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
// 1) check if I know the private key
|
||||
val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys()
|
||||
?.master
|
||||
?.fromBase64NoPadding()
|
||||
?.fromBase64()
|
||||
|
||||
var isMaterKeyTrusted = false
|
||||
if (myMasterKey.trustLevel?.locallyVerified == true) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import android.util.Base64
|
|||
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import timber.log.Timber
|
||||
|
||||
fun CryptoDeviceInfo.canonicalSignable(): String {
|
||||
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
|
||||
|
@ -32,6 +33,18 @@ fun ByteArray.toBase64NoPadding(): String {
|
|||
return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
fun String.fromBase64NoPadding(): ByteArray {
|
||||
return Base64.decode(this, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
fun String.fromBase64(): ByteArray {
|
||||
return Base64.decode(this, Base64.DEFAULT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the base 64. Return null in case of bad format. Should be used when parsing received data from external source
|
||||
*/
|
||||
fun String.fromBase64Safe(): ByteArray? {
|
||||
return try {
|
||||
Base64.decode(this, Base64.DEFAULT)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable, "Unable to decode base64 string")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
|
|||
import im.vector.matrix.android.api.session.securestorage.SsssPassphrase
|
||||
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
|
||||
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.generatePrivateKeyWithPassword
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
|
||||
|
@ -268,7 +268,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
val ivParameterSpec = IvParameterSpec(iv)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||
// secret are not that big, just do Final
|
||||
val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64NoPadding())
|
||||
val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64())
|
||||
require(cipherBytes.isNotEmpty())
|
||||
|
||||
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
||||
|
@ -295,9 +295,9 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
|
||||
val macKey = pseudoRandomKey.copyOfRange(32, 64)
|
||||
|
||||
val iv = cipherContent.initializationVector?.fromBase64NoPadding() ?: ByteArray(16)
|
||||
val iv = cipherContent.initializationVector?.fromBase64() ?: ByteArray(16)
|
||||
|
||||
val cipherRawBytes = cipherContent.ciphertext!!.fromBase64NoPadding()
|
||||
val cipherRawBytes = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||
|
||||
|
@ -314,7 +314,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
|
||||
val digest = mac.doFinal(cipherRawBytes)
|
||||
|
||||
if (!cipherContent.mac?.fromBase64NoPadding()?.contentEquals(digest).orFalse()) {
|
||||
if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) {
|
||||
throw SharedSecretStorageError.BadMac
|
||||
} else {
|
||||
// we are good
|
||||
|
|
|
@ -299,14 +299,21 @@ internal abstract class SASDefaultVerificationTransaction(
|
|||
}
|
||||
|
||||
// If not me sign his MSK and upload the signature
|
||||
if (otherMasterKeyIsVerified && otherUserId != userId) {
|
||||
if (otherMasterKeyIsVerified) {
|
||||
// we should trust this master key
|
||||
// And check verification MSK -> SSK?
|
||||
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e(failure, "## SAS Verification: Failed to trust User $otherUserId")
|
||||
if (otherUserId != userId) {
|
||||
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e(failure, "## SAS Verification: Failed to trust User $otherUserId")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Notice other master key is mine because other is me
|
||||
if (otherMasterKey?.trustLevel?.isVerified() == false) {
|
||||
crossSigningService.markMyMasterKeyAsTrusted()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (otherUserId == userId) {
|
||||
|
|
|
@ -24,6 +24,8 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationTxSt
|
|||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64Safe
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationTransaction
|
||||
import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
|
||||
|
@ -199,7 +201,7 @@ internal class DefaultQrCodeVerificationTransaction(
|
|||
return
|
||||
}
|
||||
|
||||
if (startReq.sharedSecret == qrCodeData.sharedSecret) {
|
||||
if (startReq.sharedSecret?.fromBase64Safe()?.contentEquals(qrCodeData.sharedSecret.fromBase64()) == true) {
|
||||
// Ok, we can trust the other user
|
||||
// We can only trust the master key in this case
|
||||
// But first, ask the user for a confirmation
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto.verification.qrcode
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import im.vector.matrix.android.internal.extensions.toUnsignedInt
|
||||
|
||||
|
@ -52,15 +52,15 @@ fun QrCodeData.toEncodedString(): String {
|
|||
}
|
||||
|
||||
// Keys
|
||||
firstKey.fromBase64NoPadding().forEach {
|
||||
firstKey.fromBase64().forEach {
|
||||
result += it
|
||||
}
|
||||
secondKey.fromBase64NoPadding().forEach {
|
||||
secondKey.fromBase64().forEach {
|
||||
result += it
|
||||
}
|
||||
|
||||
// Secret
|
||||
sharedSecret.fromBase64NoPadding().forEach {
|
||||
sharedSecret.fromBase64().forEach {
|
||||
result += it
|
||||
}
|
||||
|
||||
|
@ -94,11 +94,11 @@ fun String.toQrCodeData(): QrCodeData? {
|
|||
val mode = byteArray[cursor].toInt()
|
||||
cursor++
|
||||
|
||||
// Get transaction length
|
||||
val bigEndian1 = byteArray[cursor].toUnsignedInt()
|
||||
val bigEndian2 = byteArray[cursor + 1].toUnsignedInt()
|
||||
// Get transaction length, Big Endian format
|
||||
val msb = byteArray[cursor].toUnsignedInt()
|
||||
val lsb = byteArray[cursor + 1].toUnsignedInt()
|
||||
|
||||
val transactionLength = bigEndian1 * 0x0100 + bigEndian2
|
||||
val transactionLength = msb.shl(8) + lsb
|
||||
|
||||
cursor++
|
||||
cursor++
|
||||
|
|
|
@ -39,6 +39,7 @@ import com.airbnb.mvrx.fragmentViewModel
|
|||
import com.airbnb.mvrx.withState
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import com.yalantis.ucrop.UCropActivity
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.cleanup
|
||||
|
@ -115,7 +116,7 @@ class AttachmentsPreviewFragment @Inject constructor(
|
|||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
withState(viewModel) { state ->
|
||||
val editMenuItem = menu.findItem(R.id.attachmentsPreviewEditAction)
|
||||
val showEditMenuItem = state.attachments[state.currentAttachmentIndex].isEditable()
|
||||
val showEditMenuItem = state.attachments.getOrNull(state.currentAttachmentIndex)?.isEditable().orFalse()
|
||||
editMenuItem.setVisible(showEditMenuItem)
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
|
|||
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler),
|
||||
POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll),
|
||||
SHRUG("/shrug", "<message>", R.string.command_description_shrug),
|
||||
PLAIN("/plain", "<message>", R.string.command_description_plain),
|
||||
// TODO temporary command
|
||||
VERIFY_USER("/verify", "<user-id>", R.string.command_description_verify);
|
||||
|
||||
|
|
|
@ -57,6 +57,15 @@ object CommandParser {
|
|||
}
|
||||
|
||||
return when (val slashCommand = messageParts.first()) {
|
||||
Command.PLAIN.command -> {
|
||||
val text = textMessage.substring(Command.PLAIN.command.length).trim()
|
||||
|
||||
if (text.isNotEmpty()) {
|
||||
ParsedCommand.SendPlainText(text)
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.PLAIN)
|
||||
}
|
||||
}
|
||||
Command.CHANGE_DISPLAY_NAME.command -> {
|
||||
val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim()
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ sealed class ParsedCommand {
|
|||
|
||||
// Valid commands:
|
||||
|
||||
class SendPlainText(val message: CharSequence) : ParsedCommand()
|
||||
class SendEmote(val message: CharSequence) : ParsedCommand()
|
||||
class SendRainbow(val message: CharSequence) : ParsedCommand()
|
||||
class SendRainbowEmote(val message: CharSequence) : ParsedCommand()
|
||||
|
|
|
@ -42,7 +42,7 @@ import im.vector.matrix.android.api.session.events.model.LocalEcho
|
|||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
|
||||
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
|
@ -274,7 +274,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
|||
}
|
||||
is VerificationAction.GotResultFromSsss -> {
|
||||
try {
|
||||
action.cypherData.fromBase64NoPadding().inputStream().use { ins ->
|
||||
action.cypherData.fromBase64().inputStream().use { ins ->
|
||||
val res = session.loadSecureSecret<Map<String, String>>(ins, action.alias)
|
||||
val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys(
|
||||
res?.get(MASTER_KEY_SSSS_NAME),
|
||||
|
|
|
@ -342,6 +342,12 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
is ParsedCommand.ErrorUnknownSlashCommand -> {
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
|
||||
}
|
||||
is ParsedCommand.SendPlainText -> {
|
||||
// Send the text message to the room, without markdown
|
||||
room.sendTextMessage(slashCommandResult.message, autoMarkdown = false)
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.Invite -> {
|
||||
handleInviteSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
|
||||
<!-- BEGIN Strings added by Others -->
|
||||
|
||||
<string name="command_description_plain">Sends a message as plain text, without interpreting it as markdown</string>
|
||||
<!-- END Strings added by Others -->
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue