diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index 7ac55427a9..582998d492 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -1,6 +1,6 @@ name: Release checklist description: Checklist for each release. This template is only for the core team. -title: "[Release] Element Android v" +title: "[Release] Element Android v" labels: [🚀 Release] assignees: - bmarty @@ -10,7 +10,7 @@ body: id: checklist attributes: label: Release checklist - description: For the template example, we are releasing the version 1.1.10. Replace 1.1.10 with the version in the issue body. + description: For the template example, we are releasing the version 1.2.3. Replace 1.2.3 with the version in the issue body. placeholder: | If you are reading this, you have deleted the content of the release template: undo the deletion or start again. value: | @@ -22,35 +22,41 @@ body: ### Do the release - - [ ] Create release with gitflow, branch name `release/1.1.10` + - [ ] Create release with gitflow, branch name `release/1.2.3` - [ ] Check the crashes from the PlayStore - - [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.1.10-dev + - [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.2.3-dev - [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()` - - [ ] Create an account on matrix.org - - [ ] Run towncrier: `towncrier --version v1.1.10 --draft` (remove `--draft` do write the file CHANGES.md) + - [ ] Create an account on matrix.org and do some smoke tests that the sanity test does not cover like: 1-1 call, 1-1 video call, Jitsi call for instance + - [ ] Run towncrier: `towncrier --version v1.2.3 --draft` (remove `--draft` do write the file CHANGES.md) + - [ ] Check that the folder `changelog.d` is empty. It can happen that some remaining files stay here + - [ ] Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things - [ ] Add file for fastlane under ./fastlane/metadata/android/en-US/changelogs - - [ ] Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes. - - [ ] Finish release with gitflow, delete the draft PR - - [ ] Push `main` and the new tag `v1.1.10` to origin + - [ ] (optional) Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes. + - [ ] Finish release with gitflow, delete the draft PR (if created) + - [ ] Push `main` and the new tag `v1.2.3` to origin - [ ] Checkout `develop` - - [ ] Increase version in `./vector/build.gradle` + - [ ] Increase version (versionPatch + 2) in `./vector/build.gradle` - [ ] Change the value of SDK_VERSION in the file `./matrix-sdk-android/build.gradle` - [ ] Commit and push `develop` - [ ] Wait for [Buildkite](https://buildkite.com/matrix-dot-org/element-android/builds?branch=main) to build the `main` branch. - [ ] Run the script `~/scripts/releaseElement.sh`. It will download the APKs from Buildkite check them and sign them. - [ ] Install the APK on your phone to check that the upgrade went well (no init sync, etc.) + - [ ] Create the release on gitHub [from the tag](https://github.com/vector-im/element-android/tags), copy paste the block from the file CHANGES.md + - [ ] Add the 4 signed APKs to the GitHub release + - [ ] Ping the Android Internal room + + ### Once tested and validated internally + - [ ] Create a new beta release on the GooglePlay console and upload the 4 signed Apks. - [ ] Check that the version codes are correct - [ ] Copy the fastlane change to the GooglePlay console in the section en-GB. - [ ] Push to beta release to 100% of the users - - [ ] Create the release on gitHub [from the tag](https://github.com/vector-im/element-android/tags), copy paste the block from the file CHANGES.md - - [ ] Add the 4 signed APKs to the GitHub release - - [ ] Ping the Android Internal room - - [ ] Add an entry in the internal diary + - [ ] Notify the F-Droid team so that they can schedule the publication on F-Droid ### Once Live on PlayStore - [ ] Ping the Android public room and update its topic + - [ ] Add an entry in the internal diary ### After at least 2 days @@ -62,6 +68,8 @@ body: ### Android SDK2 + The SDK2 and the sample app are released only when Element has been pushed to production. + - [ ] Checkout the `main` branch on Element Android project #### On the SDK2 project diff --git a/CHANGES.md b/CHANGES.md index b1b0deee2c..ec022bc770 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +Changes in Element v1.3.18 (2022-02-03) +======================================= + +Bugfixes 🐛 +---------- + - Avoid deleting root event of CurrentState on gappy sync. In order to restore lost Events an initial sync may be triggered. ([#5137](https://github.com/vector-im/element-android/issues/5137)) + + Changes in Element v1.3.17 (2022-01-31) ======================================= diff --git a/changelog.d/3907.bugfix b/changelog.d/3907.bugfix new file mode 100644 index 0000000000..8cf6081cfc --- /dev/null +++ b/changelog.d/3907.bugfix @@ -0,0 +1 @@ +Fixes non sans-serif font weights being ignored \ No newline at end of file diff --git a/changelog.d/4295.misc b/changelog.d/4295.misc new file mode 100644 index 0000000000..652c5adc32 --- /dev/null +++ b/changelog.d/4295.misc @@ -0,0 +1 @@ +"Invite users to space" dialog now closed when user choose invite method \ No newline at end of file diff --git a/changelog.d/4304.misc b/changelog.d/4304.misc new file mode 100644 index 0000000000..7fb6f2ecb0 --- /dev/null +++ b/changelog.d/4304.misc @@ -0,0 +1 @@ +Changed layout for space card and room card used at "explore room" screen and space/room invite dialogs \ No newline at end of file diff --git a/changelog.d/4315.misc b/changelog.d/4315.misc new file mode 100644 index 0000000000..ff1271a604 --- /dev/null +++ b/changelog.d/4315.misc @@ -0,0 +1 @@ +Removed spaces restricted search hint dialogs \ No newline at end of file diff --git a/changelog.d/4641.misc b/changelog.d/4641.misc new file mode 100644 index 0000000000..f02bf14fc1 --- /dev/null +++ b/changelog.d/4641.misc @@ -0,0 +1 @@ +Remove Search from room options if not available diff --git a/changelog.d/4873.misc b/changelog.d/4873.misc new file mode 100644 index 0000000000..328a62502f --- /dev/null +++ b/changelog.d/4873.misc @@ -0,0 +1 @@ +Qr code scanning fragments merged into one \ No newline at end of file diff --git a/changelog.d/5038.bugfix b/changelog.d/5038.bugfix new file mode 100644 index 0000000000..8092c21d4f --- /dev/null +++ b/changelog.d/5038.bugfix @@ -0,0 +1 @@ +Fixing missing/intermittent notifications on the google play variant when wifi is enabled \ No newline at end of file diff --git a/changelog.d/5088.bugfix b/changelog.d/5088.bugfix new file mode 100644 index 0000000000..bc702e5e94 --- /dev/null +++ b/changelog.d/5088.bugfix @@ -0,0 +1 @@ +Fixes call statuses in the timeline for missed/rejected calls and connected calls. \ No newline at end of file diff --git a/changelog.d/5128.bugfix b/changelog.d/5128.bugfix new file mode 100644 index 0000000000..d26d0047a5 --- /dev/null +++ b/changelog.d/5128.bugfix @@ -0,0 +1 @@ +Fix fallback permalink when threads are disabled \ No newline at end of file diff --git a/changelog.d/5146.feature b/changelog.d/5146.feature new file mode 100644 index 0000000000..a1832864c8 --- /dev/null +++ b/changelog.d/5146.feature @@ -0,0 +1 @@ +Support generic location pin \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103180.txt b/fastlane/metadata/android/en-US/changelogs/40103180.txt new file mode 100644 index 0000000000..66e51f422a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103180.txt @@ -0,0 +1,2 @@ +Main changes in this version: send your location to any room. Edit poll. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.18 \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index b828855721..54b8fd3200 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -105,9 +105,6 @@ never - - sans - @style/PreferenceThemeOverlay.v14.Material @style/PinCodeScreenStyle diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index 790a0bfc7c..ee3f41635e 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -105,9 +105,6 @@ never - - sans - @style/PreferenceThemeOverlay.v14.Material @style/PinCodeScreenStyle diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 7d4bd0bc67..5137d18c00 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -31,7 +31,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.3.18\"" + buildConfigField "String", "SDK_VERSION", "\"1.3.19\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt index c090487c58..d07bd2d73a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt @@ -64,4 +64,12 @@ data class MessageLocationContent( ) : MessageContent { fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri + + /** + * @return true if the location asset is a user location, not a generic one. + */ + fun isSelfLocation(): Boolean { + // Should behave like m.self if locationAsset is null + return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index 9f2850e26a..09114436f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -66,8 +66,8 @@ interface RelationService { * @param targetEventId the id of the event being reacted * @param reaction the reaction (preferably emoji) */ - fun undoReaction(targetEventId: String, - reaction: String): Cancelable + suspend fun undoReaction(targetEventId: String, + reaction: String): Cancelable /** * Edit a poll. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt index 5a68937868..b70e6c1f80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt @@ -35,7 +35,7 @@ internal class MXOutboundSessionInfo( val sessionLifetime = System.currentTimeMillis() - creationTime if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) { - Timber.v("## needsRotation() : Rotating megolm session after " + useCount + ", " + sessionLifetime + "ms") + Timber.v("## needsRotation() : Rotating megolm session after $useCount, ${sessionLifetime}ms") needsRotation = true } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index e7ccae38d5..6c66ec9833 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -57,7 +57,7 @@ internal class RealmSessionStoreMigration @Inject constructor( ) : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 22L + const val SESSION_STORE_SCHEMA_VERSION = 23L } /** @@ -92,6 +92,7 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion <= 19) migrateTo20(realm) if (oldVersion <= 20) migrateTo21(realm) if (oldVersion <= 21) migrateTo22(realm) + if (oldVersion <= 22) migrateTo23(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -450,6 +451,22 @@ internal class RealmSessionStoreMigration @Inject constructor( private fun migrateTo22(realm: DynamicRealm) { Timber.d("Step 21 -> 22") + val listJoinedRoomIds = realm.where("RoomEntity") + .equalTo(RoomEntityFields.MEMBERSHIP_STR, Membership.JOIN.name).findAll() + .map { it.getString(RoomEntityFields.ROOM_ID) } + + val hasMissingStateEvent = realm.where("CurrentStateEventEntity") + .`in`(CurrentStateEventEntityFields.ROOM_ID, listJoinedRoomIds.toTypedArray()) + .isNull(CurrentStateEventEntityFields.ROOT.`$`).findFirst() != null + + if (hasMissingStateEvent) { + Timber.v("Has some missing state event, clear session cache") + realm.deleteAll() + } + } + + private fun migrateTo23(realm: DynamicRealm) { + Timber.d("Step 22 -> 23") val eventEntity = realm.schema.get("TimelineEventEntity") ?: return realm.schema.get("EventEntity") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt index ecb602019a..c45c27ed08 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt @@ -52,6 +52,9 @@ internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRo if (deleteStateEvents) { stateEvents.deleteAllFromRealm() } - timelineEvents.clearWith { it.deleteOnCascade(canDeleteRoot) } + timelineEvents.clearWith { + val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents) + it.deleteOnCascade(deleteRoot) + } deleteFromRealm() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index c9c96b9cc1..8cc99c3d2f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -34,27 +34,29 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration, if (LocalEcho.isLocalEchoId(eventId)) { return true } - // If we don't know if the event has been read, we assume it's not - var isEventRead = false - Realm.getInstance(realmConfiguration).use { realm -> - val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, true) - // If latest event is from you we are sure the event is read - if (latestEvent?.root?.sender == userId) { - return true - } + return Realm.getInstance(realmConfiguration).use { realm -> val eventToCheck = TimelineEventEntity.where(realm, roomId, eventId).findFirst() - isEventRead = when { - eventToCheck == null -> false - eventToCheck.root?.sender == userId -> true - else -> { - val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@use - val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() ?: return@use - readReceiptEvent.isMoreRecentThan(eventToCheck) - } + when { + // The event doesn't exist locally, let's assume it hasn't been read + eventToCheck == null -> false + eventToCheck.root?.sender == userId -> true + // If new event exists and the latest event is from ourselves we can infer the event is read + latestEventIsFromSelf(realm, roomId, userId) -> true + eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId) -> true + else -> false } } - return isEventRead +} + +private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true) + ?.root?.sender == userId + +private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String): Boolean { + return ReadReceiptEntity.where(realm, roomId, userId).findFirst()?.let { readReceipt -> + val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() + readReceiptEvent?.isMoreRecentThan(this) + } ?: false } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt index 82cd682eae..55db64f309 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt @@ -66,7 +66,7 @@ internal class ThumbnailExtractor @Inject constructor( thumbnail.recycle() outputStream.reset() } ?: run { - Timber.e("Cannot extract video thumbnail at %s", attachment.queryUri.toString()) + Timber.e("Cannot extract video thumbnail at ${attachment.queryUri}") } } catch (e: Exception) { Timber.e(e, "Cannot extract video thumbnail") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 95e5771757..3abf28fdd4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -21,7 +21,6 @@ import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.message.PollType @@ -32,19 +31,15 @@ import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider -import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.fetchCopyMap import timber.log.Timber @@ -54,15 +49,12 @@ internal class DefaultRelationService @AssistedInject constructor( private val eventSenderProcessor: EventSenderProcessor, private val eventFactory: LocalEchoEventFactory, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, - private val cryptoService: DefaultCryptoService, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val fetchEditHistoryTask: FetchEditHistoryTask, private val fetchThreadTimelineTask: FetchThreadTimelineTask, private val timelineEventMapper: TimelineEventMapper, - @UserId private val userId: String, - @SessionDatabase private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor) : - RelationService { + @SessionDatabase private val monarchy: Monarchy +) : RelationService { @AssistedFactory interface Factory { @@ -84,39 +76,31 @@ internal class DefaultRelationService @AssistedInject constructor( .none { it.addedByMe && it.key == reaction }) { val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) .also { saveLocalEcho(it) } - return eventSenderProcessor.postEvent(event, false /* reaction are not encrypted*/) + eventSenderProcessor.postEvent(event, false /* reaction are not encrypted*/) } else { Timber.w("Reaction already added") NoOpCancellable } } - override fun undoReaction(targetEventId: String, reaction: String): Cancelable { + override suspend fun undoReaction(targetEventId: String, reaction: String): Cancelable { val params = FindReactionEventForUndoTask.Params( roomId, targetEventId, reaction ) - // TODO We should avoid using MatrixCallback internally - val callback = object : MatrixCallback { - override fun onSuccess(data: FindReactionEventForUndoTask.Result) { - if (data.redactEventId == null) { - Timber.w("Cannot find reaction to undo (not yet synced?)") - // TODO? - } - data.redactEventId?.let { toRedact -> - val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null) - .also { saveLocalEcho(it) } - eventSenderProcessor.postRedaction(redactEvent, null) - } - } + + val data = findReactionEventForUndoTask.executeRetry(params, Int.MAX_VALUE) + + return if (data.redactEventId == null) { + Timber.w("Cannot find reaction to undo (not yet synced?)") + // TODO? + NoOpCancellable + } else { + val redactEvent = eventFactory.createRedactEvent(roomId, data.redactEventId, null) + .also { saveLocalEcho(it) } + eventSenderProcessor.postRedaction(redactEvent, null) } - return findReactionEventForUndoTask - .configureWith(params) { - this.retryCount = Int.MAX_VALUE - this.callback = callback - } - .executeBy(taskExecutor) } override fun editPoll(targetEvent: TimelineEvent, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 385551ea94..8507b63d1f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -36,7 +36,6 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields -import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import timber.log.Timber import java.util.Collections @@ -198,11 +197,12 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, /** * Simple log that displays the number and timeline of loaded events */ - private fun logLoadedFromStorage(loadedFromStorage: LoadedFromStorage, direction: Timeline.Direction) = + private fun logLoadedFromStorage(loadedFromStorage: LoadedFromStorage, direction: Timeline.Direction) { Timber.v("[" + "${if (timelineSettings.isThreadTimeline()) "ThreadTimeLine" else "Timeline"}] Has loaded " + "${loadedFromStorage.numberOfEvents} items from storage in $direction " + if (timelineSettings.isThreadTimeline() && loadedFromStorage.threadReachedEnd) "[Reached End]" else "") + } fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? { val builtEventIndex = builtEventsIndexes[eventId] @@ -395,7 +395,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, paginationTask.execute(taskParams).toLoadMoreResult() } } catch (failure: Throwable) { - Timber.e("Failed to fetch from server: $failure", failure) + Timber.e(failure, "Failed to fetch from server") LoadMoreResult.FAILURE } return if (loadMoreResult == LoadMoreResult.SUCCESS) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 640fe53727..99e6521eb7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -354,7 +354,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity { val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) if (isLimited && lastChunk != null) { - lastChunk.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) + lastChunk.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true) } val chunkEntity = if (!isLimited && lastChunk != null) { lastChunk diff --git a/vector/build.gradle b/vector/build.gradle index 40c1da0cdc..e533327b82 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -18,7 +18,7 @@ ext.versionMinor = 3 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 18 +ext.versionPatch = 19 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt index d7e99c63dd..a5962d16fe 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt @@ -145,7 +145,7 @@ class ElementRobot { assertDisplayed(R.string.are_you_sure) clickOn(R.string.action_skip) waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer)) - }.onFailure { Timber.w("Verification popup missing", it) } + }.onFailure { Timber.w(it, "Verification popup missing") } } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt index 47bf31355c..b3bb5172e8 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt @@ -40,8 +40,11 @@ class OnboardingRobot { private fun crawlGetStarted() { clickOn(R.id.loginSplashSubmit) + assertDisplayed(R.id.useCaseHeaderTitle, R.string.ftue_auth_use_case_title) + clickOn(R.id.useCaseOptionOne) OnboardingServersRobot().crawlSignUp() pressBack() + pressBack() } private fun crawlAlreadyHaveAccount() { @@ -66,6 +69,7 @@ class OnboardingRobot { assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_create_account) if (createAccount) { clickOn(R.id.loginSplashSubmit) + clickOn(R.id.useCaseOptionOne) } else { clickOn(R.id.loginSplashAlreadyHaveAccount) } diff --git a/vector/src/main/java/im/vector/app/AutoRageShaker.kt b/vector/src/main/java/im/vector/app/AutoRageShaker.kt index 0238931e4c..43283254b1 100644 --- a/vector/src/main/java/im/vector/app/AutoRageShaker.kt +++ b/vector/src/main/java/im/vector/app/AutoRageShaker.kt @@ -16,7 +16,6 @@ package im.vector.app -import android.content.Context import android.content.SharedPreferences import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.rageshake.BugReporter @@ -46,7 +45,6 @@ class AutoRageShaker @Inject constructor( private val sessionDataSource: ActiveSessionDataSource, private val activeSessionHolder: ActiveSessionHolder, private val bugReporter: BugReporter, - private val context: Context, private val vectorPreferences: VectorPreferences ) : Session.Listener, SharedPreferences.OnSharedPreferenceChangeListener { @@ -136,7 +134,6 @@ class AutoRageShaker @Inject constructor( private fun sendRageShake(target: E2EMessageDetected) { bugReporter.sendBugReport( - context = context, reportType = ReportType.AUTO_UISI, withDevicesLogs = true, withCrashLogs = true, @@ -218,7 +215,6 @@ class AutoRageShaker @Inject constructor( val matchingIssue = event.content?.get("recipient_rageshake")?.toString() ?: "" bugReporter.sendBugReport( - context = context, reportType = ReportType.AUTO_UISI_SENDER, withDevicesLogs = true, withCrashLogs = true, diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index d252b5d9bd..e64188765e 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -120,7 +120,7 @@ class VectorApplication : vectorAnalytics.init() invitesAcceptor.initialize() autoRageShaker.initialize() - vectorUncaughtExceptionHandler.activate(this) + vectorUncaughtExceptionHandler.activate() // Remove Log handler statically added by Jitsi Timber.forest() diff --git a/vector/src/main/java/im/vector/app/core/datastore/DataStoreProvider.kt b/vector/src/main/java/im/vector/app/core/datastore/DataStoreProvider.kt new file mode 100644 index 0000000000..5b7988b76f --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/datastore/DataStoreProvider.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 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.app.core.datastore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import java.util.concurrent.ConcurrentHashMap +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * Provides a singleton datastore cache + * allows for lazily fetching a datastore instance by key to avoid creating multiple stores for the same file + * Based on https://androidx.tech/artifacts/datastore/datastore-preferences/1.0.0-source/androidx/datastore/preferences/PreferenceDataStoreDelegate.kt.html + * + * Makes use of a ReadOnlyProperty in order to provide a simplified api on top of a Context + * ReadOnlyProperty allows us to lazily access the backing property instead of requiring it upfront as a dependency + *
+ * val Context.dataStoreProvider by dataStoreProvider()
+ * 
+ */ +fun dataStoreProvider(): ReadOnlyProperty DataStore> { + return MappedPreferenceDataStoreSingletonDelegate() +} + +private class MappedPreferenceDataStoreSingletonDelegate : ReadOnlyProperty DataStore> { + + private val dataStoreCache = ConcurrentHashMap>() + private val provider: (Context) -> (String) -> DataStore = { context -> + { key -> + dataStoreCache.getOrPut(key) { + PreferenceDataStoreFactory.create { + context.applicationContext.preferencesDataStoreFile(key) + } + } + } + } + + override fun getValue(thisRef: Context, property: KProperty<*>) = provider.invoke(thisRef) +} diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index cce231708a..2cd7136ffc 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -39,7 +39,6 @@ import im.vector.app.features.discovery.DiscoverySettingsViewModel import im.vector.app.features.discovery.change.SetIdentityServerViewModel import im.vector.app.features.home.HomeActivityViewModel import im.vector.app.features.home.HomeDetailViewModel -import im.vector.app.features.home.PromoteRestrictedViewModel import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel import im.vector.app.features.home.UnreadMessagesSharedViewModel import im.vector.app.features.home.UserColorAccountDataViewModel @@ -61,6 +60,7 @@ import im.vector.app.features.login2.created.AccountCreatedViewModel import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel import im.vector.app.features.onboarding.OnboardingViewModel import im.vector.app.features.poll.create.CreatePollViewModel +import im.vector.app.features.qrcode.QrCodeScannerViewModel import im.vector.app.features.rageshake.BugReportViewModel import im.vector.app.features.reactions.EmojiSearchResultViewModel import im.vector.app.features.room.RequireActiveMembershipViewModel @@ -220,6 +220,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(CreateDirectRoomViewModel::class) fun createDirectRoomViewModelFactory(factory: CreateDirectRoomViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(QrCodeScannerViewModel::class) + fun qrCodeViewModelFactory(factory: QrCodeScannerViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(RoomNotificationSettingsViewModel::class) @@ -235,11 +240,6 @@ interface MavericksViewModelModule { @MavericksViewModelKey(SharedSecureStorageViewModel::class) fun sharedSecureStorageViewModelFactory(factory: SharedSecureStorageViewModel.Factory): MavericksAssistedViewModelFactory<*, *> - @Binds - @IntoMap - @MavericksViewModelKey(PromoteRestrictedViewModel::class) - fun promoteRestrictedViewModelFactory(factory: PromoteRestrictedViewModel.Factory): MavericksAssistedViewModelFactory<*, *> - @Binds @IntoMap @MavericksViewModelKey(UserListViewModel::class) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index 5295cbaec3..14ba34cc52 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -76,6 +76,9 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel + locationPinProvider?.create(locationOwnerId) { pinDrawable -> GlideApp.with(holder.staticMapPinImageView) .load(pinDrawable) .into(holder.staticMapPinImageView) diff --git a/vector/src/main/java/im/vector/app/core/extensions/Activity.kt b/vector/src/main/java/im/vector/app/core/extensions/Activity.kt index aa96a4a30c..829790f857 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Activity.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Activity.kt @@ -28,6 +28,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction +import im.vector.app.R fun ComponentActivity.registerStartForActivityResult(onResult: (ActivityResult) -> Unit): ActivityResultLauncher { return registerForActivityResult(ActivityResultContracts.StartActivityForResult(), onResult) @@ -66,8 +67,12 @@ fun AppCompatActivity.replaceFragment( fragmentClass: Class, params: Parcelable? = null, tag: String? = null, - allowStateLoss: Boolean = false) { + allowStateLoss: Boolean = false, + useCustomAnimation: Boolean = false) { supportFragmentManager.commitTransaction(allowStateLoss) { + if (useCustomAnimation) { + setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) + } replace(container.id, fragmentClass, params.toMvRxBundle(), tag) } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/Context.kt b/vector/src/main/java/im/vector/app/core/extensions/Context.kt index 1063d30a41..b8b367b740 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Context.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Context.kt @@ -23,7 +23,10 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.FloatRange import androidx.core.content.ContextCompat +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences import dagger.hilt.EntryPoints +import im.vector.app.core.datastore.dataStoreProvider import im.vector.app.core.di.SingletonEntryPoint import kotlin.math.roundToInt @@ -50,3 +53,5 @@ fun Context.getTintedDrawable(@DrawableRes drawableRes: Int, private fun Float.toAndroidAlpha(): Int { return (this * 255).roundToInt() } + +val Context.dataStoreProvider: (String) -> DataStore by dataStoreProvider() diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorDummyViewState.kt b/vector/src/main/java/im/vector/app/core/platform/VectorDummyViewState.kt new file mode 100644 index 0000000000..3c293b1072 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/platform/VectorDummyViewState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 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.app.core.platform + +import com.airbnb.mvrx.MavericksState + +data class VectorDummyViewState( + val isDummy: Unit = Unit +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 03e9954b2c..fe8d58fb51 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -36,5 +36,5 @@ class DefaultVectorFeatures : VectorFeatures { override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true override fun isOnboardingSplashCarouselEnabled() = true - override fun isOnboardingUseCaseEnabled() = false + override fun isOnboardingUseCaseEnabled() = true } diff --git a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt index 5d65d7ea42..3b92e7c4de 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt @@ -17,7 +17,6 @@ package im.vector.app.features.analytics.accountdata import androidx.lifecycle.asFlow -import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -26,6 +25,7 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorDummyViewState import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.log.analyticsTag @@ -42,24 +42,20 @@ import org.matrix.android.sdk.flow.flow import timber.log.Timber import java.util.UUID -data class DummyState( - val dummy: Boolean = false -) : MavericksState - class AnalyticsAccountDataViewModel @AssistedInject constructor( - @Assisted initialState: DummyState, + @Assisted initialState: VectorDummyViewState, private val session: Session, private val analytics: VectorAnalytics -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { private var checkDone: Boolean = false @AssistedFactory - interface Factory : MavericksAssistedViewModelFactory { - override fun create(initialState: DummyState): AnalyticsAccountDataViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: VectorDummyViewState): AnalyticsAccountDataViewModel } - companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { private const val ANALYTICS_EVENT_TYPE = "im.vector.analytics" } diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index d32cef604b..62d360f5f7 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -179,7 +179,7 @@ class DefaultVectorAnalytics @Inject constructor( posthog?.identify(REUSE_EXISTING_ID, identity.getProperties().toPostHogProperties(), IGNORED_OPTIONS) } - private fun Map?.toPostHogProperties(): Properties? { + private fun Map?.toPostHogProperties(): Properties? { if (this == null) return null return Properties().apply { diff --git a/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsEvent.kt b/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsEvent.kt index c6acb3b87a..2797734343 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsEvent.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsEvent.kt @@ -18,5 +18,5 @@ package im.vector.app.features.analytics.itf interface VectorAnalyticsEvent { fun getName(): String - fun getProperties(): Map? + fun getProperties(): Map? } diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Identity.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Identity.kt index 1cc433aa7e..99f1fadfc4 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Identity.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Identity.kt @@ -55,9 +55,9 @@ data class Identity( override fun getName() = "Identity" - override fun getProperties(): Map? { - return mutableMapOf().apply { - ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) } + override fun getProperties(): Map? { + return mutableMapOf().apply { + put("ftueUseCaseSelection", null) }.takeIf { it.isNotEmpty() } } } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt index da3425d326..83c7f0a13b 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt @@ -23,4 +23,8 @@ sealed class CreateDirectRoomAction : VectorViewModelAction { data class CreateRoomAndInviteSelectedUsers( val selections: Set ) : CreateDirectRoomAction() + + data class QrScannedAction( + val result: String + ) : CreateDirectRoomAction() } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index 0df9426852..2d93bab6a3 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -22,6 +22,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail @@ -44,6 +45,10 @@ import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.contactsbook.ContactsBookFragment +import im.vector.app.features.qrcode.QrCodeScannerEvents +import im.vector.app.features.qrcode.QrCodeScannerFragment +import im.vector.app.features.qrcode.QrCodeScannerViewModel +import im.vector.app.features.qrcode.QrScannerArgs import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragmentArgs import im.vector.app.features.userdirectory.UserListSharedAction @@ -59,6 +64,8 @@ import javax.inject.Inject class CreateDirectRoomActivity : SimpleFragmentActivity() { private val viewModel: CreateDirectRoomViewModel by viewModel() + private val qrViewModel: QrCodeScannerViewModel by viewModel() + private lateinit var sharedActionViewModel: UserListSharedActionViewModel @Inject lateinit var errorFormatter: ErrorFormatter @@ -93,11 +100,38 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { viewModel.onEach(CreateDirectRoomViewState::createAndInviteState) { renderCreateAndInviteState(it) } + + viewModel.observeViewEvents { + when (it) { + CreateDirectRoomViewEvents.InvalidCode -> { + Toast.makeText(this, R.string.invalid_qr_code_uri, Toast.LENGTH_SHORT).show() + finish() + } + CreateDirectRoomViewEvents.DmSelf -> { + Toast.makeText(this, R.string.cannot_dm_self, Toast.LENGTH_SHORT).show() + finish() + } + }.exhaustive + } + + qrViewModel.observeViewEvents { + when (it) { + is QrCodeScannerEvents.CodeParsed -> { + viewModel.handle(CreateDirectRoomAction.QrScannedAction(it.result)) + } + is QrCodeScannerEvents.ParseFailed -> { + Toast.makeText(this, R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show() + finish() + } + else -> Unit + }.exhaustive + } } private fun openAddByQrCode() { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, permissionCameraLauncher)) { - addFragment(views.container, CreateDirectRoomByQrCodeFragment::class.java) + val args = QrScannerArgs(showExtraButtons = false, R.string.add_by_qr_code) + addFragment(views.container, QrCodeScannerFragment::class.java, args) } } @@ -118,7 +152,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { private val permissionCameraLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { - addFragment(views.container, CreateDirectRoomByQrCodeFragment::class.java) + addFragment(views.container, QrCodeScannerFragment::class.java) } else if (deniedPermanently) { onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code) } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt deleted file mode 100644 index 766a6f5156..0000000000 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2020 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.createdirect - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import com.airbnb.mvrx.activityViewModel -import com.google.zxing.Result -import com.google.zxing.ResultMetadataType -import im.vector.app.R -import im.vector.app.core.extensions.hideKeyboard -import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO -import im.vector.app.core.utils.checkPermissions -import im.vector.app.core.utils.onPermissionDeniedDialog -import im.vector.app.core.utils.registerForPermissionsResult -import im.vector.app.databinding.FragmentQrCodeScannerBinding -import im.vector.app.features.userdirectory.PendingSelection -import me.dm7.barcodescanner.zxing.ZXingScannerView -import org.matrix.android.sdk.api.session.permalinks.PermalinkData -import org.matrix.android.sdk.api.session.permalinks.PermalinkParser -import org.matrix.android.sdk.api.session.user.model.User -import javax.inject.Inject - -class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragment(), ZXingScannerView.ResultHandler { - - private val viewModel: CreateDirectRoomViewModel by activityViewModel() - - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeScannerBinding { - return FragmentQrCodeScannerBinding.inflate(inflater, container, false) - } - - private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> - if (allGranted) { - startCamera() - } else if (deniedPermanently) { - activity?.onPermissionDeniedDialog(R.string.denied_permission_camera) - } - } - - private fun startCamera() { - // Start camera on resume - views.scannerView.startCamera() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - setupToolbar(views.qrScannerToolbar) - .setTitle(R.string.add_by_qr_code) - .allowBack(useCross = true) - } - - override fun onResume() { - super.onResume() - view?.hideKeyboard() - // Register ourselves as a handler for scan results. - views.scannerView.setResultHandler(this) - // Start camera on resume - if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) { - startCamera() - } - } - - override fun onPause() { - super.onPause() - // Unregister ourselves as a handler for scan results. - views.scannerView.setResultHandler(null) - // Stop camera on pause - views.scannerView.stopCamera() - } - - // Copied from https://github.com/markusfisch/BinaryEye/blob/ - // 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434 - private fun getRawBytes(result: Result): ByteArray? { - val metadata = result.resultMetadata ?: return null - val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null - var bytes = ByteArray(0) - @Suppress("UNCHECKED_CAST") - for (seg in segments as Iterable) { - bytes += seg - } - // byte segments can never be shorter than the text. - // Zxing cuts off content prefixes like "WIFI:" - return if (bytes.size >= result.text.length) bytes else null - } - - private fun addByQrCode(value: String) { - val mxid = (PermalinkParser.parse(value) as? PermalinkData.UserLink)?.userId - - if (mxid === null) { - Toast.makeText(requireContext(), R.string.invalid_qr_code_uri, Toast.LENGTH_SHORT).show() - requireActivity().finish() - } else { - // The following assumes MXIDs are case insensitive - if (mxid.equals(other = viewModel.session.myUserId, ignoreCase = true)) { - Toast.makeText(requireContext(), R.string.cannot_dm_self, Toast.LENGTH_SHORT).show() - requireActivity().finish() - } else { - // Try to get user from known users and fall back to creating a User object from MXID - val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null) - - viewModel.handle( - CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingSelection.UserPendingSelection(qrInvitee))) - ) - } - } - } - - override fun handleResult(result: Result?) { - if (result === null) { - Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show() - requireActivity().finish() - } else { - val rawBytes = getRawBytes(result) - val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1) - val value = rawBytesStr ?: result.text - addByQrCode(value) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewEvents.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewEvents.kt index 0c9804e9a4..060cb0c327 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewEvents.kt @@ -18,4 +18,7 @@ package im.vector.app.features.createdirect import im.vector.app.core.platform.VectorViewEvents -sealed class CreateDirectRoomViewEvents : VectorViewEvents +sealed class CreateDirectRoomViewEvents : VectorViewEvents { + object InvalidCode : CreateDirectRoomViewEvents() + object DmSelf : CreateDirectRoomViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt index 41360eab93..9dd3ef6a9b 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt @@ -34,13 +34,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.permalinks.PermalinkData +import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.user.model.User class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateDirectRoomViewState, private val rawService: RawService, val session: Session) : - VectorViewModel(initialState) { + VectorViewModel(initialState) { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -51,15 +54,33 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted override fun handle(action: CreateDirectRoomAction) { when (action) { - is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onSubmitInvitees(action) + is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onSubmitInvitees(action.selections) + is CreateDirectRoomAction.QrScannedAction -> onCodeParsed(action) }.exhaustive } + private fun onCodeParsed(action: CreateDirectRoomAction.QrScannedAction) { + val mxid = (PermalinkParser.parse(action.result) as? PermalinkData.UserLink)?.userId + + if (mxid === null) { + _viewEvents.post(CreateDirectRoomViewEvents.InvalidCode) + } else { + // The following assumes MXIDs are case insensitive + if (mxid.equals(other = session.myUserId, ignoreCase = true)) { + _viewEvents.post(CreateDirectRoomViewEvents.DmSelf) + } else { + // Try to get user from known users and fall back to creating a User object from MXID + val qrInvitee = if (session.getUser(mxid) != null) session.getUser(mxid)!! else User(mxid, null, null) + onSubmitInvitees(setOf(PendingSelection.UserPendingSelection(qrInvitee))) + } + } + } + /** * If users already have a DM room then navigate to it instead of creating a new room. */ - private fun onSubmitInvitees(action: CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers) { - val existingRoomId = action.selections.singleOrNull()?.getMxId()?.let { userId -> + private fun onSubmitInvitees(selections: Set) { + val existingRoomId = selections.singleOrNull()?.getMxId()?.let { userId -> session.getExistingDirectRoomWithUser(userId) } if (existingRoomId != null) { @@ -69,7 +90,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } } else { // Create the DM - createRoomAndInviteSelectedUsers(action.selections) + createRoomAndInviteSelectedUsers(selections) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index b083b74c53..6b6be63480 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -66,7 +66,6 @@ import im.vector.app.features.rageshake.ReportType import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity -import im.vector.app.features.spaces.RestrictedPromoBottomSheet import im.vector.app.features.spaces.SpaceCreationActivity import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet @@ -111,7 +110,6 @@ class HomeActivity : private val userColorAccountDataViewModel: UserColorAccountDataViewModel by viewModel() private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() - private val promoteRestrictedViewModel: PromoteRestrictedViewModel by viewModel() @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @@ -267,21 +265,6 @@ class HomeActivity : shortcutsHandler.observeRoomsAndBuildShortcuts(lifecycleScope) - if (!vectorPreferences.didPromoteNewRestrictedFeature()) { - promoteRestrictedViewModel.onEach { - if (it.activeSpaceSummary != null && !it.activeSpaceSummary.isPublic && - it.activeSpaceSummary.otherMemberIds.isNotEmpty()) { - // It's a private space with some members show this once - if (it.canUserManageSpace && !popupAlertManager.hasAlertsToShow()) { - if (!vectorPreferences.didPromoteNewRestrictedFeature()) { - vectorPreferences.setDidPromoteNewRestrictedFeature() - RestrictedPromoBottomSheet().show(supportFragmentManager, "RestrictedPromoBottomSheet") - } - } - } - } - } - if (isFirstCreation()) { handleIntent(intent) } @@ -473,14 +456,14 @@ class HomeActivity : override fun onResume() { super.onResume() - if (vectorUncaughtExceptionHandler.didAppCrash(this)) { - vectorUncaughtExceptionHandler.clearAppCrashStatus(this) + if (vectorUncaughtExceptionHandler.didAppCrash()) { + vectorUncaughtExceptionHandler.clearAppCrashStatus() MaterialAlertDialogBuilder(this) .setMessage(R.string.send_bug_report_app_crashed) .setCancelable(false) .setPositiveButton(R.string.yes) { _, _ -> bugReporter.openBugReportScreen(this) } - .setNegativeButton(R.string.no) { _, _ -> bugReporter.deleteCrashFile(this) } + .setNegativeButton(R.string.no) { _, _ -> bugReporter.deleteCrashFile() } .show() } else { showDisclaimerDialog(this) diff --git a/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt deleted file mode 100644 index 5c66e7c52d..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2021 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.app.features.home - -import com.airbnb.mvrx.MavericksState -import com.airbnb.mvrx.MavericksViewModelFactory -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import im.vector.app.AppStateHandler -import im.vector.app.RoomGroupingMethod -import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.MavericksAssistedViewModelFactory -import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.platform.EmptyAction -import im.vector.app.core.platform.EmptyViewEvents -import im.vector.app.core.platform.VectorViewModel -import kotlinx.coroutines.flow.distinctUntilChanged -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent -import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper - -data class ActiveSpaceViewState( - val isInSpaceMode: Boolean = false, - val activeSpaceSummary: RoomSummary? = null, - val canUserManageSpace: Boolean = false -) : MavericksState - -class PromoteRestrictedViewModel @AssistedInject constructor( - @Assisted initialState: ActiveSpaceViewState, - private val activeSessionHolder: ActiveSessionHolder, - appStateHandler: AppStateHandler -) : VectorViewModel(initialState) { - - init { - appStateHandler.selectedRoomGroupingFlow.distinctUntilChanged().execute { state -> - val groupingMethod = state.invoke()?.orNull() - val isSpaceMode = groupingMethod is RoomGroupingMethod.BySpace - val currentSpace = (groupingMethod as? RoomGroupingMethod.BySpace)?.spaceSummary - val canManage = currentSpace?.roomId?.let { roomId -> - activeSessionHolder.getSafeActiveSession() - ?.getRoom(roomId) - ?.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) - ?.content?.toModel()?.let { - PowerLevelsHelper(it).isUserAllowedToSend(activeSessionHolder.getActiveSession().myUserId, true, EventType.STATE_SPACE_CHILD) - } ?: false - } ?: false - - copy( - isInSpaceMode = isSpaceMode, - activeSpaceSummary = currentSpace, - canUserManageSpace = canManage - ) - } - } - - @AssistedFactory - interface Factory : MavericksAssistedViewModelFactory { - override fun create(initialState: ActiveSpaceViewState): PromoteRestrictedViewModel - } - - companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() - - override fun handle(action: EmptyAction) {} -} diff --git a/vector/src/main/java/im/vector/app/features/home/UserColorAccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UserColorAccountDataViewModel.kt index 3d4f219a7c..37e15af8b3 100644 --- a/vector/src/main/java/im/vector/app/features/home/UserColorAccountDataViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UserColorAccountDataViewModel.kt @@ -16,7 +16,6 @@ package im.vector.app.features.home -import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -25,6 +24,7 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorDummyViewState import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import kotlinx.coroutines.flow.launchIn @@ -37,22 +37,18 @@ import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.unwrap import timber.log.Timber -data class DummyState( - val dummy: Boolean = false -) : MavericksState - class UserColorAccountDataViewModel @AssistedInject constructor( - @Assisted initialState: DummyState, + @Assisted initialState: VectorDummyViewState, private val session: Session, private val matrixItemColorProvider: MatrixItemColorProvider -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { @AssistedFactory - interface Factory : MavericksAssistedViewModelFactory { - override fun create(initialState: DummyState): UserColorAccountDataViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: VectorDummyViewState): UserColorAccountDataViewModel } - companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { observeAccountData() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index c63085f647..22d5fc2a77 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -85,6 +85,8 @@ data class RoomDetailViewState( fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2 + fun isSearchAvailable() = asyncRoomSummary()?.isEncrypted == false + // This checks directly on the active room widgets. // It can differs for a short period of time on the JitsiState as its computed async. fun hasActiveJitsiWidget() = activeRoomWidgets()?.any { it.type == WidgetType.Jitsi && it.isActive }.orFalse() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 1deed976bb..2da69bbe6c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -88,6 +88,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.lifecycleAwareLazy import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.core.time.Clock import im.vector.app.core.ui.views.CurrentCallsView import im.vector.app.core.ui.views.CurrentCallsViewPresenter @@ -253,6 +254,7 @@ class TimelineFragment @Inject constructor( private val vectorPreferences: VectorPreferences, private val colorProvider: ColorProvider, private val dimensionConverter: DimensionConverter, + private val userPreferencesProvider: UserPreferencesProvider, private val notificationUtils: NotificationUtils, private val matrixItemColorProvider: MatrixItemColorProvider, private val imageContentRenderer: ImageContentRenderer, @@ -610,13 +612,14 @@ class TimelineFragment @Inject constructor( } private fun handleShowLocationPreview(locationContent: MessageLocationContent, senderId: String) { + val isSelfLocation = locationContent.isSelfLocation() navigator .openLocationSharing( context = requireContext(), roomId = timelineArgs.roomId, mode = LocationSharingMode.PREVIEW, initialLocationData = locationContent.toLocationData(), - locationOwnerId = senderId + locationOwnerId = if (isSelfLocation) senderId else null ) } @@ -1139,16 +1142,12 @@ class TimelineFragment @Inject constructor( } private fun handleSearchAction() { - if (session.getRoom(timelineArgs.roomId)?.isEncrypted() == false) { - navigator.openSearch( - context = requireContext(), - roomId = timelineArgs.roomId, - roomDisplayName = timelineViewModel.getRoomSummary()?.displayName, - roomAvatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl - ) - } else { - showDialogWithMessage(getString(R.string.search_is_not_supported_in_e2e_room)) - } + navigator.openSearch( + context = requireContext(), + roomId = timelineArgs.roomId, + roomDisplayName = timelineViewModel.getRoomSummary()?.displayName, + roomAvatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl + ) } private fun displayDisabledIntegrationDialog() { @@ -1804,7 +1803,7 @@ class TimelineFragment @Inject constructor( if (roomId != timelineArgs.roomId) return false // Navigation to same room if (!isThreadTimeLine()) { - if (rootThreadEventId != null) { + if (rootThreadEventId != null && userPreferencesProvider.areThreadMessagesEnabled()) { // Thread link, so PermalinkHandler will handle the navigation return false } @@ -1924,7 +1923,7 @@ class TimelineFragment @Inject constructor( timelineViewModel.handle(action) } is EncryptedEventContent -> { - timelineViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) + timelineViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) } is MessageLocationContent -> { handleShowLocationPreview(messageContent, informationData.senderId) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index cc3dabe16b..7d678520ec 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -720,7 +720,7 @@ class TimelineViewModel @AssistedInject constructor( R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined - R.id.search -> true + R.id.search -> state.isSearchAvailable() R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled() R.id.dev_tools -> vectorPreferences.developerMode() else -> false @@ -740,14 +740,22 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleUndoReact(action: RoomDetailAction.UndoReaction) { - room.undoReaction(action.targetEventId, action.reaction) + viewModelScope.launch { + tryOrNull { + room.undoReaction(action.targetEventId, action.reaction) + } + } } private fun handleUpdateQuickReaction(action: RoomDetailAction.UpdateQuickReactAction) { if (action.add) { room.sendReaction(action.targetEventId, action.selectedReaction) } else { - room.undoReaction(action.targetEventId, action.selectedReaction) + viewModelScope.launch { + tryOrNull { + room.undoReaction(action.targetEventId, action.selectedReaction) + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 086a093068..27937047a5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -45,6 +45,7 @@ import im.vector.app.features.location.toLocationData import im.vector.app.features.media.ImageContentRenderer import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent @@ -77,10 +78,12 @@ class MessageActionsEpoxyController @Inject constructor( val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL) val body = state.messageBody.linkify(host.listener) val bindingOptions = spanUtils.getBindingOptions(body) - val locationUrl = state.timelineEvent()?.root?.getClearContent() + + val locationContent = state.timelineEvent()?.root?.getClearContent() ?.toModel(catchError = true) - ?.toLocationData() + val locationUrl = locationContent?.toLocationData() ?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, 1200, 800) } + val locationOwnerId = if (locationContent?.isSelfLocation().orTrue()) state.informationData.matrixItem.id else null bottomSheetMessagePreviewItem { id("preview") @@ -96,6 +99,7 @@ class MessageActionsEpoxyController @Inject constructor( time(formattedDate) locationUrl(locationUrl) locationPinProvider(host.locationPinProvider) + locationOwnerId(locationOwnerId) } // Send state diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt index 97f2618fe6..0161f0b55d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -101,7 +101,11 @@ class CallItemFactory @Inject constructor( createCallTileTimelineItem( roomSummary = roomSummary, callId = callEventGrouper.callId, - callStatus = if (callEventGrouper.callWasMissed()) CallTileTimelineItem.CallStatus.MISSED else CallTileTimelineItem.CallStatus.ENDED, + callStatus = if (callEventGrouper.callWasAnswered()) { + CallTileTimelineItem.CallStatus.ENDED + } else { + CallTileTimelineItem.CallStatus.MISSED + }, callKind = callKind, callback = params.callback, highlight = params.isHighlighted, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 59b7ba3a8c..77bf5970af 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -219,10 +219,12 @@ class MessageItemFactory @Inject constructor( urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height) } + val userId = if (locationContent.isSelfLocation()) informationData.senderId else null + return MessageLocationItem_() .attributes(attributes) .locationUrl(locationUrl) - .userId(informationData.senderId) + .userId(userId) .locationPinProvider(locationPinProvider) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt index e92376c44d..0cf30c8c01 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt @@ -45,7 +45,17 @@ class LocationPinProvider @Inject constructor( GlideApp.with(context) } - fun create(userId: String, callback: (Drawable) -> Unit) { + /** + * Creates a pin drawable. If userId is null then a generic pin drawable will be created. + * @param userId userId that will be used to retrieve user avatar + * @param callback Pin drawable will be sent through the callback + */ + fun create(userId: String?, callback: (Drawable) -> Unit) { + if (userId == null) { + callback(ContextCompat.getDrawable(context, R.drawable.ic_location_pin)!!) + return + } + if (cache.contains(userId)) { callback(cache[userId]!!) return diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt index 3910204293..4ff8a9fa43 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt @@ -108,11 +108,8 @@ class CallSignalingEventsGroup(private val group: TimelineEventsGroup) { } } - /** - * Returns true if there are only events from one side. - */ - fun callWasMissed(): Boolean { - return group.events.distinctBy { it.senderInfo.userId }.size == 1 + fun callWasAnswered(): Boolean { + return getAnswer() != null } private fun getAnswer(): TimelineEvent? { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt index 6f0b6abb72..607458678e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -41,14 +41,13 @@ abstract class MessageLocationItem : AbsMessageItem( renderSendState(holder.view, null) val location = locationUrl ?: return - val locationOwnerId = userId ?: return GlideApp.with(holder.staticMapImageView) .load(location) .apply(RequestOptions.centerCropTransform()) .into(holder.staticMapImageView) - locationPinProvider?.create(locationOwnerId) { pinDrawable -> + locationPinProvider?.create(userId) { pinDrawable -> GlideApp.with(holder.staticMapPinImageView) .load(pinDrawable) .into(holder.staticMapPinImageView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListDisplayModeFilter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListDisplayModeFilter.kt deleted file mode 100644 index 94a79f5fbd..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListDisplayModeFilter.kt +++ /dev/null @@ -1,38 +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.app.features.home.room.list - -import androidx.core.util.Predicate -import im.vector.app.features.home.RoomListDisplayMode -import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.RoomSummary - -class RoomListDisplayModeFilter(private val displayMode: RoomListDisplayMode) : Predicate { - - override fun test(roomSummary: RoomSummary): Boolean { - if (roomSummary.membership.isLeft()) { - return false - } - return when (displayMode) { - RoomListDisplayMode.NOTIFICATIONS -> - roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE || roomSummary.userDrafts.isNotEmpty() - RoomListDisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership.isActive() - RoomListDisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership.isActive() - RoomListDisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/location/Config.kt b/vector/src/main/java/im/vector/app/features/location/Config.kt index 29ca6b81a9..6f947290e2 100644 --- a/vector/src/main/java/im/vector/app/features/location/Config.kt +++ b/vector/src/main/java/im/vector/app/features/location/Config.kt @@ -18,6 +18,7 @@ package im.vector.app.features.location const val MAP_BASE_URL = "https://api.maptiler.com/maps/streets/style.json" const val STATIC_MAP_BASE_URL = "https://api.maptiler.com/maps/basic/static/" +const val DEFAULT_PIN_ID = "DEFAULT_PIN_ID" const val INITIAL_MAP_ZOOM_IN_PREVIEW = 15.0 const val INITIAL_MAP_ZOOM_IN_TIMELINE = 17.0 diff --git a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt index c4f2f148bf..d993c76b0e 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt @@ -121,7 +121,7 @@ class LocationPreviewFragment @Inject constructor( MapState( zoomOnlyOnce = true, pinLocationData = location, - pinId = args.locationOwnerId, + pinId = args.locationOwnerId ?: DEFAULT_PIN_ID, pinDrawable = pinDrawable ) ) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt index 67b36b8442..10c271727b 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt @@ -30,7 +30,7 @@ data class LocationSharingArgs( val roomId: String, val mode: LocationSharingMode, val initialLocationData: LocationData?, - val locationOwnerId: String + val locationOwnerId: String? ) : Parcelable @AndroidEntryPoint diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index f6bad2826b..7099bec9f0 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -118,8 +118,4 @@ class LocationSharingFragment @Inject constructor( views.mapView.render(state.toMapState()) views.shareLocationGpsLoading.isGone = state.lastKnownLocation != null } - - companion object { - const val USER_PIN_NAME = "USER_PIN_NAME" - } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt index f3b937855a..a9a24094eb 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt @@ -42,6 +42,6 @@ data class LocationSharingViewState( fun LocationSharingViewState.toMapState() = MapState( zoomOnlyOnce = true, pinLocationData = lastKnownLocation, - pinId = LocationSharingFragment.USER_PIN_NAME, + pinId = DEFAULT_PIN_ID, pinDrawable = pinDrawable ) diff --git a/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt b/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt index c56481d3f2..2f71089a39 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt @@ -41,7 +41,8 @@ class SpaceCardRenderer @Inject constructor( fun render(spaceSummary: RoomSummary?, peopleYouKnow: List, matrixLinkCallback: TimelineEventController.UrlClickCallback?, - inCard: FragmentMatrixToRoomSpaceCardBinding) { + inCard: FragmentMatrixToRoomSpaceCardBinding, + showDescription: Boolean) { if (spaceSummary == null) { inCard.matrixToCardContentVisibility.isVisible = false inCard.matrixToCardButtonLoading.isVisible = true @@ -70,6 +71,8 @@ class SpaceCardRenderer @Inject constructor( inCard.matrixToMemberPills.isVisible = false } + inCard.matrixToCardDescText.isVisible = showDescription + renderPeopleYouKnow(inCard, peopleYouKnow.map { it.toMatrixItem() }) } inCard.matrixToCardDescText.movementMethod = createLinkMovementMethod(object : TimelineEventController.UrlClickCallback { diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 37d459edc2..b521710c1e 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -556,7 +556,7 @@ class DefaultNavigator @Inject constructor( roomId: String, mode: LocationSharingMode, initialLocationData: LocationData?, - locationOwnerId: String) { + locationOwnerId: String?) { val intent = LocationSharingActivity.getIntent( context, LocationSharingArgs(roomId = roomId, mode = mode, initialLocationData = initialLocationData, locationOwnerId = locationOwnerId) diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 0ba7625aa6..b5e94241ce 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -162,7 +162,8 @@ interface Navigator { roomId: String, mode: LocationSharingMode, initialLocationData: LocationData?, - locationOwnerId: String) + locationOwnerId: String?) + fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null) fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index 87cbf44f04..b67e779a33 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -22,6 +22,7 @@ import androidx.core.net.toUri import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.isIgnored +import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.core.utils.toast import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.navigation.Navigator @@ -40,6 +41,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomType import javax.inject.Inject class PermalinkHandler @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, + private val userPreferencesProvider: UserPreferencesProvider, private val navigator: Navigator) { suspend fun launch( @@ -200,15 +202,17 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti roomSummary: RoomSummary ) { if (this?.navToRoom(roomId, eventId, rawLink, rootThreadEventId) != true) { - rootThreadEventId?.let { + if (rootThreadEventId != null && userPreferencesProvider.areThreadMessagesEnabled()) { val threadTimelineArgs = ThreadTimelineArgs( roomId = roomId, displayName = roomSummary.displayName, avatarUrl = roomSummary.avatarUrl, roomEncryptionTrustLevel = roomSummary.roomEncryptionTrustLevel, - rootThreadEventId = it) + rootThreadEventId = rootThreadEventId) navigator.openThread(context, threadTimelineArgs, eventId) - } ?: navigator.openRoom(context, roomId, eventId, buildTask) + } else { + navigator.openRoom(context, roomId, eventId, buildTask) + } } } diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerAction.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerAction.kt new file mode 100644 index 0000000000..910f0246d3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerAction.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 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.app.features.qrcode + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class QrCodeScannerAction : VectorViewModelAction { + data class CodeDecoded( + val result: String, + val isQrCode: Boolean + ) : QrCodeScannerAction() + + object ScanFailed : QrCodeScannerAction() + + object SwitchMode : QrCodeScannerAction() +} diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt index d347bc0250..dda7b2e2eb 100644 --- a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt @@ -19,57 +19,55 @@ package im.vector.app.features.qrcode import android.app.Activity import android.content.Intent import android.os.Bundle +import android.widget.Toast import androidx.activity.result.ActivityResultLauncher -import com.google.zxing.BarcodeFormat -import com.google.zxing.Result -import com.google.zxing.ResultMetadataType +import com.airbnb.mvrx.viewModel import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding @AndroidEntryPoint -class QrCodeScannerActivity : VectorBaseActivity() { +class QrCodeScannerActivity() : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) override fun getCoordinatorLayout() = views.coordinatorLayout + private val qrViewModel: QrCodeScannerViewModel by viewModel() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + qrViewModel.observeViewEvents { + when (it) { + is QrCodeScannerEvents.CodeParsed -> { + setResultAndFinish(it.result, it.isQrCode) + } + is QrCodeScannerEvents.ParseFailed -> { + Toast.makeText(this, R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show() + finish() + } + else -> Unit + }.exhaustive + } + if (isFirstCreation()) { - replaceFragment(views.simpleFragmentContainer, QrCodeScannerFragment::class.java) + val args = QrScannerArgs(showExtraButtons = false, R.string.verification_scan_their_code) + replaceFragment(views.simpleFragmentContainer, QrCodeScannerFragment::class.java, args) } } - fun setResultAndFinish(result: Result?) { - if (result != null) { - val rawBytes = getRawBytes(result) - val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1) - - setResult(RESULT_OK, Intent().apply { - putExtra(EXTRA_OUT_TEXT, rawBytesStr ?: result.text) - putExtra(EXTRA_OUT_IS_QR_CODE, result.barcodeFormat == BarcodeFormat.QR_CODE) - }) - } + private fun setResultAndFinish(result: String, isQrCode: Boolean) { + setResult(RESULT_OK, Intent().apply { + putExtra(EXTRA_OUT_TEXT, result) + putExtra(EXTRA_OUT_IS_QR_CODE, isQrCode) + }) finish() } - // Copied from https://github.com/markusfisch/BinaryEye/blob/ - // 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434 - private fun getRawBytes(result: Result): ByteArray? { - val metadata = result.resultMetadata ?: return null - val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null - var bytes = ByteArray(0) - @Suppress("UNCHECKED_CAST") - for (seg in segments as Iterable) { - bytes += seg - } - // byte segments can never be shorter than the text. - // Zxing cuts off content prefixes like "WIFI:" - return if (bytes.size >= result.text.length) bytes else null - } - companion object { private const val EXTRA_OUT_TEXT = "EXTRA_OUT_TEXT" private const val EXTRA_OUT_IS_QR_CODE = "EXTRA_OUT_IS_QR_CODE" diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerEvents.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerEvents.kt new file mode 100644 index 0000000000..69a500238e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerEvents.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 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.app.features.qrcode + +import im.vector.app.core.platform.VectorViewEvents + +sealed class QrCodeScannerEvents : VectorViewEvents { + data class CodeParsed(val result: String, val isQrCode: Boolean) : QrCodeScannerEvents() + object ParseFailed : QrCodeScannerEvents() + object SwitchMode : QrCodeScannerEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt index a7231a0c5b..c514a1c8aa 100644 --- a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 New Vector Ltd + * Copyright (c) 2022 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. @@ -16,50 +16,157 @@ package im.vector.app.features.qrcode +import android.app.Activity import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.core.view.isVisible +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.args +import com.google.zxing.BarcodeFormat import com.google.zxing.Result +import com.google.zxing.ResultMetadataType import im.vector.app.R +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedDialog +import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentQrCodeScannerBinding +import im.vector.app.features.usercode.QRCodeBitmapDecodeHelper +import im.vector.lib.multipicker.MultiPicker +import im.vector.lib.multipicker.utils.ImageUtils +import kotlinx.parcelize.Parcelize import me.dm7.barcodescanner.zxing.ZXingScannerView +import org.matrix.android.sdk.api.extensions.tryOrNull import javax.inject.Inject -class QrCodeScannerFragment @Inject constructor() : - VectorBaseFragment(), - ZXingScannerView.ResultHandler { +@Parcelize +data class QrScannerArgs( + val showExtraButtons: Boolean, + @StringRes val titleRes: Int +) : Parcelable + +open class QrCodeScannerFragment @Inject constructor() : VectorBaseFragment(), ZXingScannerView.ResultHandler { + + private val qrViewModel: QrCodeScannerViewModel by activityViewModel() + private val scannerArgs: QrScannerArgs? by args() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeScannerBinding { return FragmentQrCodeScannerBinding.inflate(inflater, container, false) } + private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + startCamera() + } else if (deniedPermanently) { + activity?.onPermissionDeniedDialog(R.string.denied_permission_camera) + } + } + + private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + MultiPicker + .get(MultiPicker.IMAGE) + .getSelectedFiles(requireActivity(), activityResult.data) + .firstOrNull() + ?.contentUri + ?.let { uri -> + // try to see if it is a valid matrix code + val bitmap = ImageUtils.getBitmap(requireContext(), uri) + ?: return@let Unit.also { + Toast.makeText(requireContext(), getString(R.string.qr_code_not_scanned), Toast.LENGTH_SHORT).show() + } + handleResult(tryOrNull { QRCodeBitmapDecodeHelper.decodeQRFromBitmap(bitmap) }) + } + } + } + + private var autoFocus = true + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val title = scannerArgs?.titleRes?.let { getString(it) } + setupToolbar(views.qrScannerToolbar) - .setTitle(R.string.verification_scan_their_code) + .setTitle(title) .allowBack(useCross = true) + + scannerArgs?.showExtraButtons?.let { showButtons -> + views.userCodeMyCodeButton.isVisible = showButtons + views.userCodeOpenGalleryButton.isVisible = showButtons + + if (showButtons) { + views.userCodeOpenGalleryButton.debouncedClicks { + MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher) + } + views.userCodeMyCodeButton.debouncedClicks { + qrViewModel.handle(QrCodeScannerAction.SwitchMode) + } + } + } + } + + private fun startCamera() { + with(views.qrScannerView) { + startCamera() + setAutoFocus(autoFocus) + debouncedClicks { + autoFocus = !autoFocus + setAutoFocus(autoFocus) + } + } } override fun onResume() { super.onResume() + view?.hideKeyboard() + // Register ourselves as a handler for scan results. - views.scannerView.setResultHandler(this) - // Start camera on resume - views.scannerView.startCamera() + views.qrScannerView.setResultHandler(this) + + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) { + startCamera() + } } override fun onPause() { super.onPause() - // Stop camera on pause - views.scannerView.stopCamera() + views.qrScannerView.setResultHandler(null) + views.qrScannerView.stopCamera() + } + + // Copied from https://github.com/markusfisch/BinaryEye/blob/ + // 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434 + private fun getRawBytes(result: Result): ByteArray? { + val metadata = result.resultMetadata ?: return null + val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null + var bytes = ByteArray(0) + @Suppress("UNCHECKED_CAST") + for (seg in segments as Iterable) { + bytes += seg + } + // byte segments can never be shorter than the text. + // Zxing cuts off content prefixes like "WIFI:" + return if (bytes.size >= result.text.length) bytes else null } override fun handleResult(rawResult: Result?) { - // Do something with the result here - // This is not intended to be used outside of QrCodeScannerActivity for the moment - (requireActivity() as? QrCodeScannerActivity)?.setResultAndFinish(rawResult) + if (rawResult == null) { + qrViewModel.handle(QrCodeScannerAction.ScanFailed) + } else { + val rawBytes = getRawBytes(rawResult) + val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1) + val result = rawBytesStr ?: rawResult.text + val isQrCode = rawResult.barcodeFormat == BarcodeFormat.QR_CODE + qrViewModel.handle(QrCodeScannerAction.CodeDecoded(result, isQrCode)) + } } } diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerViewModel.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerViewModel.kt new file mode 100644 index 0000000000..ef47ea1a6e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerViewModel.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 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.app.features.qrcode + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.VectorDummyViewState +import im.vector.app.core.platform.VectorViewModel +import org.matrix.android.sdk.api.session.Session + +class QrCodeScannerViewModel @AssistedInject constructor( + @Assisted initialState: VectorDummyViewState, + val session: Session +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: VectorDummyViewState): QrCodeScannerViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + override fun handle(action: QrCodeScannerAction) { + _viewEvents.post( + when (action) { + is QrCodeScannerAction.CodeDecoded -> QrCodeScannerEvents.CodeParsed(action.result, action.isQrCode) + is QrCodeScannerAction.SwitchMode -> QrCodeScannerEvents.SwitchMode + is QrCodeScannerAction.ScanFailed -> QrCodeScannerEvents.ParseFailed + } + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt index 0aec24f4ac..2d4bc704a4 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt @@ -151,7 +151,7 @@ class BugReportActivity : VectorBaseActivity() { views.bugReportProgressView.isVisible = true views.bugReportProgressView.progress = 0 - bugReporter.sendBugReport(this, + bugReporter.sendBugReport( reportType, views.bugReportButtonIncludeLogs.isChecked, views.bugReportButtonIncludeCrashLogs.isChecked, @@ -249,7 +249,7 @@ class BugReportActivity : VectorBaseActivity() { override fun onBackPressed() { // Ensure there is no crash status remaining, which will be sent later on by mistake - bugReporter.deleteCrashFile(this) + bugReporter.deleteCrashFile() super.onBackPressed() } diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt index b62a182fd8..2c554716d2 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt @@ -68,6 +68,7 @@ import javax.inject.Singleton */ @Singleton class BugReporter @Inject constructor( + private val context: Context, private val activeSessionHolder: ActiveSessionHolder, private val versionProvider: VersionProvider, private val vectorPreferences: VectorPreferences, @@ -153,7 +154,6 @@ class BugReporter @Inject constructor( /** * Send a bug report. * - * @param context the application context * @param reportType The report type (bug, suggestion, feedback) * @param withDevicesLogs true to include the device log * @param withCrashLogs true to include the crash logs @@ -163,8 +163,7 @@ class BugReporter @Inject constructor( * @param listener the listener */ @SuppressLint("StaticFieldLeak") - fun sendBugReport(context: Context, - reportType: ReportType, + fun sendBugReport(reportType: ReportType, withDevicesLogs: Boolean, withCrashLogs: Boolean, withKeyRequestHistory: Boolean, @@ -182,7 +181,7 @@ class BugReporter @Inject constructor( var reportURL: String? = null withContext(Dispatchers.IO) { var bugDescription = theBugDescription - val crashCallStack = getCrashDescription(context) + val crashCallStack = getCrashDescription() if (null != crashCallStack) { bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n" @@ -203,7 +202,7 @@ class BugReporter @Inject constructor( } if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) { - val gzippedLogcat = saveLogCat(context, false) + val gzippedLogcat = saveLogCat(false) if (null != gzippedLogcat) { if (gzippedFiles.size == 0) { @@ -213,7 +212,7 @@ class BugReporter @Inject constructor( } } - val crashDescription = getCrashFile(context) + val crashDescription = getCrashFile() if (crashDescription.exists()) { val compressedCrashDescription = compressFile(crashDescription) @@ -265,7 +264,7 @@ class BugReporter @Inject constructor( // build the multi part request val builder = BugReporterMultipartBody.Builder() .addFormDataPart("text", text) - .addFormDataPart("app", rageShakeAppNameForReport(context, reportType)) + .addFormDataPart("app", rageShakeAppNameForReport(reportType)) .addFormDataPart("user_agent", Matrix.getInstance(context).getUserAgent()) .addFormDataPart("user_id", userId) .addFormDataPart("can_contact", canContact.toString()) @@ -352,9 +351,9 @@ class BugReporter @Inject constructor( } } - if (getCrashFile(context).exists()) { + if (getCrashFile().exists()) { builder.addFormDataPart("label", "crash") - deleteCrashFile(context) + deleteCrashFile() } val requestBody = builder.build() @@ -487,20 +486,16 @@ class BugReporter @Inject constructor( activity.startActivity(BugReportActivity.intent(activity, reportType)) } - private fun rageShakeAppNameForReport(context: Context, reportType: ReportType): String { + private fun rageShakeAppNameForReport(reportType: ReportType): String { // As per https://github.com/matrix-org/rageshake // app: Identifier for the application (eg 'riot-web'). // Should correspond to a mapping configured in the configuration file for github issue reporting to work. // (see R.string.bug_report_url for configured RS server) - return when (reportType) { + return context.getString(when (reportType) { ReportType.AUTO_UISI_SENDER, - ReportType.AUTO_UISI -> { - context.getString(R.string.bug_report_auto_uisi_app_name) - } - else -> { - context.getString(R.string.bug_report_app_name) - } - } + ReportType.AUTO_UISI -> R.string.bug_report_auto_uisi_app_name + else -> R.string.bug_report_app_name + }) } // ============================================================================================================== // crash report management @@ -509,20 +504,17 @@ class BugReporter @Inject constructor( /** * Provides the crash file * - * @param context the context * @return the crash file */ - private fun getCrashFile(context: Context): File { + private fun getCrashFile(): File { return File(context.cacheDir.absolutePath, CRASH_FILENAME) } /** * Remove the crash file - * - * @param context */ - fun deleteCrashFile(context: Context) { - val crashFile = getCrashFile(context) + fun deleteCrashFile() { + val crashFile = getCrashFile() if (crashFile.exists()) { crashFile.delete() @@ -535,11 +527,10 @@ class BugReporter @Inject constructor( /** * Save the crash report * - * @param context the context * @param crashDescription teh crash description */ - fun saveCrashReport(context: Context, crashDescription: String) { - val crashFile = getCrashFile(context) + fun saveCrashReport(crashDescription: String) { + val crashFile = getCrashFile() if (crashFile.exists()) { crashFile.delete() @@ -557,11 +548,10 @@ class BugReporter @Inject constructor( /** * Read the crash description file and return its content. * - * @param context teh context * @return the crash description */ - private fun getCrashDescription(context: Context): String? { - val crashFile = getCrashFile(context) + private fun getCrashDescription(): String? { + val crashFile = getCrashFile() if (crashFile.exists()) { try { @@ -650,11 +640,10 @@ class BugReporter @Inject constructor( /** * Save the logcat * - * @param context the context * @param isErrorLogcat true to save the error logcat * @return the file if the operation succeeds */ - private fun saveLogCat(context: Context, isErrorLogcat: Boolean): File? { + private fun saveLogCat(isErrorLogcat: Boolean): File? { val logCatErrFile = File(context.cacheDir.absolutePath, if (isErrorLogcat) LOG_CAT_ERROR_FILENAME else LOG_CAT_FILENAME) if (logCatErrFile.exists()) { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt b/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt index 6954b9c87b..670b28f1e1 100644 --- a/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt @@ -30,9 +30,12 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter: BugReporter, - private val versionProvider: VersionProvider, - private val versionCodeProvider: VersionCodeProvider) : Thread.UncaughtExceptionHandler { +class VectorUncaughtExceptionHandler @Inject constructor( + context: Context, + private val bugReporter: BugReporter, + private val versionProvider: VersionProvider, + private val versionCodeProvider: VersionCodeProvider +) : Thread.UncaughtExceptionHandler { // key to save the crash status companion object { @@ -41,13 +44,12 @@ class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter private var previousHandler: Thread.UncaughtExceptionHandler? = null - private lateinit var context: Context + private val preferences = DefaultSharedPreferences.getInstance(context) /** * Activate this handler */ - fun activate(context: Context) { - this.context = context + fun activate() { previousHandler = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler(this) } @@ -61,7 +63,7 @@ class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter */ override fun uncaughtException(thread: Thread, throwable: Throwable) { Timber.v("Uncaught exception: $throwable") - DefaultSharedPreferences.getInstance(context).edit { + preferences.edit { putBoolean(PREFS_CRASH_KEY, true) } val b = StringBuilder() @@ -103,7 +105,7 @@ class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter val bugDescription = b.toString() Timber.e("FATAL EXCEPTION $bugDescription") - bugReporter.saveCrashReport(context, bugDescription) + bugReporter.saveCrashReport(bugDescription) // Show the classical system popup previousHandler?.uncaughtException(thread, throwable) @@ -114,16 +116,15 @@ class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter * * @return true if the application crashed */ - fun didAppCrash(context: Context): Boolean { - return DefaultSharedPreferences.getInstance(context) - .getBoolean(PREFS_CRASH_KEY, false) + fun didAppCrash(): Boolean { + return preferences.getBoolean(PREFS_CRASH_KEY, false) } /** * Clear the crash status */ - fun clearAppCrashStatus(context: Context) { - DefaultSharedPreferences.getInstance(context).edit { + fun clearAppCrashStatus() { + preferences.edit { remove(PREFS_CRASH_KEY) } } diff --git a/vector/src/main/java/im/vector/app/features/session/VectorSessionStore.kt b/vector/src/main/java/im/vector/app/features/session/VectorSessionStore.kt index ce85eeeb98..a2f3196979 100644 --- a/vector/src/main/java/im/vector/app/features/session/VectorSessionStore.kt +++ b/vector/src/main/java/im/vector/app/features/session/VectorSessionStore.kt @@ -17,44 +17,42 @@ package im.vector.app.features.session import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore +import im.vector.app.core.extensions.dataStoreProvider import im.vector.app.features.onboarding.FtueUseCase import kotlinx.coroutines.flow.first import org.matrix.android.sdk.internal.util.md5 /** - * Local storage for: + * User session scoped storage for: * - messaging use case (Enum/String) */ class VectorSessionStore constructor( - private val context: Context, + context: Context, myUserId: String ) { - private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_session_store_${myUserId.md5()}") private val useCaseKey = stringPreferencesKey("use_case") + private val dataStore by lazy { context.dataStoreProvider("vector_session_store_${myUserId.md5()}") } - suspend fun readUseCase() = context.dataStore.data.first().let { preferences -> + suspend fun readUseCase() = dataStore.data.first().let { preferences -> preferences[useCaseKey]?.let { FtueUseCase.from(it) } } suspend fun setUseCase(useCase: FtueUseCase) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings[useCaseKey] = useCase.persistableValue } } suspend fun resetUseCase() { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings.remove(useCaseKey) } } suspend fun clear() { - context.dataStore.edit { settings -> settings.clear() } + dataStore.edit { settings -> settings.clear() } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 1903b3776a..f248882211 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -185,7 +185,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY" private const val DID_ASK_TO_ENABLE_SESSION_PUSH = "DID_ASK_TO_ENABLE_SESSION_PUSH" - private const val DID_PROMOTE_NEW_RESTRICTED_JOIN_RULE = "DID_PROMOTE_NEW_RESTRICTED_JOIN_RULE" // Location Sharing const val SETTINGS_PREF_ENABLE_LOCATION_SHARING = "SETTINGS_PREF_ENABLE_LOCATION_SHARING" @@ -356,16 +355,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { } } - fun didPromoteNewRestrictedFeature(): Boolean { - return defaultPrefs.getBoolean(DID_PROMOTE_NEW_RESTRICTED_JOIN_RULE, false) - } - - fun setDidPromoteNewRestrictedFeature() { - defaultPrefs.edit { - putBoolean(DID_PROMOTE_NEW_RESTRICTED_JOIN_RULE, true) - } - } - /** * Tells if we have already asked the user to disable battery optimisations on android >= M devices. * diff --git a/vector/src/main/java/im/vector/app/features/spaces/RestrictedPromoBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/RestrictedPromoBottomSheet.kt deleted file mode 100644 index dbea6807ce..0000000000 --- a/vector/src/main/java/im/vector/app/features/spaces/RestrictedPromoBottomSheet.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2021 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.app.features.spaces - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import im.vector.app.R -import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment -import im.vector.app.databinding.BottomSheetSpaceAdvertiseRestrictedBinding - -class RestrictedPromoBottomSheet : VectorBaseBottomSheetDialogFragment() { - - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = - BottomSheetSpaceAdvertiseRestrictedBinding.inflate(inflater, container, false) - - override val showExpanded = true - - var learnMoreMode: Boolean = false - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - render() - views.skipButton.debouncedClicks { - dismiss() - } - - views.learnMore.debouncedClicks { - if (learnMoreMode) { - dismiss() - } else { - learnMoreMode = true - render() - } - } - } - - private fun render() { - if (learnMoreMode) { - views.title.text = getString(R.string.new_let_people_in_spaces_find_and_join) - views.topDescription.text = getString(R.string.to_help_space_members_find_and_join) - views.imageHint.isVisible = true - views.bottomDescription.isVisible = true - views.bottomDescription.text = getString(R.string.this_makes_it_easy_for_rooms_to_stay_private_to_a_space) - views.skipButton.isVisible = false - views.learnMore.text = getString(R.string.ok) - } else { - views.title.text = getString(R.string.help_space_members) - views.topDescription.text = getString(R.string.help_people_in_spaces_find_and_join) - views.imageHint.isVisible = false - views.bottomDescription.isVisible = false - views.skipButton.isVisible = true - views.learnMore.text = getString(R.string.learn_more) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt index bbf6ac79ca..955fedd7dc 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt @@ -170,7 +170,7 @@ class SpaceDirectoryFragment @Inject constructor( ?: getString(R.string.space_explore_activity_title) } - spaceCardRenderer.render(state.currentRootSummary, emptyList(), this, views.spaceCard) + spaceCardRenderer.render(state.currentRootSummary, emptyList(), this, views.spaceCard, showDescription = false) views.addOrCreateChatRoomButton.isVisible = state.canAddRooms } diff --git a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt index 815175c977..91cb6194b1 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt @@ -118,7 +118,7 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment { val intent = InviteUsersToRoomActivity.getIntent(requireContext(), event.spaceId) startActivity(intent) + dismissAllowingStateLoss() } is ShareSpaceViewEvents.ShowInviteByLink -> { startSharePlainTextIntent( @@ -94,6 +95,7 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment(), - ZXingScannerView.ResultHandler { - - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeScannerWithButtonBinding { - return FragmentQrCodeScannerWithButtonBinding.inflate(inflater, container, false) - } - - val sharedViewModel: UserCodeSharedViewModel by activityViewModel() - - var autoFocus = true - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - setupToolbar(views.qrScannerToolbar) - .allowBack(useCross = true) - - views.userCodeMyCodeButton.debouncedClicks { - sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW)) - } - - views.userCodeOpenGalleryButton.debouncedClicks { - MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher) - } - } - - private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, _ -> - if (allGranted) { - startCamera() - } else { - // For now just go back - sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW)) - } - } - - private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult -> - if (activityResult.resultCode == Activity.RESULT_OK) { - MultiPicker - .get(MultiPicker.IMAGE) - .getSelectedFiles(requireActivity(), activityResult.data) - .firstOrNull() - ?.contentUri - ?.let { uri -> - // try to see if it is a valid matrix code - val bitmap = ImageUtils.getBitmap(requireContext(), uri) - ?: return@let Unit.also { - Toast.makeText(requireContext(), getString(R.string.qr_code_not_scanned), Toast.LENGTH_SHORT).show() - } - handleResult(tryOrNull { QRCodeBitmapDecodeHelper.decodeQRFromBitmap(bitmap) }) - } - } - } - - private fun startCamera() { - views.userCodeScannerView.startCamera() - views.userCodeScannerView.setAutoFocus(autoFocus) - views.userCodeScannerView.debouncedClicks { - this.autoFocus = !autoFocus - views.userCodeScannerView.setAutoFocus(autoFocus) - } - } - - override fun onStart() { - super.onStart() - if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) { - startCamera() - } - } - - override fun onResume() { - super.onResume() - // Register ourselves as a handler for scan results. - views.userCodeScannerView.setResultHandler(this) - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { - startCamera() - } - } - - override fun onPause() { - super.onPause() - views.userCodeScannerView.setResultHandler(null) - // Stop camera on pause - views.userCodeScannerView.stopCamera() - } - - override fun handleResult(result: Result?) { - if (result === null) { - Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show() - requireActivity().finish() - } else { - val rawBytes = getRawBytes(result) - val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1) - val value = rawBytesStr ?: result.text - sharedViewModel.handle(UserCodeActions.DecodedQRCode(value)) - } - } - - // Copied from https://github.com/markusfisch/BinaryEye/blob/ - // 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434 - private fun getRawBytes(result: Result): ByteArray? { - val metadata = result.resultMetadata ?: return null - val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null - var bytes = ByteArray(0) - @Suppress("UNCHECKED_CAST") - for (seg in segments as Iterable) { - bytes += seg - } - // byte segments can never be shorter than the text. - // Zxing cuts off content prefixes like "WIFI:" - return if (bytes.size >= result.text.length) bytes else null - } -} diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt index 7011f8c280..356893aee2 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt @@ -30,12 +30,16 @@ import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.matrixto.MatrixToBottomSheet +import im.vector.app.features.qrcode.QrCodeScannerEvents +import im.vector.app.features.qrcode.QrCodeScannerFragment +import im.vector.app.features.qrcode.QrCodeScannerViewModel +import im.vector.app.features.qrcode.QrScannerArgs import kotlinx.parcelize.Parcelize import kotlin.reflect.KClass @@ -44,6 +48,7 @@ class UserCodeActivity : VectorBaseActivity(), MatrixToBottomSheet.InteractionListener { val sharedViewModel: UserCodeSharedViewModel by viewModel() + private val qrViewModel: QrCodeScannerViewModel by viewModel() @Parcelize data class Args( @@ -81,10 +86,13 @@ class UserCodeActivity : VectorBaseActivity(), sharedViewModel.onEach(UserCodeState::mode) { mode -> when (mode) { - UserCodeState.Mode.SHOW -> showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) - UserCodeState.Mode.SCAN -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY) + UserCodeState.Mode.SHOW -> showFragment(ShowUserCodeFragment::class) + UserCodeState.Mode.SCAN -> { + val args = QrScannerArgs(showExtraButtons = true, R.string.user_code_scan) + showFragment(QrCodeScannerFragment::class, args) + } is UserCodeState.Mode.RESULT -> { - showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) + showFragment(ShowUserCodeFragment::class) MatrixToBottomSheet.withLink(mode.rawLink).show(supportFragmentManager, "MatrixToBottomSheet") } } @@ -106,6 +114,21 @@ class UserCodeActivity : VectorBaseActivity(), } } } + + qrViewModel.observeViewEvents { + when (it) { + is QrCodeScannerEvents.CodeParsed -> { + sharedViewModel.handle(UserCodeActions.DecodedQRCode(it.result)) + } + QrCodeScannerEvents.SwitchMode -> { + sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW)) + } + is QrCodeScannerEvents.ParseFailed -> { + Toast.makeText(this, R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show() + finish() + } + }.exhaustive + } } override fun onDestroy() { @@ -113,16 +136,9 @@ class UserCodeActivity : VectorBaseActivity(), super.onDestroy() } - private fun showFragment(fragmentClass: KClass, bundle: Bundle) { + private fun showFragment(fragmentClass: KClass, params: Parcelable? = null) { if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { - supportFragmentManager.commitTransaction { - setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) - replace(views.simpleFragmentContainer.id, - fragmentClass.java, - bundle, - fragmentClass.simpleName - ) - } + replaceFragment(views.simpleFragmentContainer, fragmentClass.java, params, fragmentClass.simpleName, useCustomAnimation = true) } } diff --git a/vector/src/main/res/drawable-nodpi/room_settings.png b/vector/src/main/res/drawable-nodpi/room_settings.png deleted file mode 100644 index 2e3fb404fa..0000000000 Binary files a/vector/src/main/res/drawable-nodpi/room_settings.png and /dev/null differ diff --git a/vector/src/main/res/drawable/ic_location_pin.xml b/vector/src/main/res/drawable/ic_location_pin.xml new file mode 100644 index 0000000000..8227ea4e05 --- /dev/null +++ b/vector/src/main/res/drawable/ic_location_pin.xml @@ -0,0 +1,13 @@ + + + + diff --git a/vector/src/main/res/layout/bottom_sheet_space_advertise_restricted.xml b/vector/src/main/res/layout/bottom_sheet_space_advertise_restricted.xml deleted file mode 100644 index 7cc243ee75..0000000000 --- a/vector/src/main/res/layout/bottom_sheet_space_advertise_restricted.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - -