Bubbles: merge develop

This commit is contained in:
ganfra 2022-02-03 17:10:13 +01:00
commit 2e2da16a6b
95 changed files with 831 additions and 1009 deletions

View File

@ -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

View File

@ -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)
=======================================

1
changelog.d/3907.bugfix Normal file
View File

@ -0,0 +1 @@
Fixes non sans-serif font weights being ignored

1
changelog.d/4295.misc Normal file
View File

@ -0,0 +1 @@
"Invite users to space" dialog now closed when user choose invite method

1
changelog.d/4304.misc Normal file
View File

@ -0,0 +1 @@
Changed layout for space card and room card used at "explore room" screen and space/room invite dialogs

1
changelog.d/4315.misc Normal file
View File

@ -0,0 +1 @@
Removed spaces restricted search hint dialogs

1
changelog.d/4641.misc Normal file
View File

@ -0,0 +1 @@
Remove Search from room options if not available

1
changelog.d/4873.misc Normal file
View File

@ -0,0 +1 @@
Qr code scanning fragments merged into one

1
changelog.d/5038.bugfix Normal file
View File

@ -0,0 +1 @@
Fixing missing/intermittent notifications on the google play variant when wifi is enabled

1
changelog.d/5088.bugfix Normal file
View File

@ -0,0 +1 @@
Fixes call statuses in the timeline for missed/rejected calls and connected calls.

1
changelog.d/5128.bugfix Normal file
View File

@ -0,0 +1 @@
Fix fallback permalink when threads are disabled

1
changelog.d/5146.feature Normal file
View File

@ -0,0 +1 @@
Support generic location pin

View File

@ -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

View File

@ -107,9 +107,6 @@
<!-- disable the overscroll because setOverscrollHeader/Footer don't always work -->
<item name="android:overScrollMode">never</item>
<!-- fonts -->
<item name="android:typeface">sans</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>

View File

@ -107,9 +107,6 @@
<!-- disable the overscroll because setOverscrollHeader/Footer don't always work -->
<item name="android:overScrollMode">never</item>
<!-- fonts -->
<item name="android:typeface">sans</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>

View File

@ -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()}\""

View File

@ -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
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -57,7 +57,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
) : RealmMigration {
companion object {
const val SESSION_STORE_SCHEMA_VERSION = 23L
const val SESSION_STORE_SCHEMA_VERSION = 24L
}
/**
@ -93,6 +93,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion <= 20) migrateTo21(realm)
if (oldVersion <= 21) migrateTo22(realm)
if (oldVersion <= 22) migrateTo23(realm)
if (oldVersion <= 23) migrateTo24(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@ -451,6 +452,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")
@ -464,8 +481,8 @@ internal class RealmSessionStoreMigration @Inject constructor(
?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
}
private fun migrateTo23(realm: DynamicRealm) {
Timber.d("Step 22 -> 23")
private fun migrateTo24(realm: DynamicRealm) {
Timber.d("Step 23 -> 24")
realm.schema.get("PreviewUrlCacheEntity")
?.addField(PreviewUrlCacheEntityFields.IMAGE_WIDTH, Int::class.java)
?.setNullable(PreviewUrlCacheEntityFields.IMAGE_WIDTH, true)

View File

@ -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()
}

View File

@ -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
}
/**

View File

@ -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")

View File

@ -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<FindReactionEventForUndoTask.Result> {
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,

View File

@ -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) {

View File

@ -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

View File

@ -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'

View File

@ -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") }
}
}

View File

@ -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)
}

View File

@ -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,

View File

@ -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()

View File

@ -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
* <pre>
* val Context.dataStoreProvider by dataStoreProvider()
* </pre>
*/
fun dataStoreProvider(): ReadOnlyProperty<Context, (String) -> DataStore<Preferences>> {
return MappedPreferenceDataStoreSingletonDelegate()
}
private class MappedPreferenceDataStoreSingletonDelegate : ReadOnlyProperty<Context, (String) -> DataStore<Preferences>> {
private val dataStoreCache = ConcurrentHashMap<String, DataStore<Preferences>>()
private val provider: (Context) -> (String) -> DataStore<Preferences> = { context ->
{ key ->
dataStoreCache.getOrPut(key) {
PreferenceDataStoreFactory.create {
context.applicationContext.preferencesDataStoreFile(key)
}
}
}
}
override fun getValue(thisRef: Context, property: KProperty<*>) = provider.invoke(thisRef)
}

View File

@ -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)

