diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index cf1cd5b9ff..de434d0122 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -3,14 +3,36 @@ # https://github.com/buildkite-plugins/docker-buildkite-plugin/releases # We propagate the environment to the container (sse https://github.com/buildkite-plugins/docker-buildkite-plugin#propagate-environment-optional-boolean) -# Build debug version of the RiotX application, from the develop branch and the features branches - steps: - - label: "Assemble GPlay Debug version" + - label: "Compile and run Unit tests" agents: # We use a medium sized instance instead of the normal small ones because - # gradle build is long + # gradle build can be memory hungry queue: "medium" + commands: + - "./gradlew clean test --stacktrace" + plugins: + - docker#v3.1.0: + image: "runmymind/docker-android-sdk" + propagate-environment: true + + - label: "Compile Android tests" + agents: + # We use a medium sized instance instead of the normal small ones because + # gradle build can be memory hungry + queue: "medium" + commands: + - "./gradlew clean assembleAndroidTest --stacktrace" + plugins: + - docker#v3.1.0: + image: "runmymind/docker-android-sdk" + propagate-environment: true + + - label: "Assemble GPlay Debug version" + agents: + # We use a xlarge sized instance instead of the normal small ones because + # gradle build can be memory hungry + queue: "xlarge" commands: - "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace" artifact_paths: @@ -23,9 +45,9 @@ steps: - label: "Assemble FDroid Debug version" agents: - # We use a medium sized instance instead of the normal small ones because - # gradle build is long - queue: "medium" + # We use a xlarge sized instance instead of the normal small ones because + # gradle build can be memory hungry + queue: "xlarge" commands: - "./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace" artifact_paths: @@ -38,9 +60,9 @@ steps: - label: "Build Google Play unsigned APK" agents: - # We use a medium sized instance instead of the normal small ones because - # gradle build is long - queue: "medium" + # We use a xlarge sized instance instead of the normal small ones because + # gradle build can be memory hungry + queue: "xlarge" commands: - "./gradlew clean assembleGplayRelease --stacktrace" artifact_paths: diff --git a/CHANGES.md b/CHANGES.md index c52ad4af0d..1a29b77427 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,17 +1,40 @@ -Changes in RiotX 0.7.0 (2019-XX-XX) +Changes in RiotX 0.8.0 (2019-XX-XX) +=================================================== + +Features ✨: + - + +Improvements 🙌: + - Handle code tags (#567) + +Other changes: + - Markdown set to off by default (#412) + - Accessibility improvements to the attachment file type chooser + +Bugfix 🐛: + - Fix issues with some member events rendering (#498) + - Passphrase does not match (Export room keys) (#644) + - Ask for permission to write external storage when uri comes from the keyboard (#658) + +Translations 🗣: + - + +Build 🧱: + - + +Changes in RiotX 0.7.0 (2019-10-24) =================================================== Features: - - + - Share elements from other app to RiotX (#58) + - Read marker (#84) + - Add ability to report content (#515) Improvements: - Persist active tab between sessions (#503) - Do not upload file too big for the homeserver (#587) - - Handle read markers (#84) - Attachments: start using system pickers (#52) - - Attachments: start handling incoming share (#58) - Mark all messages as read (#396) - - Add ability to report content (#515) Other changes: @@ -26,12 +49,6 @@ Bugfix: - Invitation notifications are not dismissed automatically if room is joined from another client (#347) - Opening links from RiotX reuses browser tab (#599) -Translations: - - - -Build: - - - Changes in RiotX 0.6.1 (2019-09-24) =================================================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d64dd7110e..45834afa21 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 31f928c241..1d8e81e44f 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -11,6 +11,8 @@ android { versionCode 1 versionName "1.0" + // Multidex is useful for tests + multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/matrix-sdk-android-rx/src/androidTest/java/im/vector/matrix/rx/ExampleInstrumentedTest.java b/matrix-sdk-android-rx/src/androidTest/java/im/vector/matrix/rx/ExampleInstrumentedTest.java deleted file mode 100644 index 986d40d1a9..0000000000 --- a/matrix-sdk-android-rx/src/androidTest/java/im/vector/matrix/rx/ExampleInstrumentedTest.java +++ /dev/null @@ -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 Testing documentation - */ -@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()); - } -} diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 3e6d3ea88b..ab5f122dbc 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -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" diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt index 3cd47d4998..99fe7d29b4 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt @@ -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 { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt index 7d33fae4d8..5c86f5ad22 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt index ca75871cda..1dbee475e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.api.session.events.model -import java.util.* +import java.util.UUID object LocalEcho { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 43c1544ffd..c05383de4e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -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()?.body ?: lastMessageContent.body + return lastMessageContent.newContent?.toModel()?.body + ?: lastMessageContent.body } return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt index 7f2a23e4c2..b2002f0916 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt @@ -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") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt index 89a27c9463..86e8a1825c 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt @@ -216,7 +216,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor( sendMessageToDevices(requestMessage, request.recipients, request.requestId, object : MatrixCallback { 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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt index ca1157e583..e0cd47e0e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt @@ -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) -> 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? { + 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) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt index 24765c120d..36ed2f7edf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt @@ -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()?.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()?.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 } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 5db062b000..0d0143d318 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -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( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt index bfc37d733d..3d850c223a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt index 98ab0b5389..45571286b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt index 243e4d4b03..8c7e9fb263 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index b50424b343..9fba1d8f02 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -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) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 49c813ece6..3fa0dcdca1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -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() ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 606c20e8cb..4127e43540 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt index 260f98d97f..592191975e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt index 058a862bc8..2df2bd2bf2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt index 4a46a43f03..31da372bbe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt @@ -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 diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt index f98af53333..17543e9d25 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt @@ -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::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) } } diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt index 42e7e850b3..7651b32d20 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt @@ -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): Cancelable { + TODO("not implemented") // To change body of created functions use File | Settings | File Templates. + } + override fun getReadMarkerLive(): LiveData> { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } diff --git a/tools/import_from_riot.sh b/tools/import_from_riot.sh index 2e4b332a3c..3f93615d19 100755 --- a/tools/import_from_riot.sh +++ b/tools/import_from_riot.sh @@ -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 diff --git a/vector/build.gradle b/vector/build.gradle index 3ef125d331..d639b4c3e8 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 7 +ext.versionMinor = 8 ext.versionPatch = 0 static def getGitTimestamp() { @@ -219,7 +219,7 @@ dependencies { def epoxy_version = '3.8.0' def arrow_version = "0.8.2" def coroutines_version = "1.3.2" - def markwon_version = '3.1.0' + def markwon_version = '4.1.2' def big_image_viewer_version = '1.5.6' def glide_version = '4.10.0' def moshi_version = '1.8.0' @@ -283,8 +283,8 @@ dependencies { implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.google.android.material:material:1.1.0-beta01' implementation 'me.gujun.android:span:1.7' - implementation "ru.noties.markwon:core:$markwon_version" - implementation "ru.noties.markwon:html:$markwon_version" + implementation "io.noties.markwon:core:$markwon_version" + implementation "io.noties.markwon:html:$markwon_version" implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.google.android:flexbox:1.1.1' diff --git a/vector/src/androidTest/java/im/vector/riotx/ExampleInstrumentedTest.kt b/vector/src/androidTest/java/im/vector/riotx/ExampleInstrumentedTest.kt deleted file mode 100644 index afed0c783a..0000000000 --- a/vector/src/androidTest/java/im/vector/riotx/ExampleInstrumentedTest.kt +++ /dev/null @@ -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) - } -} diff --git a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt index d0301e2c9f..76cbb9ef94 100644 --- a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt @@ -42,7 +42,7 @@ import javax.inject.Singleton @Singleton class AppStateHandler @Inject constructor( private val sessionObservableStore: ActiveSessionObservableStore, - private val homeRoomListStore: HomeRoomListObservableStore, + private val homeRoomListObservableStore: HomeRoomListObservableStore, private val selectedGroupStore: SelectedGroupStore) : LifecycleObserver { private val compositeDisposable = CompositeDisposable() @@ -92,7 +92,7 @@ class AppStateHandler @Inject constructor( } ) .subscribe { - homeRoomListStore.post(it) + homeRoomListObservableStore.post(it) } .addTo(compositeDisposable) } diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index b1fd6a8485..20a17e55d4 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index cb9dcf375e..87ed61c695 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -41,12 +41,13 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFrag import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.riotx.features.home.room.list.RoomListFragment +import im.vector.riotx.features.home.room.list.RoomListModule import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.link.LinkHandlerActivity import im.vector.riotx.features.login.LoginActivity @@ -71,7 +72,17 @@ import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.ui.UiStateRepository -@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class]) +@Component( + dependencies = [ + VectorComponent::class + ], + modules = [ + AssistedInjectModule::class, + ViewModelModule::class, + HomeModule::class, + RoomListModule::class + ] +) @ScreenScope interface ScreenComponent { diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index a59620aacb..2dfbb5f799 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -23,6 +23,7 @@ import dagger.Component import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.session.Session +import im.vector.riotx.ActiveSessionObservableStore import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.VectorApplication @@ -42,6 +43,7 @@ import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.settings.VectorPreferences +import im.vector.riotx.features.share.ShareRoomListObservableStore import im.vector.riotx.features.ui.UiStateRepository import javax.inject.Singleton @@ -85,8 +87,12 @@ interface VectorComponent { fun homeRoomListObservableStore(): HomeRoomListObservableStore + fun shareRoomListObservableStore(): ShareRoomListObservableStore + fun selectedGroupStore(): SelectedGroupStore + fun activeSessionObservableStore(): ActiveSessionObservableStore + fun incomingVerificationRequestHandler(): IncomingVerificationRequestHandler fun incomingKeyRequestHandler(): KeyRequestHandler diff --git a/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt b/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt index fb320afded..1cb6c5406a 100644 --- a/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt b/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt index c1f58306a4..677f7894e8 100644 --- a/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt +++ b/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt @@ -30,15 +30,10 @@ import java.io.File */ @WorkerThread fun writeToFile(str: String, file: File): Try { - return Try { - val sink = file.sink() - - val bufferedSink = sink.buffer() - - bufferedSink.writeString(str, Charsets.UTF_8) - - bufferedSink.close() - sink.close() + return Try { + file.sink().buffer().use { + it.writeString(str, Charsets.UTF_8) + } } } @@ -47,15 +42,10 @@ fun writeToFile(str: String, file: File): Try { */ @WorkerThread fun writeToFile(data: ByteArray, file: File): Try { - return Try { - val sink = file.sink() - - val bufferedSink = sink.buffer() - - bufferedSink.write(data) - - bufferedSink.close() - sink.close() + return Try { + file.sink().buffer().use { + it.write(data) + } } } diff --git a/vector/src/main/java/im/vector/riotx/core/images/ImageTools.kt b/vector/src/main/java/im/vector/riotx/core/images/ImageTools.kt index b6ae2be20b..84cba7392f 100644 --- a/vector/src/main/java/im/vector/riotx/core/images/ImageTools.kt +++ b/vector/src/main/java/im/vector/riotx/core/images/ImageTools.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt b/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt index 9e9f0ae508..2b6740f62f 100644 --- a/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt +++ b/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt @@ -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('/') } diff --git a/vector/src/main/java/im/vector/riotx/core/linkify/VectorAutoLinkPatterns.kt b/vector/src/main/java/im/vector/riotx/core/linkify/VectorAutoLinkPatterns.kt index e6eb886e02..ae4131b5e9 100644 --- a/vector/src/main/java/im/vector/riotx/core/linkify/VectorAutoLinkPatterns.kt +++ b/vector/src/main/java/im/vector/riotx/core/linkify/VectorAutoLinkPatterns.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotx/core/linkify/VectorLinkify.kt b/vector/src/main/java/im/vector/riotx/core/linkify/VectorLinkify.kt index 358fff6092..99b0316cbe 100644 --- a/vector/src/main/java/im/vector/riotx/core/linkify/VectorLinkify.kt +++ b/vector/src/main/java/im/vector/riotx/core/linkify/VectorLinkify.kt @@ -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) { - Collections.sort(links, COMPARATOR) + links.sortWith(COMPARATOR) var len = links.size var i = 0 while (i < len - 1) { diff --git a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt index fc09ad0f75..b8587750a3 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt index 8d40d55a7a..1b07d739b5 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt index 41287d4e38..e2c08a1fe8 100644 --- a/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt @@ -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, diff --git a/vector/src/main/java/im/vector/riotx/core/resources/LocaleProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/LocaleProvider.kt index 74861a65cc..c78a5a99b8 100644 --- a/vector/src/main/java/im/vector/riotx/core/resources/LocaleProvider.kt +++ b/vector/src/main/java/im/vector/riotx/core/resources/LocaleProvider.kt @@ -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) { diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DebouncedClickListener.kt b/vector/src/main/java/im/vector/riotx/core/utils/DebouncedClickListener.kt index 958f642565..230b11f14d 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/DebouncedClickListener.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/DebouncedClickListener.kt @@ -16,7 +16,7 @@ package im.vector.riotx.core.utils import android.view.View -import java.util.* +import java.util.WeakHashMap /** * Simple Debounced OnClickListener diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt b/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt index c65fcafb16..a5babcc885 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt @@ -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() - source?.emojis?.values?.forEach { - knownEmojiSet?.add(it.emojiString()) + knownEmojiSet = HashSet().also { + source?.emojis?.mapTo(it) { (_, value) -> + value.emojiString() + } } done?.invoke() } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt index 9572b07216..78242d58de 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt index 8f97ef0247..f8cdeb3de6 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt @@ -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. diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt index 12371fe72d..ba0b99762b 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt @@ -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. diff --git a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt index 0b5df0d2e0..75f6893c7c 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt index ac79ed8b40..9cf6510fdb 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt @@ -44,12 +44,11 @@ object CommandParser { return ParsedCommand.ErrorNotACommand } - var messageParts: List? = 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) } diff --git a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt index ec8f1c7fa2..adf8421842 100644 --- a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt +++ b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt @@ -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 /** diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt index 54e3a34744..9642c2d8c6 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt @@ -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) { - session.exportRoomKeys(password, object : MatrixCallback { - 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 { 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) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt index 74b2a86bc1..b60e25af04 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt @@ -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) { 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 { + session.importRoomKeys(data, password, null, it) + } } - } - .fold( - { - callback.onFailure(it) - }, - { byteArray -> - session.importRoomKeys(byteArray, - password, - null, - callback) - } - ) + }.foldToCallback(callback) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index 6b01a7dffa..7b60cb2f9b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -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, diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt index 7b61ca2c0f..a5cc0510da 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -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") diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index a525e7acc6..4875c87ec0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -81,8 +81,6 @@ import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.ui.views.JumpToReadMarkerView import im.vector.riotx.core.ui.views.NotificationAreaView import im.vector.riotx.core.utils.* -import im.vector.riotx.core.utils.Debouncer -import im.vector.riotx.core.utils.createUIHandler import im.vector.riotx.features.attachments.AttachmentTypeSelectorView import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.ContactAttachment @@ -412,7 +410,7 @@ class RoomDetailFragment : composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.sendButton.setContentDescription(getString(descriptionRes)) - avatarRenderer.render(event.senderAvatar, event.root.senderId ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + avatarRenderer.render(event.senderAvatar, event.root.senderId ?: "", event.getDisambiguatedDisplayName(), composerLayout.composerRelatedMessageAvatar) composerLayout.expand { // need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -483,7 +481,7 @@ class RoomDetailFragment : jumpToReadMarkerView.render(show, readMarkerId) } } - recyclerView.setController(timelineEventController) + recyclerView.adapter = timelineEventController.adapter recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) { @@ -622,19 +620,27 @@ class RoomDetailFragment : } composerLayout.callback = object : TextComposerView.Callback { override fun onRichContentSelected(contentUri: Uri): Boolean { - val shareIntent = Intent().apply { - action = Intent.ACTION_SEND - data = contentUri + // We need WRITE_EXTERNAL permission + return if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this@RoomDetailFragment, PERMISSION_REQUEST_CODE_INCOMING_URI)) { + sendUri(contentUri) + } else { + roomDetailViewModel.pendingUri = contentUri + // Always intercept when we request some permission + true } - val isHandled = attachmentsHelper.handleShareIntent(shareIntent) - if (!isHandled) { - Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show() - } - return isHandled } } } + private fun sendUri(uri: Uri): Boolean { + val shareIntent = Intent(Intent.ACTION_SEND, uri) + val isHandled = attachmentsHelper.handleShareIntent(shareIntent) + if (!isHandled) { + Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show() + } + return isHandled + } + private fun setupAttachmentButton() { composerLayout.attachmentButton.setOnClickListener { if (!::attachmentTypeSelector.isInitialized) { @@ -909,19 +915,34 @@ class RoomDetailFragment : override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, 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 } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index f3934f618c..b1c6aa02fb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt index 84917d682b..69ecc30583 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt @@ -76,11 +76,8 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: Observable.combineLatest, Option, List>( 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()) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 135496264d..63a4919763 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index d36e98f67c..4661d8f8cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -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*/) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt index e2b976b273..93e7709b55 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt @@ -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, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 12f49c2e74..3f234fcd3e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -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( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 1df885cd35..51364e24c9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -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 ?: "" ) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 0bb5c3a1d8..eacd702b7d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -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() 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, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 2d116e4a90..9a86420277 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -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()?.historyVisibility ?: return null + val historyVisibility = event.getClearContent().toModel()?.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) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index a75ac86c1b..f40b8e6f6d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -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()?.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()?.displayName - else -> null - } -} - fun TimelineEvent.canBeMerged(): Boolean { return root.getClearType() == EventType.STATE_ROOM_MEMBER } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt new file mode 100644 index 0000000000..82a6a4db6f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt @@ -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() { + + @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(R.id.codeBlockTextView) + val editedView by bind(R.id.codeBlockEditedView) + } + + companion object { + private const val STUB_ID = R.id.messageContentCodeBlockStub + } +} diff --git a/vector/src/test/java/im/vector/riotx/ExampleUnitTest.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListModule.kt similarity index 64% rename from vector/src/test/java/im/vector/riotx/ExampleUnitTest.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListModule.kt index c51f642a1b..4541b5d2b5 100644 --- a/vector/src/test/java/im/vector/riotx/ExampleUnitTest.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListModule.kt @@ -14,20 +14,14 @@ * limitations under the License. */ -package im.vector.riotx +package im.vector.riotx.features.home.room.list -import org.junit.Test +import dagger.Binds +import dagger.Module -import org.junit.Assert.* +@Module +abstract class RoomListModule { -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } + @Binds + abstract fun providesRoomListViewModelFactory(roomListViewModelFactory: RoomListViewModelFactory): RoomListViewModel.Factory } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index 32061b9cbf..31fa9d0309 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -21,8 +21,6 @@ import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.Membership @@ -31,18 +29,18 @@ import im.vector.matrix.android.api.session.room.model.tag.RoomTag import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.LiveEvent -import im.vector.riotx.features.home.HomeRoomListObservableStore +import im.vector.riotx.core.utils.RxStore import io.reactivex.schedulers.Schedulers import timber.log.Timber +import javax.inject.Inject -class RoomListViewModel @AssistedInject constructor(@Assisted initialState: RoomListViewState, - private val session: Session, - private val homeRoomListObservableSource: HomeRoomListObservableStore, - private val alphabeticalRoomComparator: AlphabeticalRoomComparator, - private val chronologicalRoomComparator: ChronologicalRoomComparator) +class RoomListViewModel @Inject constructor(initialState: RoomListViewState, + private val session: Session, + private val roomSummariesStore: RxStore>, + private val alphabeticalRoomComparator: AlphabeticalRoomComparator, + private val chronologicalRoomComparator: ChronologicalRoomComparator) : VectorViewModel(initialState) { - @AssistedInject.Factory interface Factory { fun create(initialState: RoomListViewState): RoomListViewModel } @@ -103,7 +101,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room } private fun observeRoomSummaries() { - homeRoomListObservableSource + roomSummariesStore .observe() .observeOn(Schedulers.computation()) .map { @@ -113,7 +111,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room copy(asyncRooms = asyncRooms) } - homeRoomListObservableSource + roomSummariesStore .observe() .observeOn(Schedulers.computation()) .map { buildRoomSummaries(it) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt new file mode 100644 index 0000000000..5895aa4e52 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.list + +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.features.home.HomeRoomListObservableStore +import im.vector.riotx.features.share.ShareRoomListObservableStore +import javax.inject.Inject +import javax.inject.Provider + +class RoomListViewModelFactory @Inject constructor(private val session: Provider, + private val homeRoomListObservableStore: Provider, + private val shareRoomListObservableStore: Provider, + private val alphabeticalRoomComparator: Provider, + private val chronologicalRoomComparator: Provider) : RoomListViewModel.Factory { + + override fun create(initialState: RoomListViewState): RoomListViewModel { + return RoomListViewModel( + initialState, + session.get(), + if (initialState.displayMode == RoomListFragment.DisplayMode.SHARE) shareRoomListObservableStore.get() else homeRoomListObservableStore.get(), + alphabeticalRoomComparator.get(), + chronologicalRoomComparator.get()) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt index 9f73d24c6d..3401e041b1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt @@ -20,6 +20,8 @@ import androidx.annotation.StringRes import com.airbnb.epoxy.EpoxyController import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.noResultItem import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.filtered.FilteredRoomFooterItem import im.vector.riotx.features.home.room.filtered.filteredRoomFooterItem @@ -47,24 +49,28 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri override fun buildModels() { val nonNullViewState = viewState ?: return - if (nonNullViewState.displayMode == RoomListFragment.DisplayMode.FILTERED) { - buildFilteredRooms(nonNullViewState) - } else { - val roomSummaries = nonNullViewState.asyncFilteredRooms() - roomSummaries?.forEach { (category, summaries) -> - if (summaries.isEmpty()) { - return@forEach - } else { - val isExpanded = nonNullViewState.isCategoryExpanded(category) - buildRoomCategory(nonNullViewState, summaries, category.titleRes, nonNullViewState.isCategoryExpanded(category)) { - listener?.onToggleRoomCategory(category) - } - if (isExpanded) { - buildRoomModels(summaries, - nonNullViewState.joiningRoomsIds, - nonNullViewState.joiningErrorRoomsIds, - nonNullViewState.rejectingRoomsIds, - nonNullViewState.rejectingErrorRoomsIds) + when (nonNullViewState.displayMode) { + RoomListFragment.DisplayMode.FILTERED, + RoomListFragment.DisplayMode.SHARE -> { + buildFilteredRooms(nonNullViewState) + } + else -> { + val roomSummaries = nonNullViewState.asyncFilteredRooms() + roomSummaries?.forEach { (category, summaries) -> + if (summaries.isEmpty()) { + return@forEach + } else { + val isExpanded = nonNullViewState.isCategoryExpanded(category) + buildRoomCategory(nonNullViewState, summaries, category.titleRes, nonNullViewState.isCategoryExpanded(category)) { + listener?.onToggleRoomCategory(category) + } + if (isExpanded) { + buildRoomModels(summaries, + nonNullViewState.joiningRoomsIds, + nonNullViewState.joiningErrorRoomsIds, + nonNullViewState.rejectingRoomsIds, + nonNullViewState.rejectingErrorRoomsIds) + } } } } @@ -80,12 +86,15 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri .filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) } buildRoomModels(filteredSummaries, - viewState.joiningRoomsIds, - viewState.joiningErrorRoomsIds, - viewState.rejectingRoomsIds, - viewState.rejectingErrorRoomsIds) + viewState.joiningRoomsIds, + viewState.joiningErrorRoomsIds, + viewState.rejectingRoomsIds, + viewState.rejectingErrorRoomsIds) - addFilterFooter(viewState) + when { + viewState.displayMode == RoomListFragment.DisplayMode.FILTERED -> addFilterFooter(viewState) + filteredSummaries.isEmpty() -> addEmptyFooter() + } } private fun addFilterFooter(viewState: RoomListViewState) { @@ -96,6 +105,13 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri } } + private fun addEmptyFooter() { + noResultItem { + id("no_result") + text(stringProvider.getString(R.string.no_result_placeholder)) + } + } + private fun buildRoomCategory(viewState: RoomListViewState, summaries: List, @StringRes titleRes: Int, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index c38c5cfd37..f977fe80fd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -32,7 +32,6 @@ import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter -import im.vector.riotx.features.home.room.detail.timeline.helper.senderName import me.gujun.android.span.span import javax.inject.Inject @@ -99,10 +98,10 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte && latestEvent.root.mxDecryptionResult == null) { stringProvider.getString(R.string.encrypted_message) } else if (latestEvent.root.getClearType() == EventType.MESSAGE) { - val senderName = latestEvent.senderName() ?: latestEvent.root.senderId + val senderName = latestEvent.getDisambiguatedDisplayName() val content = latestEvent.root.getClearContent()?.toModel() 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) diff --git a/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt b/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt new file mode 100644 index 0000000000..ed8db94fc3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt @@ -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 + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt index 06af8ebca5..dc9e21e440 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt @@ -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("b", "strong"), - StrongEmphasisHandler()) - .setHandler( - asList("s", "del"), - StrikeHandler()) - .setHandler( - asList("u", "ins"), - UnderlineHandler()) - .setHandler( - asList("ul", "ol"), - ListHandler()) - .setHandler( - asList("i", "em", "cite", "dfn"), - EmphasisHandler()) - .setHandler( - asList("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()) } } diff --git a/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt index f4fa1737c9..e5733dd849 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt @@ -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 } } } diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt new file mode 100644 index 0000000000..fdcbb12cd7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt @@ -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) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxReplyTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxReplyTagHandler.kt new file mode 100644 index 0000000000..f999e253c7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/MxReplyTagHandler.kt @@ -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) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt index 06108e07fe..e38e7d548a 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt @@ -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!!, diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt index e26395641d..63cd1c5ce6 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt @@ -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 /** diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt index 1a5385663b..7d8e43d0be 100755 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt @@ -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) } /* diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt index 9a7707d063..b96542a8ce 100755 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt index 0b9cb5798c..95053790c8 100644 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt index 7fef12cddf..93931fe71d 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt @@ -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>() + val knownLocalesSet = HashSet>() 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 } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index f9601265d3..a593bb6e96 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -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) } /** diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 331b6e935a..ff76c61754 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -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() { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 2f52cdef13..a78529f06c 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -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() { diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt index 0d2f9ee040..5e471cf78b 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt @@ -20,6 +20,8 @@ import android.content.ClipDescription import android.content.Intent import android.os.Bundle import android.widget.Toast +import androidx.appcompat.widget.SearchView +import com.airbnb.mvrx.viewModel import com.kbeanie.multipicker.utils.IntentUtils import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.riotx.R @@ -39,8 +41,10 @@ class IncomingShareActivity : VectorBaseActivity(), AttachmentsHelper.Callback { @Inject lateinit var sessionHolder: ActiveSessionHolder - private lateinit var roomListFragment: RoomListFragment + @Inject lateinit var incomingShareViewModelFactory: IncomingShareViewModel.Factory + private var roomListFragment: RoomListFragment? = null private lateinit var attachmentsHelper: AttachmentsHelper + private val incomingShareViewModel: IncomingShareViewModel by viewModel() override fun getLayoutRes(): Int { return R.layout.activity_incoming_share @@ -77,12 +81,23 @@ class IncomingShareActivity : } else { cannotManageShare() } + + incomingShareSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + roomListFragment?.filterRoomsWith(newText) + return true + } + }) } override fun onContentAttachmentsReady(attachments: List) { val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Attachments(attachments)) roomListFragment = RoomListFragment.newInstance(roomListParams) - replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer) + .also { replaceFragment(it, R.id.shareRoomListFragmentContainer) } } override fun onAttachmentsProcessFailed() { @@ -102,7 +117,7 @@ class IncomingShareActivity : } else { val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Text(sharedText)) roomListFragment = RoomListFragment.newInstance(roomListParams) - replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer) + .also { replaceFragment(it, R.id.shareRoomListFragmentContainer) } true } } diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt new file mode 100644 index 0000000000..51485ecbf9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.share + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.rx.rx +import im.vector.riotx.ActiveSessionObservableStore +import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit + +data class IncomingShareState(private val dummy: Boolean = false) : MvRxState + +/** + * View model used to observe the room list and post update to the ShareRoomListObservableStore + */ +class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: IncomingShareState, + private val sessionObservableStore: ActiveSessionObservableStore, + private val shareRoomListObservableStore: ShareRoomListObservableStore) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: IncomingShareState): IncomingShareViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: IncomingShareState): IncomingShareViewModel? { + val activity: IncomingShareActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.incomingShareViewModelFactory.create(state) + } + } + + init { + observeRoomSummaries() + } + + private fun observeRoomSummaries() { + sessionObservableStore.observe() + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { + it.orNull()?.rx()?.liveRoomSummaries() + ?: Observable.just(emptyList()) + } + .throttleLast(300, TimeUnit.MILLISECONDS) + .subscribe { + shareRoomListObservableStore.post(it) + } + .disposeOnClear() + } +} diff --git a/matrix-sdk-android-rx/src/test/java/im/vector/matrix/rx/ExampleUnitTest.java b/vector/src/main/java/im/vector/riotx/features/share/ShareRoomListObservableStore.kt similarity index 61% rename from matrix-sdk-android-rx/src/test/java/im/vector/matrix/rx/ExampleUnitTest.java rename to vector/src/main/java/im/vector/riotx/features/share/ShareRoomListObservableStore.kt index 6b7fcfe7e6..c46ec42d64 100644 --- a/matrix-sdk-android-rx/src/test/java/im/vector/matrix/rx/ExampleUnitTest.java +++ b/vector/src/main/java/im/vector/riotx/features/share/ShareRoomListObservableStore.kt @@ -14,20 +14,12 @@ * limitations under the License. */ -package im.vector.matrix.rx; +package im.vector.riotx.features.share -import org.junit.Test; +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.riotx.core.utils.RxStore +import javax.inject.Inject +import javax.inject.Singleton -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file +@Singleton +class ShareRoomListObservableStore @Inject constructor() : RxStore>() diff --git a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt index 2edb59104b..40a14b3e6f 100644 --- a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt @@ -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? { diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index ab2c40c313..6661674edb 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -86,7 +86,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/roomToolbar" /> - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_attachment_type_selector.xml b/vector/src/main/res/layout/view_attachment_type_selector.xml index 2af86d6c0d..f713561084 100644 --- a/vector/src/main/res/layout/view_attachment_type_selector.xml +++ b/vector/src/main/res/layout/view_attachment_type_selector.xml @@ -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" /> @@ -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" /> @@ -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" /> @@ -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" /> @@ -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" /> @@ -139,14 +149,16 @@ android:id="@+id/attachmentStickersButton" style="@style/AttachmentTypeSelectorButton" android:src="@drawable/ic_attachment_stickers_white_24dp" + android:contentDescription="@string/attachment_type_sticker" tools:background="@color/colorAccent" /> - \ No newline at end of file + diff --git a/vector/src/main/res/values-sr/strings.xml b/vector/src/main/res/values-sr/strings.xml new file mode 100644 index 0000000000..121589a6cd --- /dev/null +++ b/vector/src/main/res/values-sr/strings.xml @@ -0,0 +1,113 @@ + + + sr + RS + Cyrl + + Светла тема + Тамна тема + Црна тема + Status.im тема + + Иницијализација сервиса + Синхронизација у току… + Бучна обавештења + Тиха обавештења + + Поруке + Соба + Подешавања + Подаци о члану + Историјски + Пријава грешке + Пошаљи налепницу + Резервна копија кључева + Користи резервну копију кључева + Верификуј уређај + + Креирање резервне копије кључева се није завршило, молим сачекајте… + Изгубићете ваше шифроване поруке ако се сад одјавите + Креирање резервне копије кључева је у току. Ако се одјавите сад, изгубићете приступ вашим шифрованим порукама. + Сигурносна копија кључева би требало да буде активна на свим вашим уређајима како би избегли губитак приступа вашим шифрованим порукама. + Не желим моје шифроване поруке + Прављење резервне копије кључева у току… + Користи резервну копију кључева + Да ли сте сигурни\? + Изгубићете приступ вашим шифрованим порукама уколико не направите резервну копију кључева пре него што се одјавите. + + Учитавање… + + У реду + Откажи + Сачувај + Напусти + Остани + Пошаљи + Копирај + Пошаљи поново + Уклони + Подели + Прихвати + Прескочи + Готово + Обустави + Игнориши + Прегледај + Одбаци + + Изађи + Акције + Одјави се + Да ли сте сигурни да желите да се одјавите\? + Гласовни позив + Видео позив + Глобална претрага + Означи све као прочитано + Брзи одговор + Означи као прочитано + Отвори + Затвори + Онемогући + + Потврда + Упозорење + Грешка + + Омиљено + Људи + Собе + Позивнице + Низак приоритет + Разговори + Локални адресар + Листа корисника + Само Matrix контакти + Нема резултата + Нема подешених сервера идентитета. + + Собе + Листа соба + Нема соба + Пошаљи позивницу + Опишите ваш проблем овде + Прочитај + + Придружи се соби + Корисничко име + Направи налог + Пријави се + Одјави се + Пошаљи налепницу + Направи фотографију или видео снимак + Направи фотографију + Направи видео снимак + + Пријави се + Пријави се помоћу single sign-on + Направи налог + Прескочи + Адреса електронске поште или корисничко име + Лозинка + Нова лозинка + Корисничко име + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index c627d40eb0..95ad9729b9 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -8,5 +8,6 @@ "Mute" "Settings" "Leave the room" + "%1$s made no changes" diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 11209b5345..96471cfebe 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -39,7 +39,7 @@ android:title="@string/settings_send_typing_notifs" />