Merge branch 'develop' into feature/room_list_actions

This commit is contained in:
ganfra 2019-11-04 15:11:20 +01:00
commit 6177e69855
97 changed files with 1157 additions and 765 deletions

View File

@ -3,14 +3,36 @@
# https://github.com/buildkite-plugins/docker-buildkite-plugin/releases
# We propagate the environment to the container (sse https://github.com/buildkite-plugins/docker-buildkite-plugin#propagate-environment-optional-boolean)
# Build debug version of the RiotX application, from the develop branch and the features branches
steps:
- label: "Assemble GPlay Debug version"
- label: "Compile and run Unit tests"
agents:
# We use a medium sized instance instead of the normal small ones because
# gradle build is long
# gradle build can be memory hungry
queue: "medium"
commands:
- "./gradlew clean test --stacktrace"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
propagate-environment: true
- label: "Compile Android tests"
agents:
# We use a medium sized instance instead of the normal small ones because
# gradle build can be memory hungry
queue: "medium"
commands:
- "./gradlew clean assembleAndroidTest --stacktrace"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
propagate-environment: true
- label: "Assemble GPlay Debug version"
agents:
# We use a xlarge sized instance instead of the normal small ones because
# gradle build can be memory hungry
queue: "xlarge"
commands:
- "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace"
artifact_paths:
@ -23,9 +45,9 @@ steps:
- label: "Assemble FDroid Debug version"
agents:
# We use a medium sized instance instead of the normal small ones because
# gradle build is long
queue: "medium"
# We use a xlarge sized instance instead of the normal small ones because
# gradle build can be memory hungry
queue: "xlarge"
commands:
- "./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace"
artifact_paths:
@ -38,9 +60,9 @@ steps:
- label: "Build Google Play unsigned APK"
agents:
# We use a medium sized instance instead of the normal small ones because
# gradle build is long
queue: "medium"
# We use a xlarge sized instance instead of the normal small ones because
# gradle build can be memory hungry
queue: "xlarge"
commands:
- "./gradlew clean assembleGplayRelease --stacktrace"
artifact_paths:

View File