View File

@ -76,6 +76,9 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
@EpoxyAttribute
var locationPinProvider: LocationPinProvider? = null
@EpoxyAttribute
var locationOwnerId: String? = null
@EpoxyAttribute
var movementMethod: MovementMethod? = null
@ -109,7 +112,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
.apply(RequestOptions.centerCropTransform())
.into(holder.staticMapImageView)
locationPinProvider?.create(matrixItem.id) { pinDrawable ->
locationPinProvider?.create(locationOwnerId) { pinDrawable ->
GlideApp.with(holder.staticMapPinImageView)
.load(pinDrawable)
.into(holder.staticMapPinImageView)

View File

@ -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<Intent> {
return registerForActivityResult(ActivityResultContracts.StartActivityForResult(), onResult)
@ -66,8 +67,12 @@ fun <T : Fragment> AppCompatActivity.replaceFragment(
fragmentClass: Class<T>,
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)
}
}

View File

@ -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<Preferences> by dataStoreProvider()

View File

@ -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

View File

@ -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
}

View File

@ -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<DummyState, EmptyAction, EmptyViewEvents>(initialState) {
) : VectorViewModel<VectorDummyViewState, EmptyAction, EmptyViewEvents>(initialState) {
private var checkDone: Boolean = false
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<AnalyticsAccountDataViewModel, DummyState> {
override fun create(initialState: DummyState): AnalyticsAccountDataViewModel
interface Factory : MavericksAssistedViewModelFactory<AnalyticsAccountDataViewModel, VectorDummyViewState> {
override fun create(initialState: VectorDummyViewState): AnalyticsAccountDataViewModel
}
companion object : MavericksViewModelFactory<AnalyticsAccountDataViewModel, DummyState> by hiltMavericksViewModelFactory() {
companion object : MavericksViewModelFactory<AnalyticsAccountDataViewModel, VectorDummyViewState> by hiltMavericksViewModelFactory() {
private const val ANALYTICS_EVENT_TYPE = "im.vector.analytics"
}

View File

@ -179,7 +179,7 @@ class DefaultVectorAnalytics @Inject constructor(
posthog?.identify(REUSE_EXISTING_ID, identity.getProperties().toPostHogProperties(), IGNORED_OPTIONS)
}
private fun Map<String, Any>?.toPostHogProperties(): Properties? {
private fun Map<String, Any?>?.toPostHogProperties(): Properties? {
if (this == null) return null
return Properties().apply {

View File

@ -18,5 +18,5 @@ package im.vector.app.features.analytics.itf
interface VectorAnalyticsEvent {
fun getName(): String
fun getProperties(): Map<String, Any>?
fun getProperties(): Map<String, Any?>?
}

View File

@ -55,9 +55,9 @@ data class Identity(
override fun getName() = "Identity"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) }
override fun getProperties(): Map<String, Any?>? {
return mutableMapOf<String, Any?>().apply {
put("ftueUseCaseSelection", null)
}.takeIf { it.isNotEmpty() }
}
}

View File

@ -23,4 +23,8 @@ sealed class CreateDirectRoomAction : VectorViewModelAction {
data class CreateRoomAndInviteSelectedUsers(
val selections: Set<PendingSelection>
) : CreateDirectRoomAction()
data class QrScannedAction(
val result: String
) : CreateDirectRoomAction()
}

View File

@ -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)
}

View File

@ -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<FragmentQrCodeScannerBinding>(), 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<ByteArray>) {
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)
}
}
}

View File

@ -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()
}

View File

@ -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<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> {
@ -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<PendingSelection>) {
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)
}
}

View File

@ -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)

View File

@ -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<ActiveSpaceViewState, EmptyAction, EmptyViewEvents>(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<PowerLevelsContent>()?.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<PromoteRestrictedViewModel, ActiveSpaceViewState> {
override fun create(initialState: ActiveSpaceViewState): PromoteRestrictedViewModel
}
companion object : MavericksViewModelFactory<PromoteRestrictedViewModel, ActiveSpaceViewState> by hiltMavericksViewModelFactory()
override fun handle(action: EmptyAction) {}
}

View File

@ -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<DummyState, EmptyAction, EmptyViewEvents>(initialState) {
) : VectorViewModel<VectorDummyViewState, EmptyAction, EmptyViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<UserColorAccountDataViewModel, DummyState> {
override fun create(initialState: DummyState): UserColorAccountDataViewModel
interface Factory : MavericksAssistedViewModelFactory<UserColorAccountDataViewModel, VectorDummyViewState> {
override fun create(initialState: VectorDummyViewState): UserColorAccountDataViewModel
}
companion object : MavericksViewModelFactory<UserColorAccountDataViewModel, DummyState> by hiltMavericksViewModelFactory()
companion object : MavericksViewModelFactory<UserColorAccountDataViewModel, VectorDummyViewState> by hiltMavericksViewModelFactory()
init {
observeAccountData()

View File

@ -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()

View File

@ -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)

View File

@ -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)
}
}
}
}

View File

@ -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<MessageLocationContent>(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

View File

@ -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,

View File

@ -130,7 +130,6 @@ class MessageItemFactory @Inject constructor(
private val locationPinProvider: LocationPinProvider,
private val vectorPreferences: VectorPreferences,
private val urlMapProvider: UrlMapProvider,
private val resources: Resources
) {
// TODO inject this properly?
@ -218,12 +217,14 @@ 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)
.mapWidth(width)
.mapHeight(height)
.userId(informationData.senderId)
.userId(userId)
.locationPinProvider(locationPinProvider)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)

View File

@ -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

View File

@ -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? {

View File

@ -50,7 +50,6 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
super.bind(holder)
renderSendState(holder.view, null)
val location = locationUrl ?: return
val locationOwnerId = userId ?: return
val messageLayout = attributes.informationData.messageLayout
val dimensionConverter = DimensionConverter(holder.view.resources)
val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
@ -67,7 +66,7 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
.transform(imageCornerTransformation)
.into(holder.staticMapImageView)
locationPinProvider?.create(locationOwnerId) { pinDrawable ->
locationPinProvider?.create(userId) { pinDrawable ->
GlideApp.with(holder.staticMapPinImageView)
.load(pinDrawable)
.into(holder.staticMapPinImageView)

View File

@ -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<RoomSummary> {
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
}
}
}

View File

@ -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

View File

@ -121,7 +121,7 @@ class LocationPreviewFragment @Inject constructor(
MapState(
zoomOnlyOnce = true,
pinLocationData = location,
pinId = args.locationOwnerId,
pinId = args.locationOwnerId ?: DEFAULT_PIN_ID,
pinDrawable = pinDrawable
)
)

View File

@ -30,7 +30,7 @@ data class LocationSharingArgs(
val roomId: String,
val mode: LocationSharingMode,
val initialLocationData: LocationData?,
val locationOwnerId: String
val locationOwnerId: String?
) : Parcelable
@AndroidEntryPoint

View File

@ -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"
}
}

View File

@ -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
)

View File

@ -41,7 +41,8 @@ class SpaceCardRenderer @Inject constructor(
fun render(spaceSummary: RoomSummary?,
peopleYouKnow: List<User>,
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 {

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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()
}

View File

@ -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<ActivitySimpleBinding>() {
class QrCodeScannerActivity() : VectorBaseActivity<ActivitySimpleBinding>() {
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<ByteArray>) {
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"

View File

@ -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()
}

View File

@ -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<FragmentQrCodeScannerBinding>(),
ZXingScannerView.ResultHandler {
@Parcelize
data class QrScannerArgs(
val showExtraButtons: Boolean,
@StringRes val titleRes: Int
) : Parcelable
open class QrCodeScannerFragment @Inject constructor() : VectorBaseFragment<FragmentQrCodeScannerBinding>(), 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<ByteArray>) {
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))
}
}
}

View File

@ -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<VectorDummyViewState, QrCodeScannerAction, QrCodeScannerEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<QrCodeScannerViewModel, VectorDummyViewState> {
override fun create(initialState: VectorDummyViewState): QrCodeScannerViewModel
}
companion object : MavericksViewModelFactory<QrCodeScannerViewModel, VectorDummyViewState> 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
}
)
}
}

View File

@ -151,7 +151,7 @@ class BugReportActivity : VectorBaseActivity<ActivityBugReportBinding>() {
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<ActivityBugReportBinding>() {
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()
}

View File

@ -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()) {

View File

@ -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)
}
}

View File

@ -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<Preferences> 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() }
}
}

View File

@ -186,7 +186,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"
@ -357,16 +356,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.
*

View File

@ -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<BottomSheetSpaceAdvertiseRestrictedBinding>() {
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)
}
}
}

View File

@ -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
}

View File

@ -118,7 +118,7 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
views.inviterMxid.isVisible = false
}
spaceCardRenderer.render(summary, state.peopleYouKnow.invoke().orEmpty(), null, views.spaceCard)
spaceCardRenderer.render(summary, state.peopleYouKnow.invoke().orEmpty(), null, views.spaceCard, showDescription = true)
views.spaceCard.matrixToCardMainButton.button.text = getString(R.string.action_accept)
views.spaceCard.matrixToCardSecondaryButton.button.text = getString(R.string.action_decline)

View File

@ -85,6 +85,7 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpa
is ShareSpaceViewEvents.NavigateToInviteUser -> {
val intent = InviteUsersToRoomActivity.getIntent(requireContext(), event.spaceId)
startActivity(intent)
dismissAllowingStateLoss()
}
is ShareSpaceViewEvents.ShowInviteByLink -> {
startSharePlainTextIntent(
@ -94,6 +95,7 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpa
text = getString(R.string.share_space_link_message, event.spaceName, event.permalink),
extraTitle = getString(R.string.share_space_link_message, event.spaceName, event.permalink)
)
dismissAllowingStateLoss()
}
}
}

View File

@ -1,156 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.usercode
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.ContextCompat
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.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.registerForPermissionsResult
import im.vector.app.databinding.FragmentQrCodeScannerWithButtonBinding
import im.vector.lib.multipicker.MultiPicker
import im.vector.lib.multipicker.utils.ImageUtils
import me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.extensions.tryOrNull
import javax.inject.Inject
class ScanUserCodeFragment @Inject constructor() :
VectorBaseFragment<FragmentQrCodeScannerWithButtonBinding>(),
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<ByteArray>) {
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
}
}

View File

@ -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<ActivitySimpleBinding>(),
MatrixToBottomSheet.InteractionListener {
val sharedViewModel: UserCodeSharedViewModel by viewModel()
private val qrViewModel: QrCodeScannerViewModel by viewModel()
@Parcelize
data class Args(
@ -81,10 +86,13 @@ class UserCodeActivity : VectorBaseActivity<ActivitySimpleBinding>(),
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<ActivitySimpleBinding>(),
}
}
}
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<ActivitySimpleBinding>(),
super.onDestroy()
}
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
private fun showFragment(fragmentClass: KClass<out Fragment>, 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)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="51dp"
android:height="54dp"
android:viewportWidth="51"
android:viewportHeight="54">
<path
android:pathData="M27.2956,44.2191C37.5577,42.7292 45.4403,33.8952 45.4403,23.2202C45.4403,11.5006 35.9397,2 24.2202,2C12.5006,2 3,11.5006 3,23.2202C3,33.8953 10.8827,42.7293 21.1449,44.2191L24.2202,47.1784L27.2956,44.2191Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M23.8041,15.073C20.5837,15.073 17.979,17.7486 17.979,21.0567C17.979,24.6213 21.6572,29.5365 23.1717,31.4085C23.5046,31.8188 24.112,31.8188 24.4449,31.4085C25.9511,29.5365 29.6293,24.6213 29.6293,21.0567C29.6293,17.7486 27.0246,15.073 23.8041,15.073ZM23.8041,23.1937C22.6558,23.1937 21.7237,22.2364 21.7237,21.0567C21.7237,19.8771 22.6558,18.9197 23.8041,18.9197C24.9525,18.9197 25.8846,19.8771 25.8846,21.0567C25.8846,22.2364 24.9525,23.1937 23.8041,23.1937Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -1,89 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://www.figma.com/file/HOGxCoUWoedha639SjD90n/%5BBeta%5D-Restricted-room-access?node-id=107%3A61157 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurface"
android:orientation="vertical"
android:padding="16dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="16dp"
android:layout_marginBottom="12dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_beta_pill" />
<TextView
style="@style/Widget.Vector.TextView.Title"
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="12dp"
android:gravity="start"
tools:text="@string/new_let_people_in_spaces_find_and_join"
android:textColor="?vctr_content_primary"
android:textStyle="bold" />
<TextView
style="@style/Widget.Vector.TextView.Body"
android:id="@+id/topDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:gravity="start"
android:textColor="?vctr_content_secondary"
tools:text="@string/help_people_in_spaces_find_and_join" />
<ImageView
android:id="@+id/imageHint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:maxWidth="300dp"
android:src="@drawable/room_settings"
android:visibility="gone"
tools:visibility="visible" />
<TextView
style="@style/Widget.Vector.TextView.Body"
android:id="@+id/bottomDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="start"
android:text="@string/this_makes_it_easy_for_rooms_to_stay_private_to_a_space"
android:textColor="?vctr_content_secondary"
android:visibility="gone"
tools:visibility="visible" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:gravity="end"
android:orientation="horizontal">
<Button
android:id="@+id/skipButton"
style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/action_skip"
android:textAllCaps="true" />
<Button
android:id="@+id/learnMore"
style="@style/Widget.Vector.Button.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/learn_more"
android:textAllCaps="true" />
</LinearLayout>
</LinearLayout>

