Bubbles: merge develop
This commit is contained in:
commit
2e2da16a6b
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
=======================================
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Fixes non sans-serif font weights being ignored
|
|
@ -0,0 +1 @@
|
|||
"Invite users to space" dialog now closed when user choose invite method
|
|
@ -0,0 +1 @@
|
|||
Changed layout for space card and room card used at "explore room" screen and space/room invite dialogs
|
|
@ -0,0 +1 @@
|
|||
Removed spaces restricted search hint dialogs
|
|
@ -0,0 +1 @@
|
|||
Remove Search from room options if not available
|
|
@ -0,0 +1 @@
|
|||
Qr code scanning fragments merged into one
|
|
@ -0,0 +1 @@
|
|||
Fixing missing/intermittent notifications on the google play variant when wifi is enabled
|
|
@ -0,0 +1 @@
|
|||
Fixes call statuses in the timeline for missed/rejected calls and connected calls.
|
|
@ -0,0 +1 @@
|
|||
Fix fallback permalink when threads are disabled
|
|
@ -0,0 +1 @@
|
|||
Support generic location pin
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()}\""
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ interface RelationService {
|
|||
* @param targetEventId the id of the event being reacted
|
||||
* @param reaction the reaction (preferably emoji)
|
||||
*/
|
||||
fun undoReaction(targetEventId: String,
|
||||
suspend fun undoReaction(targetEventId: String,
|
||||
reaction: String): Cancelable
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
when {
|
||||
// The event doesn't exist locally, let's assume it hasn't been read
|
||||
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)
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,40 +76,32 @@ 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) {
|
||||
|
||||
val data = findReactionEventForUndoTask.executeRetry(params, Int.MAX_VALUE)
|
||||
|
||||
return 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)
|
||||
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,
|
||||
pollType: PollType,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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") }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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?>?
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,4 +23,8 @@ sealed class CreateDirectRoomAction : VectorViewModelAction {
|
|||
data class CreateRoomAndInviteSelectedUsers(
|
||||
val selections: Set<PendingSelection>
|
||||
) : CreateDirectRoomAction()
|
||||
|
||||
data class QrScannedAction(
|
||||
val result: String
|
||||
) : CreateDirectRoomAction()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -34,7 +34,10 @@ 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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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,16 +740,24 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleUndoReact(action: RoomDetailAction.UndoReaction) {
|
||||
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 {
|
||||
viewModelScope.launch {
|
||||
tryOrNull {
|
||||
room.undoReaction(action.targetEventId, action.selectedReaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendMedia(action: RoomDetailAction.SendMedia) {
|
||||
room.sendMedias(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -121,7 +121,7 @@ class LocationPreviewFragment @Inject constructor(
|
|||
MapState(
|
||||
zoomOnlyOnce = true,
|
||||
pinLocationData = location,
|
||||
pinId = args.locationOwnerId,
|
||||
pinId = args.locationOwnerId ?: DEFAULT_PIN_ID,
|
||||
pinDrawable = pinDrawable
|
||||
)
|
||||
)
|
||||
|
|
|
@ -30,7 +30,7 @@ data class LocationSharingArgs(
|
|||
val roomId: String,
|
||||
val mode: LocationSharingMode,
|
||||
val initialLocationData: LocationData?,
|
||||
val locationOwnerId: String
|
||||
val locationOwnerId: String?
|
||||
) : Parcelable
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -19,55 +19,53 @@ 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)
|
||||
if (isFirstCreation()) {
|
||||
replaceFragment(views.simpleFragmentContainer, QrCodeScannerFragment::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
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()
|
||||
}
|
||||
|
||||
// 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
|
||||
else -> Unit
|
||||
}.exhaustive
|
||||
}
|
||||
// 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
|
||||
|
||||
if (isFirstCreation()) {
|
||||
val args = QrScannerArgs(showExtraButtons = false, R.string.verification_scan_their_code)
|
||||
replaceFragment(views.simpleFragmentContainer, QrCodeScannerFragment::class.java, args)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -30,9 +30,12 @@ import javax.inject.Inject
|
|||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter: BugReporter,
|
||||
class VectorUncaughtExceptionHandler @Inject constructor(
|
||||
context: Context,
|
||||
private val bugReporter: BugReporter,
|
||||
private val versionProvider: VersionProvider,
|
||||
private val versionCodeProvider: VersionCodeProvider) : Thread.UncaughtExceptionHandler {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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. It’s trusted by the world’s 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 room’s settings by tapping on the avatar.</string>
|
||||
|
||||
<!-- %s will be replaced by an email at runtime -->
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue