mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Merge branch 'develop' into feature/search_reaction
This commit is contained in:
commit
9fdfd091ac
@ -3,13 +3,35 @@
|
||||
# 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"
|
||||
@ -23,8 +45,8 @@ steps:
|
||||
|
||||
- label: "Assemble FDroid Debug version"
|
||||
agents:
|
||||
# We use a medium sized instance instead of the normal small ones because
|
||||
# gradle build is long
|
||||
# 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"
|
||||
@ -38,8 +60,8 @@ 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
|
||||
# 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"
|
||||
|
@ -6,12 +6,16 @@ Features ✨:
|
||||
|
||||
Improvements 🙌:
|
||||
- Search reaction by name or keyword in emoji picker
|
||||
- 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 🗣:
|
||||
-
|
||||
|
@ -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.
|
||||
|
@ -11,6 +11,8 @@ android {
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
// Multidex is useful for tests
|
||||
multiDexEnabled true
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -1,33 +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 org.junit.Test;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.events.model
|
||||
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
object LocalEcho {
|
||||
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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('/')
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -16,7 +16,7 @@
|
||||
package im.vector.riotx.core.utils
|
||||
|
||||
import android.view.View
|
||||
import java.util.*
|
||||
import java.util.WeakHashMap
|
||||
|
||||
/**
|
||||
* Simple Debounced OnClickListener
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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
|
||||
@ -409,7 +407,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()
|
||||
@ -480,7 +478,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) {
|
||||
@ -619,19 +617,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) {
|
||||
@ -906,19 +912,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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()) {
|
||||
|
@ -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
|
||||
|
@ -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*/) {
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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 ?: ""
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -30,7 +30,6 @@ import im.vector.riotx.core.resources.DateProvider
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
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
|
||||
|
||||
@ -97,10 +96,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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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!!,
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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() {
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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? {
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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>
|
@ -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,10 +149,12 @@
|
||||
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>
|
||||
|
113
vector/src/main/res/values-sr/strings.xml
Normal file
113
vector/src/main/res/values-sr/strings.xml
Normal 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>
|
@ -2,6 +2,6 @@
|
||||
<resources>
|
||||
|
||||
<!-- Strings not defined in Riot -->
|
||||
|
||||
<string name="notice_member_no_changes">"%1$s made no changes"</string>
|
||||
|
||||
</resources>
|
||||
|
@ -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" />
|
||||
|
@ -1,33 +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 org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user