View File

@ -79,7 +79,7 @@
<TextView
android:id="@+id/useCaseOptionOne"
style="@style/Widget.Vector.TextView.Subtitle"
style="@style/Widget.Vector.TextView.Subtitle.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
@ -97,7 +97,7 @@
<TextView
android:id="@+id/useCaseOptionTwo"
style="@style/Widget.Vector.TextView.Subtitle"
style="@style/Widget.Vector.TextView.Subtitle.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
@ -115,7 +115,7 @@
<TextView
android:id="@+id/useCaseOptionThree"
style="@style/Widget.Vector.TextView.Subtitle"
style="@style/Widget.Vector.TextView.Subtitle.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
@ -133,10 +133,12 @@
<TextView
android:id="@+id/useCaseSkip"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="true"
android:gravity="center"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/contentFooterSpacing"
app:layout_constraintEnd_toEndOf="@id/useCaseGutterEnd"
app:layout_constraintStart_toStartOf="@id/useCaseGutterStart"
@ -153,10 +155,12 @@
<TextView
android:id="@+id/useCaseFooter"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/ftue_auth_use_case_join_existing_server"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/useCaseConnectToServer"
app:layout_constraintEnd_toEndOf="@id/useCaseGutterEnd"
app:layout_constraintStart_toStartOf="@id/useCaseGutterStart"

View File