@ -1,17 +1,40 @@
Changes in RiotX 0.7.0 (2019-XX-XX)
Changes in RiotX 0.8.0 (2019-XX-XX)
===================================================
Features ✨:
-
Improvements 🙌:
- Handle code tags (#567)
Other changes:
- Markdown set to off by default (#412)
- Accessibility improvements to the attachment file type chooser
Bugfix 🐛:
- Fix issues with some member events rendering (#498)
- Passphrase does not match (Export room keys) (#644)
- Ask for permission to write external storage when uri comes from the keyboard (#658)
Translations 🗣:
-
Build 🧱:
-
Changes in RiotX 0.7.0 (2019-10-24)
===================================================
Features:
-
- Share elements from other app to RiotX (#58)
- Read marker (#84)
- Add ability to report content (#515)
Improvements:
- Persist active tab between sessions (#503)
- Do not upload file too big for the homeserver (#587)
- Handle read markers (#84)
- Attachments: start using system pickers (#52)
- Attachments: start handling incoming share (#58)
- Mark all messages as read (#396)
- Add ability to report content (#515)
Other changes:
@ -26,12 +49,6 @@ Bugfix:
- Invitation notifications are not dismissed automatically if room is joined from another client (#347)
- Opening links from RiotX reuses browser tab (#599)
Translations:
-
Build:
-
Changes in RiotX 0.6.1 (2019-09-24)
===================================================

View File

@ -86,6 +86,10 @@ Also, if possible, please test your change on a real device. Testing on Android
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/).
Do not hesitate to use plurals when appropriate.
### Accessibility
Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`.
### Layout
When adding or editing layouts, make sure the layout will render correctly if device uses a RTL (Right To Left) language.

View File

@ -11,6 +11,8 @@ android {
versionCode 1
versionName "1.0"
// Multidex is useful for tests
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@ -1,42 +0,0 @@
/*
* Copyright 2019 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.rx;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("im.vector.matrix.rx.test", appContext.getPackageName());
}
}

View File

@ -155,7 +155,8 @@ dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:4.3'
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
testImplementation 'io.mockk:mockk:1.9.3.kotlin12'
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
testImplementation 'io.mockk:mockk:1.9.2.kotlin12'
testImplementation 'org.amshove.kluent:kluent-android:1.44'
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
@ -165,7 +166,8 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
androidTestImplementation 'io.mockk:mockk-android:1.9.3.kotlin12'
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
androidTestImplementation 'io.mockk:mockk-android:1.9.2.kotlin12'
androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

View File

@ -17,12 +17,12 @@
package im.vector.matrix.android
import android.content.Context
import androidx.test.InstrumentationRegistry
import androidx.test.core.app.ApplicationProvider
import java.io.File
interface InstrumentedTest {
fun context(): Context {
return InstrumentationRegistry.getTargetContext()
return ApplicationProvider.getApplicationContext()
}
fun cacheDir(): File {

View File

@ -17,8 +17,8 @@
package im.vector.matrix.android.auth
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import androidx.test.runner.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.OkReplayRuleChainNoActivity
import im.vector.matrix.android.api.auth.Authenticator

View File

@ -16,7 +16,7 @@
package im.vector.matrix.android.api.session.events.model
import java.util.*
import java.util.UUID
object LocalEcho {

View File

@ -62,15 +62,11 @@ data class TimelineEvent(
}
fun getDisambiguatedDisplayName(): String {
return if (isUniqueDisplayName) {
senderName
} else {
senderName?.let { name ->
"$name (${root.senderId})"
}
return when {
senderName.isNullOrBlank() -> root.senderId ?: ""
isUniqueDisplayName -> senderName
else -> "$senderName (${root.senderId})"
}
?: root.senderId
?: ""
}
/**
@ -104,7 +100,7 @@ fun TimelineEvent.getEditedEventId(): String? {
* Get last MessageContent, after a possible edition
*/
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel()
?: root.getClearContent().toModel()
/**
* Get last Message body, after a possible edition
@ -113,7 +109,8 @@ fun TimelineEvent.getLastMessageBody(): String? {
val lastMessageContent = getLastMessageContent()
if (lastMessageContent != null) {
return lastMessageContent.newContent?.toModel<MessageContent>()?.body ?: lastMessageContent.body
return lastMessageContent.newContent?.toModel<MessageContent>()?.body
?: lastMessageContent.body
}
return null

View File

@ -66,7 +66,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
if (':' in userId) {
try {
synchronized(notReadyToRetryHS) {
res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1))
res = !notReadyToRetryHS.contains(userId.substringAfterLast(':'))
}
} catch (e: Exception) {
Timber.e(e, "## canRetryKeysDownload() failed")

View File

@ -216,7 +216,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
sendMessageToDevices(requestMessage, request.recipients, request.requestId, object : MatrixCallback<Unit> {
private fun onDone(state: OutgoingRoomKeyRequest.RequestState) {
if (request.state !== OutgoingRoomKeyRequest.RequestState.UNSENT) {
Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to " + request.state)
Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to ${request.state}")
} else {
request.state = state
cryptoStore.updateOutgoingRoomKeyRequest(request)

View File

@ -43,6 +43,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.Exception
import java.util.UUID
import javax.inject.Inject
import kotlin.collections.HashMap
@ -166,72 +167,59 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
return
}
// Download device keys prior to everything
checkKeysAreDownloaded(
otherUserId!!,
startReq,
success = {
Timber.v("## SAS onStartRequestReceived ${startReq.transactionID!!}")
val tid = startReq.transactionID!!
val existing = getExistingTransaction(otherUserId, tid)
val existingTxs = getExistingTransactionsForUser(otherUserId)
if (existing != null) {
// should cancel both!
Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}")
existing.cancel(CancelCode.UnexpectedMessage)
cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else if (existingTxs?.isEmpty() == false) {
Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}")
// Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time.
existingTxs.forEach {
it.cancel(CancelCode.UnexpectedMessage)
}
cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else {
// Ok we can create
if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) {
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
val tx = IncomingSASVerificationTransaction(
this,
setDeviceVerificationAction,
credentials,
cryptoStore,
sendToDeviceTask,
taskExecutor,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionID!!,
otherUserId)
addTransaction(tx)
tx.acceptToDeviceEvent(otherUserId, startReq)
} else {
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
cancelTransaction(tid, otherUserId, startReq.fromDevice
?: event.getSenderKey()!!, CancelCode.UnknownMethod)
}
}
},
error = {
cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
})
if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) {
Timber.v("## SAS onStartRequestReceived ${startReq.transactionID!!}")
val tid = startReq.transactionID!!
val existing = getExistingTransaction(otherUserId, tid)
val existingTxs = getExistingTransactionsForUser(otherUserId)
if (existing != null) {
// should cancel both!
Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}")
existing.cancel(CancelCode.UnexpectedMessage)
cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else if (existingTxs?.isEmpty() == false) {
Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}")
// Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time.
existingTxs.forEach {
it.cancel(CancelCode.UnexpectedMessage)
}
cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else {
// Ok we can create
if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) {
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
val tx = IncomingSASVerificationTransaction(
this,
setDeviceVerificationAction,
credentials,
cryptoStore,
sendToDeviceTask,
taskExecutor,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionID!!,
otherUserId)
addTransaction(tx)
tx.acceptToDeviceEvent(otherUserId, startReq)
} else {
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
cancelTransaction(tid, otherUserId, startReq.fromDevice
?: event.getSenderKey()!!, CancelCode.UnknownMethod)
}
}
} else {
cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
}
}
private suspend fun checkKeysAreDownloaded(otherUserId: String,
startReq: KeyVerificationStart,
success: (MXUsersDevicesMap<MXDeviceInfo>) -> Unit,
error: () -> Unit) {
runCatching {
deviceListManager.downloadKeys(listOf(otherUserId), true)
}.fold(
{
if (it.getUserDeviceIds(otherUserId)?.contains(startReq.fromDevice) == true) {
success(it)
} else {
error()
}
},
{
error()
}
)
startReq: KeyVerificationStart): MXUsersDevicesMap<MXDeviceInfo>? {
return try {
val keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null
keys.takeIf { deviceIds.contains(startReq.fromDevice) }
} catch (e: Exception) {
null
}
}
private suspend fun onCancelReceived(event: Event) {
@ -342,10 +330,8 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
private fun addTransaction(tx: VerificationTransaction) {
tx.otherUserId.let { otherUserId ->
synchronized(txMap) {
if (txMap[otherUserId] == null) {
txMap[otherUserId] = HashMap()
}
txMap[otherUserId]?.set(tx.transactionId, tx)
val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() }
txInnerMap[tx.transactionId] = tx
dispatchTxAdded(tx)
tx.addListener(this)
}

View File

@ -39,14 +39,17 @@ internal fun TimelineEventEntity.updateSenderData() {
val isUnlinked = chunkEntity.isUnlinked()
var senderMembershipEvent: EventEntity?
var senderRoomMemberContent: String?
var senderRoomMemberPrevContent: String?
when {
stateIndex <= 0 -> {
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root
senderRoomMemberContent = senderMembershipEvent?.prevContent
senderRoomMemberPrevContent = senderMembershipEvent?.content
}
else -> {
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root
senderRoomMemberContent = senderMembershipEvent?.content
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
}
}
@ -58,11 +61,27 @@ internal fun TimelineEventEntity.updateSenderData() {
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER)
.prev(since = stateIndex)
senderRoomMemberContent = senderMembershipEvent?.content
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
}
ContentMapper.map(senderRoomMemberContent).toModel<RoomMember>()?.also {
this.senderAvatar = it.avatarUrl
this.senderName = it.displayName
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
}
// We try to fallback on prev content if we got a room member state events with null fields
if (root?.type == EventType.STATE_ROOM_MEMBER) {
ContentMapper.map(senderRoomMemberPrevContent).toModel<RoomMember>()?.also {
if (this.senderAvatar == null && it.avatarUrl != null) {
this.senderAvatar = it.avatarUrl
}
if (this.senderName == null && it.displayName != null) {
this.senderName = it.displayName
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
}
}
}
val senderRoomMember: RoomMember? = ContentMapper.map(senderRoomMemberContent).toModel()
this.senderAvatar = senderRoomMember?.avatarUrl
this.senderName = senderRoomMember?.displayName
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(senderRoomMember?.displayName)
this.senderMembershipEvent = senderMembershipEvent
}

View File

@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import java.util.*
import java.util.UUID
import javax.inject.Inject
internal class RoomSummaryMapper @Inject constructor(

View File

@ -22,7 +22,7 @@ import com.novoda.merlin.MerlinsBeard
import im.vector.matrix.android.internal.di.MatrixScope
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import timber.log.Timber
import java.util.*
import java.util.Collections
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

View File

@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import java.util.*
import java.util.Date
import javax.inject.Inject
internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit>

View File

@ -32,7 +32,7 @@ import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.worker.WorkManagerUtil
import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import java.util.*
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject

View File

@ -73,6 +73,7 @@ internal class RoomMembers(private val realm: Realm,
return EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.isNotNull(EventEntityFields.STATE_KEY)
.distinct(EventEntityFields.STATE_KEY)
.isNotNull(EventEntityFields.CONTENT)
}

View File

@ -39,7 +39,6 @@ import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.util.StringProvider
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import java.util.*
import javax.inject.Inject
/**
@ -119,7 +118,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
permalink,
stringProvider.getString(R.string.message_reply_to_prefix),
userLink,
originalEvent.senderName ?: originalEvent.root.senderId,
originalEvent.getDisambiguatedDisplayName(),
body.takeFormatted(),
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
)

View File

@ -52,7 +52,8 @@ import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import timber.log.Timber
import java.util.*
import java.util.Collections
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.ArrayList

View File

@ -31,7 +31,7 @@ import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.SecureRandom
import java.util.*
import java.util.Calendar
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec

View File

@ -36,7 +36,7 @@ import java.security.*
import java.security.cert.CertificateException
import java.security.spec.AlgorithmParameterSpec
import java.security.spec.RSAKeyGenParameterSpec
import java.util.*
import java.util.Calendar
import java.util.zip.GZIPOutputStream
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.util
import im.vector.matrix.android.api.MatrixPatterns
import timber.log.Timber
import java.util.*
import java.util.Locale
/**
* Convert a string to an UTF8 String

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.internal.di.MoshiProvider
import org.junit.Assert
import org.junit.Assert.*
import org.junit.Test
class PushRuleActionsTest {
@ -63,22 +63,17 @@ class PushRuleActionsTest {
val pushRule = MoshiProvider.providesMoshi().adapter<PushRule>(PushRule::class.java).fromJson(rawPushRule)
Assert.assertNotNull("Should have parsed the rule", pushRule)
Assert.assertNotNull("Failed to parse actions", Action.mapFrom(pushRule!!))
assertNotNull("Should have parsed the rule", pushRule)
val actions = Action.mapFrom(pushRule)
Assert.assertEquals(3, actions!!.size)
val actions = pushRule!!.getActions()
assertEquals(3, actions.size)
Assert.assertEquals("First action should be notify", Action.Type.NOTIFY, actions[0].type)
assertTrue("First action should be notify", actions[0] is Action.Notify)
Assert.assertEquals("Second action should be tweak", Action.Type.SET_TWEAK, actions[1].type)
Assert.assertEquals("Second action tweak key should be sound", "sound", actions[1].tweak_action)
Assert.assertEquals("Second action should have default as stringValue", "default", actions[1].stringValue)
Assert.assertNull("Second action boolValue should be null", actions[1].boolValue)
assertTrue("Second action should be sound", actions[1] is Action.Sound)
assertEquals("Second action should have default sound", "default", (actions[1] as Action.Sound).sound)
Assert.assertEquals("Third action should be tweak", Action.Type.SET_TWEAK, actions[2].type)
Assert.assertEquals("Third action tweak key should be highlight", "highlight", actions[2].tweak_action)
Assert.assertEquals("Third action tweak param should be false", false, actions[2].boolValue)
Assert.assertNull("Third action stringValue should be null", actions[2].stringValue)
assertTrue("Third action should be highlight", actions[2] is Action.Highlight)
assertEquals("Third action tweak param should be false", false, (actions[2] as Action.Highlight).highlight)
}
}

View File

@ -199,6 +199,10 @@ class PushrulesConditionTest {
}
class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room {
override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback<Unit>): Cancelable {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
override fun getReadMarkerLive(): LiveData<Optional<String>> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}

View File

@ -102,6 +102,7 @@ cp ../riot-android/vector/src/main/res/values-ro/strings.xml ./vector/src
cp ../riot-android/vector/src/main/res/values-ru/strings.xml ./vector/src/main/res/values-ru/strings.xml
cp ../riot-android/vector/src/main/res/values-sk/strings.xml ./vector/src/main/res/values-sk/strings.xml
cp ../riot-android/vector/src/main/res/values-sq/strings.xml ./vector/src/main/res/values-sq/strings.xml
cp ../riot-android/vector/src/main/res/values-sr/strings.xml ./vector/src/main/res/values-sr/strings.xml
cp ../riot-android/vector/src/main/res/values-te/strings.xml ./vector/src/main/res/values-te/strings.xml
cp ../riot-android/vector/src/main/res/values-th/strings.xml ./vector/src/main/res/values-th/strings.xml
cp ../riot-android/vector/src/main/res/values-tlh/strings.xml ./vector/src/main/res/values-tlh/strings.xml

View File

@ -15,7 +15,7 @@ androidExtensions {
}
ext.versionMajor = 0
ext.versionMinor = 7
ext.versionMinor = 8
ext.versionPatch = 0
static def getGitTimestamp() {
@ -219,7 +219,7 @@ dependencies {
def epoxy_version = '3.8.0'
def arrow_version = "0.8.2"
def coroutines_version = "1.3.2"
def markwon_version = '3.1.0'
def markwon_version = '4.1.2'
def big_image_viewer_version = '1.5.6'
def glide_version = '4.10.0'
def moshi_version = '1.8.0'
@ -283,8 +283,8 @@ dependencies {
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.android.material:material:1.1.0-beta01'
implementation 'me.gujun.android:span:1.7'
implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version"
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version"
implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1'

View File

@ -1,40 +0,0 @@
/*
* Copyright 2019 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
import androidx.test.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
assertEquals("im.vector.riotx", appContext.packageName)
}
}

View File

@ -42,7 +42,7 @@ import javax.inject.Singleton
@Singleton
class AppStateHandler @Inject constructor(
private val sessionObservableStore: ActiveSessionObservableStore,
private val homeRoomListStore: HomeRoomListObservableStore,
private val homeRoomListObservableStore: HomeRoomListObservableStore,
private val selectedGroupStore: SelectedGroupStore) : LifecycleObserver {
private val compositeDisposable = CompositeDisposable()
@ -92,7 +92,7 @@ class AppStateHandler @Inject constructor(
}
)
.subscribe {
homeRoomListStore.post(it)
homeRoomListObservableStore.post(it)
}
.addTo(compositeDisposable)
}

View File

@ -55,7 +55,8 @@ import im.vector.riotx.features.version.VersionProvider
import im.vector.riotx.push.fcm.FcmHelper
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
import javax.inject.Inject
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {

View File

@ -41,12 +41,13 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFrag
import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.*
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.home.room.list.RoomListModule
import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.link.LinkHandlerActivity
import im.vector.riotx.features.login.LoginActivity
@ -71,7 +72,17 @@ import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.share.IncomingShareActivity
import im.vector.riotx.features.ui.UiStateRepository
@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
@Component(
dependencies = [
VectorComponent::class
],
modules = [
AssistedInjectModule::class,
ViewModelModule::class,
HomeModule::class,
RoomListModule::class
]
)
@ScreenScope
interface ScreenComponent {

View File

@ -23,6 +23,7 @@ import dagger.Component
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.ActiveSessionObservableStore
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.VectorApplication
@ -42,6 +43,7 @@ import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.share.ShareRoomListObservableStore
import im.vector.riotx.features.ui.UiStateRepository
import javax.inject.Singleton
@ -85,8 +87,12 @@ interface VectorComponent {
fun homeRoomListObservableStore(): HomeRoomListObservableStore
fun shareRoomListObservableStore(): ShareRoomListObservableStore
fun selectedGroupStore(): SelectedGroupStore
fun activeSessionObservableStore(): ActiveSessionObservableStore
fun incomingVerificationRequestHandler(): IncomingVerificationRequestHandler
fun incomingKeyRequestHandler(): KeyRequestHandler

View File

@ -44,15 +44,15 @@ class ExportKeysDialog {
val textWatcher = object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) {
when {
passPhrase1EditText.text.isNullOrEmpty() -> {
passPhrase1EditText.text.isNullOrEmpty() -> {
exportButton.isEnabled = false
passPhrase2Til.error = null
}
passPhrase1EditText.text == passPhrase2EditText.text -> {
passPhrase1EditText.text.toString() == passPhrase2EditText.text.toString() -> {
exportButton.isEnabled = true
passPhrase2Til.error = null
}
else -> {
else -> {
exportButton.isEnabled = false
passPhrase2Til.error = activity.getString(R.string.passphrase_passphrase_does_not_match)
}

View File

@ -30,15 +30,10 @@ import java.io.File
*/
@WorkerThread
fun writeToFile(str: String, file: File): Try<Unit> {
return Try {
val sink = file.sink()
val bufferedSink = sink.buffer()
bufferedSink.writeString(str, Charsets.UTF_8)
bufferedSink.close()
sink.close()
return Try<Unit> {
file.sink().buffer().use {
it.writeString(str, Charsets.UTF_8)
}
}
}
@ -47,15 +42,10 @@ fun writeToFile(str: String, file: File): Try<Unit> {
*/
@WorkerThread
fun writeToFile(data: ByteArray, file: File): Try<Unit> {
return Try {
val sink = file.sink()
val bufferedSink = sink.buffer()
bufferedSink.write(data)
bufferedSink.close()
sink.close()
return Try<Unit> {
file.sink().buffer().use {
it.write(data)
}
}
}

View File

@ -17,7 +17,6 @@
package im.vector.riotx.core.images
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import androidx.exifinterface.media.ExifInterface
@ -37,26 +36,24 @@ class ImageTools @Inject constructor(private val context: Context) {
if (uri.scheme == "content") {
val proj = arrayOf(MediaStore.Images.Media.DATA)
var cursor: Cursor? = null
try {
cursor = context.contentResolver.query(uri, proj, null, null, null)
if (cursor != null && cursor.count > 0) {
cursor.moveToFirst()
val idxData = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val path = cursor.getString(idxData)
if (path.isNullOrBlank()) {
Timber.w("Cannot find path in media db for uri $uri")
return orientation
val cursor = context.contentResolver.query(uri, proj, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val idxData = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val path = it.getString(idxData)
if (path.isNullOrBlank()) {
Timber.w("Cannot find path in media db for uri $uri")
return orientation
}
val exif = ExifInterface(path)
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
}
val exif = ExifInterface(path)
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
}
} catch (e: Exception) {
// eg SecurityException from com.google.android.apps.photos.content.GooglePhotosImageProvider URIs
// eg IOException from trying to parse the returned path as a file when it is an http uri.
Timber.e(e, "Cannot get orientation for bitmap")
} finally {
cursor?.close()
}
} else if (uri.scheme == "file") {
try {

View File

@ -17,28 +17,17 @@
package im.vector.riotx.core.intent
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
var result: String? = null
if (context != null && uri.scheme == "content") {
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
return it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
} finally {
cursor?.close()
}
}
if (result == null) {
result = uri.path
val cut = result?.lastIndexOf('/') ?: -1
if (cut != -1) {
result = result?.substring(cut + 1)
}
}
return result
return uri.path?.substringAfterLast('/')
}

View File

@ -15,8 +15,6 @@
*/
package im.vector.riotx.core.linkify
import java.util.regex.Pattern
/**
* Better support for geo URi
*/
@ -26,7 +24,7 @@ object VectorAutoLinkPatterns {
private const val LAT_OR_LONG_OR_ALT_NUMBER = "-?\\d+(?:\\.\\d+)?"
private const val COORDINATE_SYSTEM = ";crs=[\\w-]+"
val GEO_URI: Pattern = Pattern.compile("(?:geo:)?" +
val GEO_URI: Regex = Regex("(?:geo:)?" +
"(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" +
"," +
"(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" +
@ -35,5 +33,5 @@ object VectorAutoLinkPatterns {
"(?:" + ";u=\\d+(?:\\.\\d+)?" + ")?" + // uncertainty in meters
"(?:" +
";[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+" + // dafuk
")*", Pattern.CASE_INSENSITIVE)
")*", RegexOption.IGNORE_CASE)
}

View File

@ -19,7 +19,6 @@ import android.text.Spannable
import android.text.style.URLSpan
import android.text.util.Linkify
import androidx.core.text.util.LinkifyCompat
import java.util.*
object VectorLinkify {
/**
@ -95,7 +94,7 @@ object VectorLinkify {
createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
}
LinkifyCompat.addLinks(spannable, VectorAutoLinkPatterns.GEO_URI, "geo:", arrayOf("geo:"), geoMatchFilter, null)
LinkifyCompat.addLinks(spannable, VectorAutoLinkPatterns.GEO_URI.toPattern(), "geo:", arrayOf("geo:"), geoMatchFilter, null)
spannable.forEachSpanIndexed { _, urlSpan, start, end ->
spannable.removeSpan(urlSpan)
createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
@ -108,7 +107,7 @@ object VectorLinkify {
}
private fun pruneOverlaps(links: ArrayList<LinkSpec>) {
Collections.sort(links, COMPARATOR)
links.sortWith(COMPARATOR)
var len = links.size
var i = 0
while (i < len - 1) {

View File

@ -16,17 +16,15 @@
package im.vector.riotx.core.platform
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.widget.ScrollView
import androidx.core.widget.NestedScrollView
import im.vector.riotx.R
private const val DEFAULT_MAX_HEIGHT = 200
class MaxHeightScrollView : ScrollView {
class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
: NestedScrollView(context, attrs, defStyle) {
var maxHeight: Int = 0
set(value) {
@ -34,28 +32,7 @@ class MaxHeightScrollView : ScrollView {
requestLayout()
}
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
if (!isInEditMode) {
init(context, attrs)
}
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
if (!isInEditMode) {
init(context, attrs)
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
if (!isInEditMode) {
init(context, attrs)
}
}
private fun init(context: Context, attrs: AttributeSet?) {
init {
if (attrs != null) {
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView)
maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)

View File

@ -30,7 +30,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.utils.DimensionConverter
import java.util.*
import java.util.UUID
/**
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)

View File

@ -22,8 +22,9 @@ import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.AppNameProvider
import im.vector.riotx.core.resources.LocaleProvider
import im.vector.riotx.core.resources.StringProvider
import java.util.*
import java.util.UUID
import javax.inject.Inject
import kotlin.math.abs
private const val DEFAULT_PUSHER_FILE_TAG = "mobile"
@ -36,7 +37,7 @@ class PushersManager @Inject constructor(
fun registerPusherWithFcmKey(pushKey: String): UUID {
val currentSession = activeSessionHolder.getActiveSession()
var profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + Math.abs(currentSession.myUserId.hashCode())
val profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(currentSession.myUserId.hashCode())
return currentSession.addHttpPusher(
pushKey,

View File

@ -18,7 +18,7 @@ package im.vector.riotx.core.resources
import android.content.res.Resources
import androidx.core.os.ConfigurationCompat
import java.util.*
import java.util.Locale
import javax.inject.Inject
class LocaleProvider @Inject constructor(private val resources: Resources) {

View File

@ -16,7 +16,7 @@
package im.vector.riotx.core.utils
import android.view.View
import java.util.*
import java.util.WeakHashMap
/**
* Simple Debounced OnClickListener

View File

@ -59,9 +59,10 @@ fun initKnownEmojiHashSet(context: Context, done: (() -> Unit)? = null) {
val jsonAdapter = moshi.adapter(EmojiDataSource.EmojiData::class.java)
val inputAsString = input.bufferedReader().use { it.readText() }
val source = jsonAdapter.fromJson(inputAsString)
knownEmojiSet = HashSet<String>()
source?.emojis?.values?.forEach {
knownEmojiSet?.add(it.emojiString())
knownEmojiSet = HashSet<String>().also {
source?.emojis?.mapTo(it) { (_, value) ->
value.emojiString()
}
}
done?.invoke()
}

View File

@ -32,7 +32,8 @@ import im.vector.riotx.R
import timber.log.Timber
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
/**
* Open a url in the internet browser of the system

View File

@ -67,6 +67,7 @@ const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
/**
* Log the used permissions statuses.

View File

@ -31,7 +31,7 @@ import im.vector.riotx.R
import im.vector.riotx.features.notifications.NotificationUtils
import im.vector.riotx.features.settings.VectorLocale
import timber.log.Timber
import java.util.*
import java.util.Locale
/**
* Tells if the application ignores battery optimizations.

View File

@ -19,7 +19,7 @@ package im.vector.riotx.core.utils
import android.content.Context
import android.os.Build
import android.text.format.Formatter
import java.util.*
import java.util.TreeMap
object TextUtils {

View File

@ -44,12 +44,11 @@ object CommandParser {
return ParsedCommand.ErrorNotACommand
}
var messageParts: List<String>? = null
try {
messageParts = textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }
val messageParts = try {
textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }
} catch (e: Exception) {
Timber.e(e, "## manageSplashCommand() : split failed")
null
}
// test if the string cut fails
@ -57,10 +56,8 @@ object CommandParser {
return ParsedCommand.ErrorEmptySlashCommand
}
val slashCommand = messageParts[0]
when (slashCommand) {
Command.CHANGE_DISPLAY_NAME.command -> {
when (val slashCommand = messageParts.first()) {
Command.CHANGE_DISPLAY_NAME.command -> {
val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim()
return if (newDisplayName.isNotEmpty()) {
@ -69,7 +66,7 @@ object CommandParser {
ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME)
}
}
Command.TOPIC.command -> {
Command.TOPIC.command -> {
val newTopic = textMessage.substring(Command.TOPIC.command.length).trim()
return if (newTopic.isNotEmpty()) {
@ -78,12 +75,12 @@ object CommandParser {
ParsedCommand.ErrorSyntax(Command.TOPIC)
}
}
Command.EMOTE.command -> {
Command.EMOTE.command -> {
val message = textMessage.substring(Command.EMOTE.command.length).trim()
return ParsedCommand.SendEmote(message)
}
Command.JOIN_ROOM.command -> {
Command.JOIN_ROOM.command -> {
val roomAlias = textMessage.substring(Command.JOIN_ROOM.command.length).trim()
return if (roomAlias.isNotEmpty()) {
@ -92,7 +89,7 @@ object CommandParser {
ParsedCommand.ErrorSyntax(Command.JOIN_ROOM)
}
}
Command.PART.command -> {
Command.PART.command -> {
val roomAlias = textMessage.substring(Command.PART.command.length).trim()
return if (roomAlias.isNotEmpty()) {
@ -101,7 +98,7 @@ object CommandParser {
ParsedCommand.ErrorSyntax(Command.PART)
}
}
Command.INVITE.command -> {
Command.INVITE.command -> {
return if (messageParts.size == 2) {
val userId = messageParts[1]
@ -114,7 +111,7 @@ object CommandParser {
ParsedCommand.ErrorSyntax(Command.INVITE)
}
}
Command.KICK_USER.command -> {
Command.KICK_USER.command -> {
return if (messageParts.size >= 2) {
val userId = messageParts[1]
if (MatrixPatterns.isUserId(userId)) {
@ -130,7 +127,7 @@ object CommandParser {
ParsedCommand.ErrorSyntax(Command.KICK_USER)
}
}
Command.BAN_USER.command -> {
Command.BAN_USER.command -> {
return if (messageParts.size >= 2) {
val userId = messageParts[1]
if (MatrixPatterns.isUserId(userId)) {
@ -146,7 +143,7 @@ object CommandParser {
ParsedCommand.ErrorSyntax(Command.BAN_USER)
}
}
Command.UNBAN_USER.command -> {
Command.UNBAN_USER.command -> {
return if (messageParts.size == 2) {
val userId = messageParts[1]
@ -159,7 +156,7 @@ object CommandParser {
ParsedCommand.ErrorSyntax(Command.UNBAN_USER)
}
}
Command.SET_USER_POWER_LEVEL.command -> {
Command.SET_USER_POWER_LEVEL.command -> {
return if (messageParts.size == 3) {
val userId = messageParts[1]
if (MatrixPatterns.isUserId(userId)) {
@ -192,25 +189,25 @@ object CommandParser {
ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL)
}
}
Command.MARKDOWN.command -> {
Command.MARKDOWN.command -> {
return if (messageParts.size == 2) {
when {
"on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true)
"on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true)
"off".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(false)
else -> ParsedCommand.ErrorSyntax(Command.MARKDOWN)
else -> ParsedCommand.ErrorSyntax(Command.MARKDOWN)
}
} else {
ParsedCommand.ErrorSyntax(Command.MARKDOWN)
}
}
Command.CLEAR_SCALAR_TOKEN.command -> {
Command.CLEAR_SCALAR_TOKEN.command -> {
return if (messageParts.size == 1) {
ParsedCommand.ClearScalarToken
} else {
ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN)
}
}
else -> {
else -> {
// Unknown command
return ParsedCommand.ErrorUnknownSlashCommand(slashCommand)
}

View File

@ -24,7 +24,7 @@ import im.vector.riotx.features.settings.FontScale
import im.vector.riotx.features.settings.VectorLocale
import im.vector.riotx.features.themes.ThemeUtils
import timber.log.Timber
import java.util.*
import java.util.Locale
import javax.inject.Inject
/**

View File

@ -18,10 +18,10 @@ package im.vector.riotx.features.crypto.keys
import android.content.Context
import android.os.Environment
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.files.writeToFile
import kotlinx.coroutines.Dispatchers
@ -36,28 +36,20 @@ class KeysExporter(private val session: Session) {
* Export keys and return the file path with the callback
*/
fun export(context: Context, password: String, callback: MatrixCallback<String>) {
session.exportRoomKeys(password, object : MatrixCallback<ByteArray> {
override fun onSuccess(data: ByteArray) {
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
Try {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")
GlobalScope.launch(Dispatchers.Main) {
runCatching {
val data = awaitCallback<ByteArray> { session.exportRoomKeys(password, it) }
withContext(Dispatchers.IO) {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")
writeToFile(data, file)
writeToFile(data, file)
addEntryToDownloadManager(context, file, "text/plain")
addEntryToDownloadManager(context, file, "text/plain")
file.absolutePath
}
}
.foldToCallback(callback)
file.absolutePath
}
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
})
}.foldToCallback(callback)
}
}
}

View File

@ -18,10 +18,11 @@ package im.vector.riotx.features.crypto.keys
import android.content.Context
import android.net.Uri
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.core.intent.getMimeTypeFromUri
import im.vector.riotx.core.resources.openResource
import kotlinx.coroutines.Dispatchers
@ -41,8 +42,8 @@ class KeysImporter(private val session: Session) {
password: String,
callback: MatrixCallback<ImportRoomKeysResult>) {
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
Try {
runCatching {
withContext(Dispatchers.IO) {
val resource = openResource(context, uri, mimetype ?: getMimeTypeFromUri(context, uri))
if (resource?.mContentStream == null) {
@ -51,33 +52,17 @@ class KeysImporter(private val session: Session) {
val data: ByteArray
try {
data = ByteArray(resource.mContentStream!!.available())
resource.mContentStream!!.read(data)
resource.mContentStream!!.close()
data
data = resource.mContentStream!!.use { it.readBytes() }
} catch (e: Exception) {
try {
resource.mContentStream!!.close()
} catch (e2: Exception) {
Timber.e(e2, "## importKeys()")
}
Timber.e(e, "## importKeys()")
throw e
}
awaitCallback<ImportRoomKeysResult> {
session.importRoomKeys(data, password, null, it)
}
}
}
.fold(
{
callback.onFailure(it)
},
{ byteArray ->
session.importRoomKeys(byteArray,
password,
null,
callback)
}
)
}.foldToCallback(callback)
}
}
}

View File

@ -31,7 +31,7 @@ import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.GenericItem
import im.vector.riotx.core.ui.list.genericItem
import java.util.*
import java.util.UUID
import javax.inject.Inject
class KeysBackupSettingsRecyclerViewController @Inject constructor(private val stringProvider: StringProvider,

View File

@ -170,8 +170,8 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
private fun exportRecoveryKeyToFile(data: String) {
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
Try {
Try {
withContext(Dispatchers.IO) {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")

View File

@ -81,8 +81,6 @@ import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.utils.*
import im.vector.riotx.core.utils.Debouncer
import im.vector.riotx.core.utils.createUIHandler
import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.attachments.ContactAttachment
@ -412,7 +410,7 @@ class RoomDetailFragment :
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
avatarRenderer.render(event.senderAvatar, event.root.senderId ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
avatarRenderer.render(event.senderAvatar, event.root.senderId ?: "", event.getDisambiguatedDisplayName(), composerLayout.composerRelatedMessageAvatar)
composerLayout.expand {
// need to do it here also when not using quick reply
focusComposerAndShowKeyboard()
@ -483,7 +481,7 @@ class RoomDetailFragment :
jumpToReadMarkerView.render(show, readMarkerId)
}
}
recyclerView.setController(timelineEventController)
recyclerView.adapter = timelineEventController.adapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
@ -622,19 +620,27 @@ class RoomDetailFragment :
}
composerLayout.callback = object : TextComposerView.Callback {
override fun onRichContentSelected(contentUri: Uri): Boolean {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
data = contentUri
// We need WRITE_EXTERNAL permission
return if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this@RoomDetailFragment, PERMISSION_REQUEST_CODE_INCOMING_URI)) {
sendUri(contentUri)
} else {
roomDetailViewModel.pendingUri = contentUri
// Always intercept when we request some permission
true
}
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
if (!isHandled) {
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
}
return isHandled
}
}
}
private fun sendUri(uri: Uri): Boolean {
val shareIntent = Intent(Intent.ACTION_SEND, uri)
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
if (!isHandled) {
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
}
return isHandled
}
private fun setupAttachmentButton() {
composerLayout.attachmentButton.setOnClickListener {
if (!::attachmentTypeSelector.isInitialized) {
@ -909,19 +915,34 @@ class RoomDetailFragment :
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) {
val action = roomDetailViewModel.pendingAction
if (action != null) {
roomDetailViewModel.pendingAction = null
roomDetailViewModel.process(action)
when (requestCode) {
PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> {
val action = roomDetailViewModel.pendingAction
if (action != null) {
roomDetailViewModel.pendingAction = null
roomDetailViewModel.process(action)
}
}
} else if (requestCode == PERMISSION_REQUEST_CODE_PICK_ATTACHMENT) {
val pendingType = attachmentsHelper.pendingType
if (pendingType != null) {
attachmentsHelper.pendingType = null
launchAttachmentProcess(pendingType)
PERMISSION_REQUEST_CODE_INCOMING_URI -> {
val pendingUri = roomDetailViewModel.pendingUri
if (pendingUri != null) {
roomDetailViewModel.pendingUri = null
sendUri(pendingUri)
}
}
PERMISSION_REQUEST_CODE_PICK_ATTACHMENT -> {
val pendingType = attachmentsHelper.pendingType
if (pendingType != null) {
attachmentsHelper.pendingType = null
launchAttachmentProcess(pendingType)
}
}
}
} else {
// Reset all pending data
roomDetailViewModel.pendingAction = null
roomDetailViewModel.pendingUri = null
attachmentsHelper.pendingType = null
}
}

View File

@ -16,6 +16,7 @@
package im.vector.riotx.features.home.room.detail
import android.net.Uri
import androidx.annotation.IdRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@ -95,6 +96,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
// Slot to keep a pending action during permission request
var pendingAction: RoomDetailActions? = null
// Slot to keep a pending uri during permission request
var pendingUri: Uri? = null
@AssistedInject.Factory
interface Factory {

View File

@ -76,11 +76,8 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState:
Observable.combineLatest<List<String>, Option<AutocompleteUserQuery>, List<User>>(
room.rx().liveRoomMemberIds(),
usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
BiFunction { roomMembers, query ->
val users = roomMembers
.mapNotNull {
session.getUser(it)
}
BiFunction { roomMemberIds, query ->
val users = roomMemberIds.mapNotNull { session.getUser(it) }
val filter = query.orNull()
if (filter.isNullOrBlank()) {

View File

@ -42,7 +42,8 @@ import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventForm
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
/**
* Quick reactions state

View File

@ -38,7 +38,7 @@ import im.vector.riotx.features.html.EventHtmlRenderer
import me.gujun.android.span.span
import name.fraser.neil.plaintext.diff_match_patch
import timber.log.Timber
import java.util.*
import java.util.Calendar
/**
* Epoxy controller for edit history list
@ -94,7 +94,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
val body = cContent.second?.let { eventHtmlRenderer.render(it) }
?: cContent.first
val nextEvent = if (index + 1 <= sourceEvents.lastIndex) sourceEvents[index + 1] else null
val nextEvent = sourceEvents.getOrNull(index + 1)
var spannedDiff: Spannable? = null
if (nextEvent != null && cContent.second == null /*No diff for html*/) {

View File

@ -30,7 +30,7 @@ import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import timber.log.Timber
import java.util.*
import java.util.UUID
data class ViewEditHistoryViewState(
val eventId: String,

View File

@ -27,8 +27,6 @@ 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.senderAvatar
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
@ -41,13 +39,13 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?): NoticeItem? {
val text = buildNoticeText(event.root, event.senderName) ?: return null
val text = buildNoticeText(event.root, event.getDisambiguatedDisplayName()) ?: return null
val informationData = MessageInformationData(
eventId = event.root.eventId ?: "?",
senderId = event.root.senderId ?: "",
sendState = event.root.sendState,
avatarUrl = event.senderAvatar(),
memberName = event.senderName(),
avatarUrl = event.senderAvatar,
memberName = event.getDisambiguatedDisplayName(),
showInformation = false
)
val attributes = NoticeItem.Attributes(

View File

@ -64,12 +64,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
if (!showReadMarker && mergedEvent.hasReadMarker && readMarkerVisible) {
showReadMarker = true
}
val senderAvatar = mergedEvent.senderAvatar()
val senderName = mergedEvent.senderName()
val senderAvatar = mergedEvent.senderAvatar
val senderName = mergedEvent.getDisambiguatedDisplayName()
val data = MergedHeaderItem.Data(
userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar,
memberName = senderName ?: "",
memberName = senderName,
localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: ""
)

View File

@ -46,9 +46,11 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle
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.html.EventHtmlRenderer
import im.vector.riotx.features.html.CodeVisitor
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import me.gujun.android.span.span
import org.commonmark.node.Document
import javax.inject.Inject
class MessageItemFactory @Inject constructor(
@ -97,16 +99,8 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent()
// val ev = all.toModel<Event>()
return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData,
highlight,
callback,
attributes)
is MessageTextContent -> buildTextMessageItem(messageContent,
informationData,
highlight,
callback,
attributes)
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
@ -229,34 +223,75 @@ class MessageItemFactory @Inject constructor(
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
}
private fun buildTextMessageItem(messageContent: MessageTextContent,
private fun buildItemForTextContent(messageContent: MessageTextContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
return if (isFormatted) {
val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document
val codeVisitor = CodeVisitor()
codeVisitor.visit(localFormattedBody)
when (codeVisitor.codeKind) {
CodeVisitor.Kind.BLOCK -> {
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
buildCodeBlockItem(codeFormattedBlock, informationData, highlight, callback, attributes)
}
CodeVisitor.Kind.INLINE -> {
val codeFormatted = htmlRenderer.get().render(localFormattedBody)
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
}
CodeVisitor.Kind.NONE -> {
val formattedBody = htmlRenderer.get().render(messageContent.formattedBody!!)
buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
}
}
} else {
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
}
}
private fun buildMessageTextItem(body: CharSequence,
isFormatted: Boolean,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
val bodyToUse = messageContent.formattedBody?.let {
htmlRenderer.get().render(it.trim())
} ?: messageContent.body
val linkifiedBody = linkifyBody(body, callback)
val linkifiedBody = linkifyBody(bodyToUse, callback)
return MessageTextItem_()
.apply {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited(linkifiedBody, callback, informationData)
message(spannable)
} else {
message(linkifiedBody)
}
}
return MessageTextItem_().apply {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited(linkifiedBody, callback, informationData)
message(spannable)
} else {
message(linkifiedBody)
}
}
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
.searchForPills(isFormatted)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.urlClickCallback(callback)
// click on the text
}
private fun buildCodeBlockItem(formattedBody: CharSequence,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageBlockCodeItem? {
return MessageBlockCodeItem_()
.apply {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited("", callback, informationData)
editedSpan(spannable)
}
}
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.message(formattedBody)
}
private fun annotateWithEdited(linkifiedBody: CharSequence,

View File

@ -19,13 +19,17 @@ package im.vector.riotx.features.home.room.detail.timeline.format
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.*
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomMember
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.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import timber.log.Timber
import javax.inject.Inject
@ -36,7 +40,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
return when (val type = timelineEvent.root.getClearType()) {
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName())
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName())
EventType.CALL_INVITE,
@ -96,7 +100,8 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
}
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
?: return null
val formattedVisibility = when (historyVisibility) {
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
@ -135,7 +140,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
}
}
private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String {
val displayText = StringBuilder()
// Check display name has been changed
if (eventContent?.displayName != prevEventContent?.displayName) {
@ -146,7 +151,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName)
else ->
stringProvider.getString(R.string.notice_display_name_changed_from,
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
}
displayText.append(displayNameText)
}
@ -160,6 +165,11 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
}
displayText.append(displayAvatarText)
}
if (displayText.isEmpty()) {
displayText.append(
stringProvider.getString(R.string.notice_member_no_changes, senderName)
)
}
return displayText.toString()
}
@ -171,9 +181,10 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId
when {
eventContent.thirdPartyInvite != null -> {
val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey
val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid
?: event.stateKey
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
userWhoHasAccepted, eventContent.thirdPartyInvite?.displayName)
userWhoHasAccepted, eventContent.thirdPartyInvite?.displayName)
}
event.stateKey == selfUserId ->
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)

View File

@ -17,8 +17,6 @@
package im.vector.riotx.features.home.room.detail.timeline.helper
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.extensions.localDateTime
@ -47,25 +45,6 @@ object TimelineDisplayableEvents {
)
}
fun TimelineEvent.senderAvatar(): String? {
// We might have no avatar when user leave, so we try to get it from prevContent
return senderAvatar
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
root.prevContent.toModel<RoomMember>()?.avatarUrl
} else {
null
}
}
fun TimelineEvent.senderName(): String? {
// We might have no senderName when user leave, so we try to get it from prevContent
return when {
senderName != null -> getDisambiguatedDisplayName()
root.type == EventType.STATE_ROOM_MEMBER -> root.prevContent.toModel<RoomMember>()?.displayName
else -> null
}
}
fun TimelineEvent.canBeMerged(): Boolean {
return root.getClearType() == EventType.STATE_ROOM_MEMBER
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2019 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.timeline.item
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.extensions.setTextOrHide
import me.saket.bettermovementmethod.BetterLinkMovementMethod
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageBlockCodeItem : AbsMessageItem<MessageBlockCodeItem.Holder>() {
@EpoxyAttribute
var message: CharSequence? = null
@EpoxyAttribute
var editedSpan: CharSequence? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.messageView.text = message
renderSendState(holder.messageView, holder.messageView)
holder.messageView.setOnClickListener(attributes.itemClickListener)
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
holder.editedView.movementMethod = BetterLinkMovementMethod.getInstance()
holder.editedView.setTextOrHide(editedSpan)
}
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val messageView by bind<TextView>(R.id.codeBlockTextView)
val editedView by bind<TextView>(R.id.codeBlockEditedView)
}
companion object {
private const val STUB_ID = R.id.messageContentCodeBlockStub
}
}

View File

@ -14,20 +14,14 @@
* limitations under the License.
*/
package im.vector.riotx
package im.vector.riotx.features.home.room.list
import org.junit.Test
import dagger.Binds
import dagger.Module
import org.junit.Assert.*
@Module
abstract class RoomListModule {
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
@Binds
abstract fun providesRoomListViewModelFactory(roomListViewModelFactory: RoomListViewModelFactory): RoomListViewModel.Factory
}

View File

@ -21,8 +21,6 @@ import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
@ -31,18 +29,18 @@ import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.home.HomeRoomListObservableStore
import im.vector.riotx.core.utils.RxStore
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import javax.inject.Inject
class RoomListViewModel @AssistedInject constructor(@Assisted initialState: RoomListViewState,
private val session: Session,
private val homeRoomListObservableSource: HomeRoomListObservableStore,
private val alphabeticalRoomComparator: AlphabeticalRoomComparator,
private val chronologicalRoomComparator: ChronologicalRoomComparator)
class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
private val session: Session,
private val roomSummariesStore: RxStore<List<RoomSummary>>,
private val alphabeticalRoomComparator: AlphabeticalRoomComparator,
private val chronologicalRoomComparator: ChronologicalRoomComparator)
: VectorViewModel<RoomListViewState>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: RoomListViewState): RoomListViewModel
}
@ -103,7 +101,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
}
private fun observeRoomSummaries() {
homeRoomListObservableSource
roomSummariesStore
.observe()
.observeOn(Schedulers.computation())
.map {
@ -113,7 +111,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
copy(asyncRooms = asyncRooms)
}
homeRoomListObservableSource
roomSummariesStore
.observe()
.observeOn(Schedulers.computation())
.map { buildRoomSummaries(it) }

View File

@ -0,0 +1,39 @@
/*
* Copyright 2019 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.list
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.features.home.HomeRoomListObservableStore
import im.vector.riotx.features.share.ShareRoomListObservableStore
import javax.inject.Inject
import javax.inject.Provider
class RoomListViewModelFactory @Inject constructor(private val session: Provider<Session>,
private val homeRoomListObservableStore: Provider<HomeRoomListObservableStore>,
private val shareRoomListObservableStore: Provider<ShareRoomListObservableStore>,
private val alphabeticalRoomComparator: Provider<AlphabeticalRoomComparator>,
private val chronologicalRoomComparator: Provider<ChronologicalRoomComparator>) : RoomListViewModel.Factory {
override fun create(initialState: RoomListViewState): RoomListViewModel {
return RoomListViewModel(
initialState,
session.get(),
if (initialState.displayMode == RoomListFragment.DisplayMode.SHARE) shareRoomListObservableStore.get() else homeRoomListObservableStore.get(),
alphabeticalRoomComparator.get(),
chronologicalRoomComparator.get())
}
}

View File

@ -20,6 +20,8 @@ import androidx.annotation.StringRes
import com.airbnb.epoxy.EpoxyController
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.noResultItem
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.filtered.FilteredRoomFooterItem
import im.vector.riotx.features.home.room.filtered.filteredRoomFooterItem
@ -47,24 +49,28 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
override fun buildModels() {
val nonNullViewState = viewState ?: return
if (nonNullViewState.displayMode == RoomListFragment.DisplayMode.FILTERED) {
buildFilteredRooms(nonNullViewState)
} else {
val roomSummaries = nonNullViewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) {
return@forEach
} else {
val isExpanded = nonNullViewState.isCategoryExpanded(category)
buildRoomCategory(nonNullViewState, summaries, category.titleRes, nonNullViewState.isCategoryExpanded(category)) {
listener?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries,
nonNullViewState.joiningRoomsIds,
nonNullViewState.joiningErrorRoomsIds,
nonNullViewState.rejectingRoomsIds,
nonNullViewState.rejectingErrorRoomsIds)
when (nonNullViewState.displayMode) {
RoomListFragment.DisplayMode.FILTERED,
RoomListFragment.DisplayMode.SHARE -> {
buildFilteredRooms(nonNullViewState)
}
else -> {
val roomSummaries = nonNullViewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) {
return@forEach
} else {
val isExpanded = nonNullViewState.isCategoryExpanded(category)
buildRoomCategory(nonNullViewState, summaries, category.titleRes, nonNullViewState.isCategoryExpanded(category)) {
listener?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries,
nonNullViewState.joiningRoomsIds,
nonNullViewState.joiningErrorRoomsIds,
nonNullViewState.rejectingRoomsIds,
nonNullViewState.rejectingErrorRoomsIds)
}
}
}
}
@ -80,12 +86,15 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
.filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
buildRoomModels(filteredSummaries,
viewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds)
viewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds)
addFilterFooter(viewState)
when {
viewState.displayMode == RoomListFragment.DisplayMode.FILTERED -> addFilterFooter(viewState)
filteredSummaries.isEmpty() -> addEmptyFooter()
}
}
private fun addFilterFooter(viewState: RoomListViewState) {
@ -96,6 +105,13 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
}
}
private fun addEmptyFooter() {
noResultItem {
id("no_result")
text(stringProvider.getString(R.string.no_result_placeholder))
}
}
private fun buildRoomCategory(viewState: RoomListViewState,
summaries: List<RoomSummary>,
@StringRes titleRes: Int,

View File

@ -32,7 +32,6 @@ import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import me.gujun.android.span.span
import javax.inject.Inject
@ -99,10 +98,10 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
&& latestEvent.root.mxDecryptionResult == null) {
stringProvider.getString(R.string.encrypted_message)
} else if (latestEvent.root.getClearType() == EventType.MESSAGE) {
val senderName = latestEvent.senderName() ?: latestEvent.root.senderId
val senderName = latestEvent.getDisambiguatedDisplayName()
val content = latestEvent.root.getClearContent()?.toModel<MessageContent>()
val message = content?.body ?: ""
if (roomSummary.isDirect.not() && senderName != null) {
if (roomSummary.isDirect.not()) {
span {
text = senderName
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)

View File

@ -0,0 +1,55 @@
/*
* Copyright 2019 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.html
import org.commonmark.node.AbstractVisitor
import org.commonmark.node.Code
import org.commonmark.node.FencedCodeBlock
import org.commonmark.node.IndentedCodeBlock
/**
* This class is in charge of visiting nodes and tells if we have some code nodes (inline or block).
*/
class CodeVisitor : AbstractVisitor() {
var codeKind: Kind = Kind.NONE
private set
override fun visit(fencedCodeBlock: FencedCodeBlock?) {
if (codeKind == Kind.NONE) {
codeKind = Kind.BLOCK
}
}
override fun visit(indentedCodeBlock: IndentedCodeBlock?) {
if (codeKind == Kind.NONE) {
codeKind = Kind.BLOCK
}
}
override fun visit(code: Code?) {
if (codeKind == Kind.NONE) {
codeKind = Kind.INLINE
}
}
enum class Kind {
NONE,
INLINE,
BLOCK
}
}

View File

@ -17,171 +17,46 @@
package im.vector.riotx.features.html
import android.content.Context
import android.text.style.URLSpan
import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequests
import im.vector.riotx.features.home.AvatarRenderer
import org.commonmark.node.BlockQuote
import org.commonmark.node.HtmlBlock
import org.commonmark.node.HtmlInline
import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.html.TagHandlerNoOp
import org.commonmark.node.Node
import ru.noties.markwon.*
import ru.noties.markwon.html.HtmlTag
import ru.noties.markwon.html.MarkwonHtmlParserImpl
import ru.noties.markwon.html.MarkwonHtmlRenderer
import ru.noties.markwon.html.TagHandler
import ru.noties.markwon.html.tag.*
import java.util.Arrays.asList
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class EventHtmlRenderer @Inject constructor(context: Context,
avatarRenderer: AvatarRenderer,
sessionHolder: ActiveSessionHolder) {
htmlConfigure: MatrixHtmlPluginConfigure) {
private val markwon = Markwon.builder(context)
.usePlugin(MatrixPlugin.create(GlideApp.with(context), context, avatarRenderer, sessionHolder))
.usePlugin(HtmlPlugin.create(htmlConfigure))
.build()
fun parse(text: String): Node {
return markwon.parse(text)
}
fun render(text: String): CharSequence {
return markwon.toMarkdown(text)
}
fun render(node: Node) : CharSequence {
fun render(node: Node): CharSequence {
return markwon.render(node)
}
}
private class MatrixPlugin private constructor(private val glideRequests: GlideRequests,
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val session: ActiveSessionHolder) : AbstractMarkwonPlugin() {
class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.htmlParser(MarkwonHtmlParserImpl.create())
}
override fun configureHtmlRenderer(builder: MarkwonHtmlRenderer.Builder) {
builder
.setHandler(
"img",
ImageHandler.create())
.setHandler(
"a",
MxLinkHandler(glideRequests, context, avatarRenderer, session))
.setHandler(
"blockquote",
BlockquoteHandler())
.setHandler(
"font",
FontTagHandler())
.setHandler(
"sub",
SubScriptHandler())
.setHandler(
"sup",
SuperScriptHandler())
.setHandler(
asList<String>("b", "strong"),
StrongEmphasisHandler())
.setHandler(
asList<String>("s", "del"),
StrikeHandler())
.setHandler(
asList<String>("u", "ins"),
UnderlineHandler())
.setHandler(
asList<String>("ul", "ol"),
ListHandler())
.setHandler(
asList<String>("i", "em", "cite", "dfn"),
EmphasisHandler())
.setHandler(
asList<String>("h1", "h2", "h3", "h4", "h5", "h6"),
HeadingHandler())
.setHandler("mx-reply",
MxReplyTagHandler())
}
override fun afterRender(node: Node, visitor: MarkwonVisitor) {
val configuration = visitor.configuration()
configuration.htmlRenderer().render(visitor, configuration.htmlParser())
}
override fun configureVisitor(builder: MarkwonVisitor.Builder) {
builder
.on(HtmlBlock::class.java) { visitor, htmlBlock -> visitHtml(visitor, htmlBlock.literal) }
.on(HtmlInline::class.java) { visitor, htmlInline -> visitHtml(visitor, htmlInline.literal) }
}
private fun visitHtml(visitor: MarkwonVisitor, html: String?) {
if (html != null) {
visitor.configuration().htmlParser().processFragment(visitor.builder(), html)
}
}
companion object {
fun create(glideRequests: GlideRequests, context: Context, avatarRenderer: AvatarRenderer, session: ActiveSessionHolder): MatrixPlugin {
return MatrixPlugin(glideRequests, context, avatarRenderer, session)
}
}
}
private class MxLinkHandler(private val glideRequests: GlideRequests,
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val sessionHolder: ActiveSessionHolder) : TagHandler() {
private val linkHandler = LinkHandler()
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val link = tag.attributes()["href"]
if (link != null) {
val permalinkData = PermalinkParser.parse(link)
when (permalinkData) {
is PermalinkData.UserLink -> {
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user)
SpannableBuilder.setSpans(
visitor.builder(),
span,
tag.start(),
tag.end()
)
// also add clickable span
SpannableBuilder.setSpans(
visitor.builder(),
URLSpan(link),
tag.start(),
tag.end()
)
}
else -> linkHandler.handle(visitor, renderer, tag)
}
} else {
linkHandler.handle(visitor, renderer, tag)
}
}
}
private class MxReplyTagHandler : TagHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val configuration = visitor.configuration()
val factory = configuration.spansFactory().get(BlockQuote::class.java)
if (factory != null) {
SpannableBuilder.setSpans(
visitor.builder(),
factory.getSpans(configuration, visitor.renderProps()),
tag.start(),
tag.end()
)
val replyText = visitor.builder().removeFromEnd(tag.end())
visitor.builder().append("\n\n").append(replyText)
}
override fun configureHtml(plugin: HtmlPlugin) {
plugin
.addHandler(TagHandlerNoOp.create("a"))
.addHandler(FontTagHandler())
.addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session))
.addHandler(MxReplyTagHandler())
}
}

View File

@ -17,15 +17,18 @@ package im.vector.riotx.features.html
import android.graphics.Color
import android.text.style.ForegroundColorSpan
import ru.noties.markwon.MarkwonConfiguration
import ru.noties.markwon.RenderProps
import ru.noties.markwon.html.HtmlTag
import ru.noties.markwon.html.tag.SimpleTagHandler
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.RenderProps
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.tag.SimpleTagHandler
/**
* custom to matrix for IRC-style font coloring
*/
class FontTagHandler : SimpleTagHandler() {
override fun supportedTags() = listOf("font")
override fun getSpans(configuration: MarkwonConfiguration, renderProps: RenderProps, tag: HtmlTag): Any? {
val colorString = tag.attributes()["color"]?.let { parseColor(it) } ?: Color.BLACK
return ForegroundColorSpan(colorString)
@ -37,23 +40,23 @@ class FontTagHandler : SimpleTagHandler() {
} catch (e: Exception) {
// try other w3c colors?
return when (color_name) {
"white" -> Color.WHITE
"yellow" -> Color.YELLOW
"white" -> Color.WHITE
"yellow" -> Color.YELLOW
"fuchsia" -> Color.parseColor("#FF00FF")
"red" -> Color.RED
"silver" -> Color.parseColor("#C0C0C0")
"gray" -> Color.GRAY
"olive" -> Color.parseColor("#808000")
"purple" -> Color.parseColor("#800080")
"maroon" -> Color.parseColor("#800000")
"aqua" -> Color.parseColor("#00FFFF")
"lime" -> Color.parseColor("#00FF00")
"teal" -> Color.parseColor("#008080")
"green" -> Color.GREEN
"blue" -> Color.BLUE
"orange" -> Color.parseColor("#FFA500")
"navy" -> Color.parseColor("#000080")
else -> Color.BLACK
"red" -> Color.RED
"silver" -> Color.parseColor("#C0C0C0")
"gray" -> Color.GRAY
"olive" -> Color.parseColor("#808000")
"purple" -> Color.parseColor("#800080")
"maroon" -> Color.parseColor("#800000")
"aqua" -> Color.parseColor("#00FFFF")
"lime" -> Color.parseColor("#00FF00")
"teal" -> Color.parseColor("#008080")
"green" -> Color.GREEN
"blue" -> Color.BLUE
"orange" -> Color.parseColor("#FFA500")
"navy" -> Color.parseColor("#000080")
else -> Color.BLACK
}
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2019 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.html
import android.content.Context
import android.text.style.URLSpan
import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideRequests
import im.vector.riotx.features.home.AvatarRenderer
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.SpannableBuilder
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.MarkwonHtmlRenderer
import io.noties.markwon.html.tag.LinkHandler
class MxLinkTagHandler(private val glideRequests: GlideRequests,
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val sessionHolder: ActiveSessionHolder) : LinkHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val link = tag.attributes()["href"]
if (link != null) {
val permalinkData = PermalinkParser.parse(link)
when (permalinkData) {
is PermalinkData.UserLink -> {
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user)
SpannableBuilder.setSpans(
visitor.builder(),
span,
tag.start(),
tag.end()
)
// also add clickable span
SpannableBuilder.setSpans(
visitor.builder(),
URLSpan(link),
tag.start(),
tag.end()
)
}
else -> super.handle(visitor, renderer, tag)
}
} else {
super.handle(visitor, renderer, tag)
}
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2019 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.html
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.SpannableBuilder
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.MarkwonHtmlRenderer
import io.noties.markwon.html.TagHandler
import org.commonmark.node.BlockQuote
class MxReplyTagHandler : TagHandler() {
override fun supportedTags() = listOf("mx-reply")
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val configuration = visitor.configuration()
val factory = configuration.spansFactory().get(BlockQuote::class.java)
if (factory != null) {
SpannableBuilder.setSpans(
visitor.builder(),
factory.getSpans(configuration, visitor.renderProps()),
tag.start(),
tag.end()
)
val replyText = visitor.builder().removeFromEnd(tag.end())
visitor.builder().append("\n\n").append(replyText)
}
}
}

View File

@ -33,7 +33,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import timber.log.Timber
import java.util.*
import java.util.UUID
import javax.inject.Inject
/**
@ -94,7 +94,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
event.getLastMessageBody()
?: stringProvider.getString(R.string.notification_unknown_new_event)
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.senderName ?: event.root.senderId
val senderDisplayName = event.getDisambiguatedDisplayName()
val notifiableEvent = NotifiableMessageEvent(
eventId = event.root.eventId!!,
@ -128,7 +128,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
val body = event.getLastMessageBody()
?: stringProvider.getString(R.string.notification_unknown_new_event)
val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderName ?: event.root.senderId
val senderDisplayName = event.getDisambiguatedDisplayName()
val notifiableEvent = NotifiableMessageEvent(
eventId = event.root.eventId!!,

View File

@ -27,7 +27,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.vectorComponent
import timber.log.Timber
import java.util.*
import java.util.UUID
import javax.inject.Inject
/**

View File

@ -45,9 +45,9 @@ import im.vector.riotx.features.home.room.detail.RoomDetailActivity
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.settings.VectorPreferences
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.random.Random
/**
* Util class for creating notifications.
@ -299,7 +299,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
// use a generator for the private requestCode.
// When using 0, the intent is not created/launched when the user taps on the notification.
//
val pendingIntent = stackBuilder.getPendingIntent(Random().nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT)
val pendingIntent = stackBuilder.getPendingIntent(Random.nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT)
builder.setContentIntent(pendingIntent)
@ -599,7 +599,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
val intent = HomeActivity.newIntent(context, clearNotification = true)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
intent.data = Uri.parse("foobar://tapSummary")
return PendingIntent.getActivity(context, Random().nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT)
return PendingIntent.getActivity(context, Random.nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/*

View File

@ -46,7 +46,7 @@ import org.json.JSONObject
import timber.log.Timber
import java.io.*
import java.net.HttpURLConnection
import java.util.*
import java.util.Locale
import java.util.zip.GZIPOutputStream
import javax.inject.Inject
import javax.inject.Singleton

View File

@ -24,7 +24,9 @@ import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.logging.*
import java.util.logging.Formatter
import javax.inject.Inject

View File

@ -21,12 +21,16 @@ import android.content.res.Configuration
import android.os.Build
import android.preference.PreferenceManager
import androidx.core.content.edit
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Locale
import kotlin.Comparator
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
/**
* Object to manage the Locale choice of the user
@ -35,6 +39,7 @@ object VectorLocale {
private const val APPLICATION_LOCALE_COUNTRY_KEY = "APPLICATION_LOCALE_COUNTRY_KEY"
private const val APPLICATION_LOCALE_VARIANT_KEY = "APPLICATION_LOCALE_VARIANT_KEY"
private const val APPLICATION_LOCALE_LANGUAGE_KEY = "APPLICATION_LOCALE_LANGUAGE_KEY"
private const val APPLICATION_LOCALE_SCRIPT_KEY = "APPLICATION_LOCALE_SCRIPT_KEY"
private val defaultLocale = Locale("en", "US")
@ -106,6 +111,15 @@ object VectorLocale {
} else {
putString(APPLICATION_LOCALE_VARIANT_KEY, variant)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val script = locale.script
if (script.isEmpty()) {
remove(APPLICATION_LOCALE_SCRIPT_KEY)
} else {
putString(APPLICATION_LOCALE_SCRIPT_KEY, script)
}
}
}
}
@ -159,24 +173,43 @@ object VectorLocale {
* @param context the context
*/
private fun initApplicationLocales(context: Context) {
val knownLocalesSet = HashSet<Pair<String, String>>()
val knownLocalesSet = HashSet<Triple<String, String, String>>()
try {
val availableLocales = Locale.getAvailableLocales()
for (locale in availableLocales) {
knownLocalesSet.add(Pair(getString(context, locale, R.string.resources_language),
getString(context, locale, R.string.resources_country_code)))
knownLocalesSet.add(
Triple(
getString(context, locale, R.string.resources_language),
getString(context, locale, R.string.resources_country_code),
getString(context, locale, R.string.resources_script)
)
)
}
} catch (e: Exception) {
Timber.e(e, "## getApplicationLocales() : failed")
knownLocalesSet.add(Pair(context.getString(R.string.resources_language), context.getString(R.string.resources_country_code)))
knownLocalesSet.add(
Triple(
context.getString(R.string.resources_language),
context.getString(R.string.resources_country_code),
context.getString(R.string.resources_script)
)
)
}
supportedLocales.clear()
knownLocalesSet.mapTo(supportedLocales) { (language, country) ->
Locale(language, country)
knownLocalesSet.mapTo(supportedLocales) { (language, country, script) ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Locale.Builder()
.setLanguage(language)
.setRegion(country)
.setScript(script)
.build()
} else {
Locale(language, country)
}
}
// sort by human display names
@ -190,12 +223,37 @@ object VectorLocale {
* @return the string
*/
fun localeToLocalisedString(locale: Locale): String {
var res = locale.getDisplayLanguage(locale)
return buildString {
append(locale.getDisplayLanguage(locale))
if (locale.getDisplayCountry(locale).isNotEmpty()) {
res += " (" + locale.getDisplayCountry(locale) + ")"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& locale.script != "Latn"
&& locale.getDisplayScript(locale).isNotEmpty()) {
append(" - ")
append(locale.getDisplayScript(locale))
}
if (locale.getDisplayCountry(locale).isNotEmpty()) {
append(" (")
append(locale.getDisplayCountry(locale))
append(")")
}
// In debug mode, also display information about the locale in the current locale.
if (BuildConfig.DEBUG) {
append("\n[")
append(locale.displayLanguage)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && locale.script != "Latn") {
append(" - ")
append(locale.displayScript)
}
if (locale.displayCountry.isNotEmpty()) {
append(" (")
append(locale.displayCountry)
append(")")
}
append("]")
}
}
return res
}
}

View File

@ -576,7 +576,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
* @return true if the markdown is enabled
*/
fun isMarkdownEnabled(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, true)
return defaultPrefs.getBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, false)
}
/**

View File

@ -51,7 +51,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.*
class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {

View File

@ -56,7 +56,8 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActiv
import timber.log.Timber
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() {

View File

@ -20,6 +20,8 @@ import android.content.ClipDescription
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import com.airbnb.mvrx.viewModel
import com.kbeanie.multipicker.utils.IntentUtils
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.riotx.R
@ -39,8 +41,10 @@ class IncomingShareActivity :
VectorBaseActivity(), AttachmentsHelper.Callback {
@Inject lateinit var sessionHolder: ActiveSessionHolder
private lateinit var roomListFragment: RoomListFragment
@Inject lateinit var incomingShareViewModelFactory: IncomingShareViewModel.Factory
private var roomListFragment: RoomListFragment? = null
private lateinit var attachmentsHelper: AttachmentsHelper
private val incomingShareViewModel: IncomingShareViewModel by viewModel()
override fun getLayoutRes(): Int {
return R.layout.activity_incoming_share
@ -77,12 +81,23 @@ class IncomingShareActivity :
} else {
cannotManageShare()
}
incomingShareSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
override fun onQueryTextChange(newText: String): Boolean {
roomListFragment?.filterRoomsWith(newText)
return true
}
})
}
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Attachments(attachments))
roomListFragment = RoomListFragment.newInstance(roomListParams)
replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer)
.also { replaceFragment(it, R.id.shareRoomListFragmentContainer) }
}
override fun onAttachmentsProcessFailed() {
@ -102,7 +117,7 @@ class IncomingShareActivity :
} else {
val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Text(sharedText))
roomListFragment = RoomListFragment.newInstance(roomListParams)
replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer)
.also { replaceFragment(it, R.id.shareRoomListFragmentContainer) }
true
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright 2019 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.share
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.rx.rx
import im.vector.riotx.ActiveSessionObservableStore
import im.vector.riotx.core.platform.VectorViewModel
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
data class IncomingShareState(private val dummy: Boolean = false) : MvRxState
/**
* View model used to observe the room list and post update to the ShareRoomListObservableStore
*/
class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: IncomingShareState,
private val sessionObservableStore: ActiveSessionObservableStore,
private val shareRoomListObservableStore: ShareRoomListObservableStore)
: VectorViewModel<IncomingShareState>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: IncomingShareState): IncomingShareViewModel
}
companion object : MvRxViewModelFactory<IncomingShareViewModel, IncomingShareState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: IncomingShareState): IncomingShareViewModel? {
val activity: IncomingShareActivity = (viewModelContext as ActivityViewModelContext).activity()
return activity.incomingShareViewModelFactory.create(state)
}
}
init {
observeRoomSummaries()
}
private fun observeRoomSummaries() {
sessionObservableStore.observe()
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
it.orNull()?.rx()?.liveRoomSummaries()
?: Observable.just(emptyList())
}
.throttleLast(300, TimeUnit.MILLISECONDS)
.subscribe {
shareRoomListObservableStore.post(it)
}
.disposeOnClear()
}
}

View File

@ -14,20 +14,12 @@
* limitations under the License.
*/
package im.vector.matrix.rx;
package im.vector.riotx.features.share
import org.junit.Test;
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.core.utils.RxStore
import javax.inject.Inject
import javax.inject.Singleton
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}
@Singleton
class ShareRoomListObservableStore @Inject constructor() : RxStore<List<RoomSummary>>()

View File

@ -28,7 +28,6 @@ import androidx.core.graphics.drawable.DrawableCompat
import androidx.preference.PreferenceManager
import im.vector.riotx.R
import timber.log.Timber
import java.util.*
/**
* Util class for managing themes.
@ -131,24 +130,16 @@ object ThemeUtils {
*/
@ColorInt
fun getColor(c: Context, @AttrRes colorAttribute: Int): Int {
if (mColorByAttr.containsKey(colorAttribute)) {
return mColorByAttr[colorAttribute] as Int
return mColorByAttr.getOrPut(colorAttribute) {
try {
val color = TypedValue()
c.theme.resolveAttribute(colorAttribute, color, true)
color.data
} catch (e: Exception) {
Timber.e(e, "Unable to get color")
ContextCompat.getColor(c, android.R.color.holo_red_dark)
}
}
var matchedColor: Int
try {
val color = TypedValue()
c.theme.resolveAttribute(colorAttribute, color, true)
matchedColor = color.data
} catch (e: Exception) {
Timber.e(e, "Unable to get color")
matchedColor = ContextCompat.getColor(c, android.R.color.holo_red_dark)
}
mColorByAttr[colorAttribute] = matchedColor
return matchedColor
}
fun getAttribute(c: Context, @AttrRes attribute: Int): TypedValue? {

View File

@ -86,7 +86,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomToolbar" />
<com.airbnb.epoxy.EpoxyRecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"

View File

@ -78,6 +78,13 @@
android:layout="@layout/item_timeline_event_text_message_stub"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentCodeBlockStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_code_block_stub"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentMediaStub"
style="@style/TimelineContentStubBaseParams"

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/codeBlockTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textSize="14sp" />
</HorizontalScrollView>
<TextView
android:id="@+id/codeBlockEditedView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
</LinearLayout>

View File

@ -30,10 +30,12 @@
android:id="@+id/attachmentCameraButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_camera_white_24dp"
android:contentDescription="@string/attachment_type_camera"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_camera" />
</LinearLayout>
@ -50,10 +52,12 @@
android:id="@+id/attachmentGalleryButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_gallery_white_24dp"
android:contentDescription="@string/attachment_type_gallery"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_gallery" />
</LinearLayout>
@ -70,10 +74,12 @@
android:id="@+id/attachmentFileButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_file_white_24dp"
android:contentDescription="@string/attachment_type_file"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_file" />
</LinearLayout>
@ -99,10 +105,12 @@
android:id="@+id/attachmentAudioButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_audio_white_24dp"
android:contentDescription="@string/attachment_type_audio"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_audio" />
</LinearLayout>
@ -119,10 +127,12 @@
android:id="@+id/attachmentContactButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_contact_white_24dp"
android:contentDescription="@string/attachment_type_contact"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_contact" />
</LinearLayout>
@ -139,14 +149,16 @@
android:id="@+id/attachmentStickersButton"
style="@style/AttachmentTypeSelectorButton"
android:src="@drawable/ic_attachment_stickers_white_24dp"
android:contentDescription="@string/attachment_type_sticker"
tools:background="@color/colorAccent" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_sticker" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,113 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="resources_language">sr</string>
<string name="resources_country_code">RS</string>
<string name="resources_script">Cyrl</string>
<string name="light_theme">Светла тема</string>
<string name="dark_theme">Тамна тема</string>
<string name="black_them">Црна тема</string>
<string name="status_theme">Status.im тема</string>
<string name="notification_sync_init">Иницијализација сервиса</string>
<string name="notification_sync_in_progress">Синхронизација у току…</string>
<string name="notification_noisy_notifications">Бучна обавештења</string>
<string name="notification_silent_notifications">Тиха обавештења</string>
<string name="title_activity_home">Поруке</string>
<string name="title_activity_room">Соба</string>
<string name="title_activity_settings">Подешавања</string>
<string name="title_activity_member_details">Подаци о члану</string>
<string name="title_activity_historical">Историјски</string>
<string name="title_activity_bug_report">Пријава грешке</string>
<string name="title_activity_choose_sticker">Пошаљи налепницу</string>
<string name="title_activity_keys_backup_setup">Резервна копија кључева</string>
<string name="title_activity_keys_backup_restore">Користи резервну копију кључева</string>
<string name="title_activity_verify_device">Верификуј уређај</string>
<string name="keys_backup_is_not_finished_please_wait">Креирање резервне копије кључева се није завршило, молим сачекајте…</string>
<string name="sign_out_bottom_sheet_warning_no_backup">Изгубићете ваше шифроване поруке ако се сад одјавите</string>
<string name="sign_out_bottom_sheet_warning_backing_up">Креирање резервне копије кључева је у току. Ако се одјавите сад, изгубићете приступ вашим шифрованим порукама.</string>
<string name="sign_out_bottom_sheet_warning_backup_not_active">Сигурносна копија кључева би требало да буде активна на свим вашим уређајима како би избегли губитак приступа вашим шифрованим порукама.</string>
<string name="sign_out_bottom_sheet_dont_want_secure_messages">Не желим моје шифроване поруке</string>
<string name="sign_out_bottom_sheet_backing_up_keys">Прављење резервне копије кључева у току…</string>
<string name="keys_backup_activate">Користи резервну копију кључева</string>
<string name="are_you_sure">Да ли сте сигурни\?</string>
<string name="sign_out_bottom_sheet_will_lose_secure_messages">Изгубићете приступ вашим шифрованим порукама уколико не направите резервну копију кључева пре него што се одјавите.</string>
<string name="loading">Учитавање…</string>
<string name="ok">У реду</string>
<string name="cancel">Откажи</string>
<string name="save">Сачувај</string>
<string name="leave">Напусти</string>
<string name="stay">Остани</string>
<string name="send">Пошаљи</string>
<string name="copy">Копирај</string>
<string name="resend">Пошаљи поново</string>
<string name="redact">Уклони</string>
<string name="share">Подели</string>
<string name="accept">Прихвати</string>
<string name="skip">Прескочи</string>
<string name="done">Готово</string>
<string name="abort">Обустави</string>
<string name="ignore">Игнориши</string>
<string name="review">Прегледај</string>
<string name="decline">Одбаци</string>
<string name="action_exit">Изађи</string>
<string name="actions">Акције</string>
<string name="action_sign_out">Одјави се</string>
<string name="action_sign_out_confirmation_simple">Да ли сте сигурни да желите да се одјавите\?</string>
<string name="action_voice_call">Гласовни позив</string>
<string name="action_video_call">Видео позив</string>
<string name="action_global_search">Глобална претрага</string>
<string name="action_mark_all_as_read">Означи све као прочитано</string>
<string name="action_quick_reply">Брзи одговор</string>
<string name="action_mark_room_read">Означи као прочитано</string>
<string name="action_open">Отвори</string>
<string name="action_close">Затвори</string>
<string name="disable">Онемогући</string>
<string name="dialog_title_confirmation">Потврда</string>
<string name="dialog_title_warning">Упозорење</string>
<string name="dialog_title_error">Грешка</string>
<string name="bottom_action_favourites">Омиљено</string>
<string name="bottom_action_people">Људи</string>
<string name="bottom_action_rooms">Собе</string>
<string name="invitations_header">Позивнице</string>
<string name="low_priority_header">Низак приоритет</string>
<string name="direct_chats_header">Разговори</string>
<string name="local_address_book_header">Локални адресар</string>
<string name="user_directory_header">Листа корисника</string>
<string name="matrix_only_filter">Само Matrix контакти</string>
<string name="no_result_placeholder">Нема резултата</string>
<string name="people_no_identity_server">Нема подешених сервера идентитета.</string>
<string name="rooms_header">Собе</string>
<string name="rooms_directory_header">Листа соба</string>
<string name="no_room_placeholder">Нема соба</string>
<string name="groups_invite_header">Пошаљи позивницу</string>
<string name="send_bug_report_placeholder">Опишите ваш проблем овде</string>
<string name="read_receipt">Прочитај</string>
<string name="join_room">Придружи се соби</string>
<string name="username">Корисничко име</string>
<string name="create_account">Направи налог</string>
<string name="login">Пријави се</string>
<string name="logout">Одјави се</string>
<string name="option_send_sticker">Пошаљи налепницу</string>
<string name="option_take_photo_video">Направи фотографију или видео снимак</string>
<string name="option_take_photo">Направи фотографију</string>
<string name="option_take_video">Направи видео снимак</string>
<string name="auth_login">Пријави се</string>
<string name="auth_login_sso">Пријави се помоћу single sign-on</string>
<string name="auth_register">Направи налог</string>
<string name="auth_skip">Прескочи</string>
<string name="auth_user_id_placeholder">Адреса електронске поште или корисничко име</string>
<string name="auth_password_placeholder">Лозинка</string>
<string name="auth_new_password_placeholder">Нова лозинка</string>
<string name="auth_user_name_placeholder">Корисничко име</string>
</resources>

View File

@ -8,5 +8,6 @@
<string name="room_list_quick_actions_notifications_mute">"Mute"</string>
<string name="room_list_quick_actions_settings">"Settings"</string>
<string name="room_list_quick_actions_leave">"Leave the room"</string>
<string name="notice_member_no_changes">"%1$s made no changes"</string>
</resources>

View File

@ -39,7 +39,7 @@
android:title="@string/settings_send_typing_notifs" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true"
android:defaultValue="false"
android:key="SETTINGS_ENABLE_MARKDOWN_KEY"
android:summary="@string/settings_send_markdown_summary"
android:title="@string/settings_send_markdown" />