@ -10,7 +10,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="200dp"
android:padding="16dp"
android:padding="@dimen/layout_horizontal_margin"
android:visibility="gone"
tools:visibility="visible">
@ -19,11 +19,9 @@
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginTop="20dp"
android:importantForAccessibility="no"
android:elevation="4dp"
android:importantForAccessibility="no"
android:transitionName="profile"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@sample/room_round_avatars" />
@ -31,21 +29,23 @@
<TextView
android:id="@+id/matrixToCardNameText"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:maxLines="1"
android:singleLine="true"
android:textAlignment="textStart"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@id/matrixToCardAvatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/matrixToCardAvatar"
app:layout_constraintTop_toTopOf="@id/matrixToCardAvatar"
tools:text="@sample/rooms.json/data/name" />
<TextView
android:id="@+id/matrixToCardAliasText"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:maxLines="1"
@ -53,6 +53,8 @@
android:textAlignment="textStart"
android:textColor="?vctr_content_secondary"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/matrixToCardNameText"
app:layout_constraintTop_toBottomOf="@id/matrixToCardNameText"
app:layout_goneMarginTop="0dp"
tools:text="@sample/rooms.json/data/alias"
@ -65,7 +67,7 @@
android:importantForAccessibility="no"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/matrixToAccessText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toStartOf="@id/matrixToCardNameText"
app:layout_constraintTop_toTopOf="@id/matrixToAccessText"
app:tint="?vctr_content_secondary"
tools:ignore="MissingPrefix"
@ -75,7 +77,7 @@
<TextView
android:id="@+id/matrixToAccessText"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
@ -89,6 +91,13 @@
tools:text="Public Space"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/matrixToHeaderBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="matrixToAccessText, matrixToCardAvatar" />
<LinearLayout
android:id="@+id/matrixToMemberPills"
android:layout_width="wrap_content"
@ -105,7 +114,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/matrixToAccessText">
app:layout_constraintTop_toBottomOf="@id/matrixToHeaderBarrier">
<ImageView
android:id="@+id/spaceChildMemberCountIcon"

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -21,7 +22,7 @@
</com.google.android.material.appbar.AppBarLayout>
<me.dm7.barcodescanner.zxing.ZXingScannerView
android:id="@+id/scannerView"
android:id="@+id/qrScannerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
@ -30,29 +31,44 @@
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
<!-- TODO In the future we could add a toggle to switch the flash, and other possible settings -->
<Button
android:id="@+id/userCodeMyCodeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
android:maxWidth="160dp"
android:text="@string/user_code_my_code"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<!-- TODO add take from album option.. -->
<!-- <Button-->
<!-- android:id="@+id/openAlbumButton"-->
<!-- style="@style/Widget.MaterialComponents.Button.Icon"-->
<!-- android:layout_width="34dp"-->
<!-- android:layout_height="34dp"-->
<!-- android:layout_marginEnd="@dimen/layout_horizontal_margin"-->
<!-- android:layout_marginBottom="@dimen/layout_vertical_margin_big"-->
<!-- android:backgroundTint="?vctr_bottom_nav_background_color"-->
<!-- android:elevation="0dp"-->
<!-- android:insetLeft="0dp"-->
<!-- android:insetTop="0dp"-->
<!-- android:insetRight="0dp"-->
<!-- android:insetBottom="0dp"-->
<!-- android:padding="0dp"-->
<!-- app:cornerRadius="17dp"-->
<!-- app:icon="@drawable/ic_picture_icon"-->
<!-- app:iconGravity="textStart"-->
<!-- app:iconPadding="0dp"-->
<!-- app:iconSize="20dp"-->
<!-- app:iconTint="?colorPrimary"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintEnd_toEndOf="parent"/>-->
<Button
android:id="@+id/userCodeOpenGalleryButton"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:backgroundTint="?colorSurface"
android:elevation="0dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
android:visibility="gone"
app:cornerRadius="17dp"
app:icon="@drawable/ic_picture_icon"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconSize="20dp"
app:iconTint="?colorPrimary"
app:layout_constraintBottom_toBottomOf="@id/userCodeMyCodeButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/userCodeMyCodeButton"
app:layout_constraintTop_toTopOf="@id/userCodeMyCodeButton"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/qrScannerToolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:title="@string/user_code_scan" />
</com.google.android.material.appbar.AppBarLayout>
<me.dm7.barcodescanner.zxing.ZXingScannerView
android:id="@+id/userCodeScannerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
<Button
android:id="@+id/userCodeMyCodeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
android:maxWidth="160dp"
android:text="@string/user_code_my_code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/userCodeOpenGalleryButton"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:backgroundTint="?colorSurface"
android:elevation="0dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:cornerRadius="17dp"
app:icon="@drawable/ic_picture_icon"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconSize="20dp"
app:iconTint="?colorPrimary"
app:layout_constraintBottom_toBottomOf="@id/userCodeMyCodeButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/userCodeMyCodeButton"
app:layout_constraintTop_toTopOf="@id/userCodeMyCodeButton" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -16,15 +16,4 @@
<!-- onboarding english only word play -->
<string name="cut_the_slack_from_teams" translatable="false">Cut the slack from teams.</string>
<!-- WIP strings, will move to strings.xml when signed off -->
<string name="ftue_auth_use_case_title" translatable="false">Who will you chat to the most?</string>
<string name="ftue_auth_use_case_subtitle" translatable="false">We\'ll help you get connected.</string>
<string name="ftue_auth_use_case_option_one" translatable="false">Friends and family</string>
<string name="ftue_auth_use_case_option_two" translatable="false">Teams</string>
<string name="ftue_auth_use_case_option_three" translatable="false">Communities</string>
<string name="ftue_auth_use_case_skip" translatable="false">Not sure yet? %s</string>
<string name="ftue_auth_use_case_skip_partial" translatable="false">You can skip this question</string>
<string name="ftue_auth_use_case_join_existing_server" translatable="false">Looking to join an existing server?</string>
<string name="ftue_auth_use_case_connect_to_server" translatable="false">Connect to server</string>
</resources>

View File

@ -1064,6 +1064,7 @@
<string name="tab_title_search_messages">MESSAGES</string>
<string name="tab_title_search_people">PEOPLE</string>
<string name="tab_title_search_files">FILES</string>
<!-- TODO TO BE REMOVED -->
<string name="search_is_not_supported_in_e2e_room">Searching in encrypted rooms is not supported yet.</string>
<!-- Directory -->
@ -2555,6 +2556,17 @@
<!-- Note to translators: the translation MUST contain the string "${app_name}", which will be replaced by the application name -->
<string name="template_ftue_auth_carousel_body_workplace" translatable="false">${app_name} is also great for the workplace. Its trusted by the worlds most secure organisations.</string>
<string name="ftue_auth_use_case_title">Who will you chat to the most?</string>
<string name="ftue_auth_use_case_subtitle">We\'ll help you get connected.</string>
<string name="ftue_auth_use_case_option_one">Friends and family</string>
<string name="ftue_auth_use_case_option_two">Teams</string>
<string name="ftue_auth_use_case_option_three">Communities</string>
<!-- Note to translators: the %s is replaced by the content of ftue_auth_use_case_skip_partial -->
<string name="ftue_auth_use_case_skip">Not sure yet? You can %s</string>
<string name="ftue_auth_use_case_skip_partial">skip this question</string>
<string name="ftue_auth_use_case_join_existing_server">Looking to join an existing server?</string>
<string name="ftue_auth_use_case_connect_to_server">Connect to server</string>
<string name="login_splash_title">It\'s your conversation. Own it.</string>
<string name="login_splash_text1">Chat with people directly or in groups</string>
<string name="login_splash_text2">Keep conversations private with encryption</string>
@ -3672,10 +3684,15 @@
<string name="upgrade_room_for_restricted_note">Please note upgrading will make a new version of the room. All current messages will stay in this archived room.</string>
<!-- TODO TO BE REMOVED -->
<string name="new_let_people_in_spaces_find_and_join">New: Let people in spaces find and join private rooms</string>
<!-- TODO TO BE REMOVED -->
<string name="help_people_in_spaces_find_and_join">Help people in spaces to find and join private rooms themselves, no need to manually invite everyone.</string>
<!-- TODO TO BE REMOVED -->
<string name="this_makes_it_easy_for_rooms_to_stay_private_to_a_space">This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.</string>
<!-- TODO TO BE REMOVED -->
<string name="help_space_members">Help space members find private rooms</string>
<!-- TODO TO BE REMOVED -->
<string name="to_help_space_members_find_and_join">To help space members find and join a private room, go to that rooms settings by tapping on the avatar.</string>
<!-- %s will be replaced by an email at runtime -->

View File

@ -18,7 +18,11 @@ package im.vector.app.features.location
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldBeTrue
import org.junit.Test
import org.matrix.android.sdk.api.session.room.model.message.LocationAsset
import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
class LocationDataTest {
@Test
@ -57,4 +61,16 @@ class LocationDataTest {
parseGeo("ge o:12.34,56.78;13.56").shouldBeNull()
parseGeo("geo :12.34,56.78;13.56").shouldBeNull()
}
@Test
fun selfLocationTest() {
val contentWithNullAsset = MessageLocationContent(body = "", geoUri = "", locationAsset = null)
contentWithNullAsset.isSelfLocation().shouldBeTrue()
val contentWithNullAssetType = MessageLocationContent(body = "", geoUri = "", locationAsset = LocationAsset(type = null))
contentWithNullAssetType.isSelfLocation().shouldBeTrue()
val contentWithSelfAssetType = MessageLocationContent(body = "", geoUri = "", locationAsset = LocationAsset(type = LocationAssetType.SELF))
contentWithSelfAssetType.isSelfLocation().shouldBeTrue()
}
}