diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml index 4ecc824424..3bb5ab73aa 100644 --- a/.github/workflows/triage-incoming.yml +++ b/.github/workflows/triage-incoming.yml @@ -2,11 +2,13 @@ name: Move new issues onto Issue triage board on: issues: - types: [opened] + types: [ opened ] jobs: automate-project-columns: runs-on: ubuntu-latest + if: | + github.repository == 'vector-im/element-android' # Skip in forks steps: - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 with: diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index 67c4e9dbab..96d302ceea 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -2,12 +2,14 @@ name: Move labelled issues to correct boards and columns on: issues: - types: [labeled] - + types: [ labeled ] + jobs: move_needs_info_issues: name: X-Needs-Info issues to Need info column on triage board runs-on: ubuntu-latest + if: | + github.repository == 'vector-im/element-android' # Skip in forks steps: - uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338 with: @@ -19,15 +21,16 @@ jobs: add_priority_design_issues_to_project: name: P1 X-Needs-Design to Design project board runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'X-Needs-Design') && - (contains(github.event.issue.labels.*.name, 'S-Critical') && - (contains(github.event.issue.labels.*.name, 'O-Frequent') || + if: | + github.repository == 'vector-im/element-android' && # Skip in forks + contains(github.event.issue.labels.*.name, 'X-Needs-Design') && + (contains(github.event.issue.labels.*.name, 'S-Critical') && + (contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'O-Occasional')) || - contains(github.event.issue.labels.*.name, 'S-Major') && - contains(github.event.issue.labels.*.name, 'O-Frequent') || - contains(github.event.issue.labels.*.name, 'A11y') && - contains(github.event.issue.labels.*.name, 'O-Frequent')) + contains(github.event.issue.labels.*.name, 'S-Major') && + contains(github.event.issue.labels.*.name, 'O-Frequent') || + contains(github.event.issue.labels.*.name, 'A11y') && + contains(github.event.issue.labels.*.name, 'O-Frequent')) steps: - uses: octokit/graphql-action@v2.x id: add_to_project @@ -47,36 +50,38 @@ jobs: PROJECT_ID: "PN_kwDOAM0swc0sUA" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} -# delight_issues_to_board: -# name: Spaces issues to new Delight project board -# runs-on: ubuntu-latest -# if: > -# contains(github.event.issue.labels.*.name, 'A-Spaces') || -# contains(github.event.issue.labels.*.name, 'A-Space-Settings') || -# contains(github.event.issue.labels.*.name, 'A-Subspaces') -# steps: -# - uses: octokit/graphql-action@v2.x -# with: -# headers: '{"GraphQL-Features": "projects_next_graphql"}' -# query: | -# mutation add_to_project($projectid:ID!,$contentid:ID!) { -# addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { -# projectNextItem { -# id -# } -# } -# } -# projectid: ${{ env.PROJECT_ID }} -# contentid: ${{ github.event.issue.node_id }} -# env: -# PROJECT_ID: "PN_kwDOAM0swc1HvQ" -# GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + # delight_issues_to_board: + # name: Spaces issues to new Delight project board + # runs-on: ubuntu-latest + # if: | + # github.repository == 'vector-im/element-android' && # Skip in forks + # contains(github.event.issue.labels.*.name, 'A-Spaces') || + # contains(github.event.issue.labels.*.name, 'A-Space-Settings') || + # contains(github.event.issue.labels.*.name, 'A-Subspaces') + # steps: + # - uses: octokit/graphql-action@v2.x + # with: + # headers: '{"GraphQL-Features": "projects_next_graphql"}' + # query: | + # mutation add_to_project($projectid:ID!,$contentid:ID!) { + # addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + # projectNextItem { + # id + # } + # } + # } + # projectid: ${{ env.PROJECT_ID }} + # contentid: ${{ github.event.issue.node_id }} + # env: + # PROJECT_ID: "PN_kwDOAM0swc1HvQ" + # GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_voice-message_issues: name: A-Voice Messages to voice message board runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'A-Voice Messages') + if: | + github.repository == 'vector-im/element-android' && # Skip in forks + contains(github.event.issue.labels.*.name, 'A-Voice Messages') steps: - uses: octokit/graphql-action@v2.x with: @@ -98,8 +103,9 @@ jobs: move_threads_issues: name: A-Threads to Thread board runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'A-Threads') + if: | + github.repository == 'vector-im/element-android' && # Skip in forks + contains(github.event.issue.labels.*.name, 'A-Threads') steps: - uses: octokit/graphql-action@v2.x with: @@ -121,8 +127,9 @@ jobs: move_message_bubbles_issues: name: A-Message-Bubbles to Message bubbles board runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') + if: | + github.repository == 'vector-im/element-android' && # Skip in forks + contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') steps: - uses: octokit/graphql-action@v2.x with: diff --git a/.github/workflows/triage-move-unlabelled.yml b/.github/workflows/triage-move-unlabelled.yml index 94bd049b91..5f13165939 100644 --- a/.github/workflows/triage-move-unlabelled.yml +++ b/.github/workflows/triage-move-unlabelled.yml @@ -2,15 +2,15 @@ name: Move unlabelled from needs info columns to triaged on: issues: - types: [unlabeled] - + types: [ unlabeled ] + jobs: Move_Unabeled_Issue_On_Project_Board: name: Move no longer X-Needs-Info issues to Triaged runs-on: ubuntu-latest - if: > - ${{ - !contains(github.event.issue.labels.*.name, 'X-Needs-Info') }} + if: | + github.repository == 'vector-im/element-android' && # Skip in forks + !contains(github.event.issue.labels.*.name, 'X-Needs-Info') env: BOARD_NAME: "Issue triage" OWNER: ${{ github.repository_owner }} diff --git a/.github/workflows/triage-priority-bugs.yml b/.github/workflows/triage-priority-bugs.yml index 976879a3ae..7564387a1c 100644 --- a/.github/workflows/triage-priority-bugs.yml +++ b/.github/workflows/triage-priority-bugs.yml @@ -2,28 +2,29 @@ name: Move P1 bugs to boards on: issues: - types: [labeled, unlabeled] + types: [ labeled, unlabeled ] jobs: p1_issues_to_team_workboard: runs-on: ubuntu-latest - if: > - (!contains(github.event.issue.labels.*.name, 'A-E2EE') && - !contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') && - !contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') && - !contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') && - !contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') && - !contains(github.event.issue.labels.*.name, 'A-Spaces') && - !contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') && - !contains(github.event.issue.labels.*.name, 'A-Subspaces')) && - (contains(github.event.issue.labels.*.name, 'T-Defect') && - contains(github.event.issue.labels.*.name, 'S-Critical') && - (contains(github.event.issue.labels.*.name, 'O-Frequent') || + if: | + github.repository == 'vector-im/element-android' && # Skip in forks + (!contains(github.event.issue.labels.*.name, 'A-E2EE') && + !contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') && + !contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') && + !contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') && + !contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') && + !contains(github.event.issue.labels.*.name, 'A-Spaces') && + !contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') && + !contains(github.event.issue.labels.*.name, 'A-Subspaces')) && + (contains(github.event.issue.labels.*.name, 'T-Defect') && + contains(github.event.issue.labels.*.name, 'S-Critical') && + (contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'O-Occasional')) || - contains(github.event.issue.labels.*.name, 'S-Major') && - contains(github.event.issue.labels.*.name, 'O-Frequent') || - contains(github.event.issue.labels.*.name, 'A11y') && - contains(github.event.issue.labels.*.name, 'O-Frequent')) + contains(github.event.issue.labels.*.name, 'S-Major') && + contains(github.event.issue.labels.*.name, 'O-Frequent') || + contains(github.event.issue.labels.*.name, 'A11y') && + contains(github.event.issue.labels.*.name, 'O-Frequent')) steps: - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 with: @@ -33,20 +34,21 @@ jobs: P1_issues_to_crypto_team_workboard: runs-on: ubuntu-latest - if: > - (contains(github.event.issue.labels.*.name, 'A-E2EE') || - contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') || - contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || - contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') || - contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) && - (contains(github.event.issue.labels.*.name, 'T-Defect') && - contains(github.event.issue.labels.*.name, 'S-Critical') && - (contains(github.event.issue.labels.*.name, 'O-Frequent') || + if: | + github.repository == 'vector-im/element-android' && # Skip in forks + (contains(github.event.issue.labels.*.name, 'A-E2EE') || + contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') || + contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || + contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') || + contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) && + (contains(github.event.issue.labels.*.name, 'T-Defect') && + contains(github.event.issue.labels.*.name, 'S-Critical') && + (contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'O-Occasional')) || - contains(github.event.issue.labels.*.name, 'S-Major') && - contains(github.event.issue.labels.*.name, 'O-Frequent') || - contains(github.event.issue.labels.*.name, 'A11y') && - contains(github.event.issue.labels.*.name, 'O-Frequent')) + contains(github.event.issue.labels.*.name, 'S-Major') && + contains(github.event.issue.labels.*.name, 'O-Frequent') || + contains(github.event.issue.labels.*.name, 'A11y') && + contains(github.event.issue.labels.*.name, 'O-Frequent')) steps: - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 with: diff --git a/build.gradle b/build.gradle index f057d234e5..9d83268a56 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ buildscript { // ktlint Plugin plugins { - id "org.jlleitschuh.gradle.ktlint" version "10.2.0" + id "org.jlleitschuh.gradle.ktlint" version "10.2.1" } allprojects { diff --git a/changelog.d/3444.bugfix b/changelog.d/3444.bugfix new file mode 100644 index 0000000000..bf397da5b7 --- /dev/null +++ b/changelog.d/3444.bugfix @@ -0,0 +1 @@ +Attachment picker UI improvements \ No newline at end of file diff --git a/changelog.d/4612.misc b/changelog.d/4612.misc new file mode 100644 index 0000000000..43b5007b7e --- /dev/null +++ b/changelog.d/4612.misc @@ -0,0 +1 @@ +Workaround to fetch all the pending toDevice events from a Synapse homeserver \ No newline at end of file diff --git a/changelog.d/4747.misc b/changelog.d/4747.misc new file mode 100644 index 0000000000..37a960671c --- /dev/null +++ b/changelog.d/4747.misc @@ -0,0 +1 @@ +Cleaning rendering of state events in timeline \ No newline at end of file diff --git a/changelog.d/4756.bugfix b/changelog.d/4756.bugfix new file mode 100644 index 0000000000..8e0c373557 --- /dev/null +++ b/changelog.d/4756.bugfix @@ -0,0 +1 @@ +Fixes newer emojis rendering strangely when inserting from the system keyboard \ No newline at end of file diff --git a/changelog.d/4767.bugfix b/changelog.d/4767.bugfix new file mode 100644 index 0000000000..172e9d80ca --- /dev/null +++ b/changelog.d/4767.bugfix @@ -0,0 +1 @@ +Fixing unable to change change avatar in some scenarios \ No newline at end of file diff --git a/changelog.d/4804.bugfix b/changelog.d/4804.bugfix new file mode 100644 index 0000000000..8f845662ab --- /dev/null +++ b/changelog.d/4804.bugfix @@ -0,0 +1 @@ +Fixing encrypted non message events showing up as notification messages (eg when a participant joins, mutes or leaves a voice call) \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fa58fc5aae..ee6ba9a3ac 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=dd54e87b4d7aa8ff3c6afb0f7805aa121d4b70bca55b8c9b1b896eb103184582 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-all.zip +distributionSha256Sum=c9490e938b221daf0094982288e4038deed954a3f12fb54cbf270ddf4e37d879 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 8e86eab51a..07852c72aa 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -46,4 +46,9 @@ 24dp 48dp 48dp + + + 56dp + 52dp + 1dp \ No newline at end of file diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt index 2a0abd3d24..669e27edfd 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt @@ -152,6 +152,13 @@ class FlowSession(private val session: Session) { } } + fun liveUserAccountData(type: String): Flow> { + return session.accountDataService().getLiveUserAccountDataEvent(type).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.accountDataService().getUserAccountDataEvent(type).toOptional() + } + } + fun liveRoomAccountData(types: Set): Flow> { return session.accountDataService().getLiveRoomAccountDataEvents(types).asFlow() .startWith(session.coroutineDispatchers.io) { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 49620676b3..3fb6b81505 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -160,7 +160,7 @@ dependencies { implementation libs.apache.commonsImaging // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.39' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40' testImplementation libs.tests.junit testImplementation 'org.robolectric:robolectric:4.7.3' diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 7d9c351410..9dd369f426 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -429,7 +429,17 @@ internal class DefaultCryptoService @Inject constructor( val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0 oneTimeKeysUploader.updateOneTimeKeyCount(currentCount) } - if (isStarted()) { + // There is a limit of to_device events returned per sync. + // If we are in a case of such limited to_device sync we can't try to generate/upload + // new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate + // the old otk too early. In this case we want to wait for the pending to_device before doing anything + // As per spec: + // If there is a large queue of send-to-device messages, the server should limit the number sent in each /sync response. + // 100 messages is recommended as a reasonable limit. + // The limit is not part of the spec, so it's probably safer to handle that when there are no more to_device ( so we are sure + // that there are no pending to_device + val toDevices = syncResponse.toDevice?.events.orEmpty() + if (isStarted() && toDevices.isEmpty()) { // Make sure we process to-device messages before generating new one-time-keys #2782 deviceListManager.refreshOutdatedDeviceLists() // The presence of device_unused_fallback_key_types indicates that the server supports fallback keys. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index 1b0ccbb489..b988f2253c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -109,18 +109,23 @@ internal class FileUploader @Inject constructor( filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val inputStream = withContext(Dispatchers.IO) { - context.contentResolver.openInputStream(uri) - } ?: throw FileNotFoundException() - val workingFile = temporaryFileCreator.create() - workingFile.outputStream().use { - inputStream.copyTo(it) - } + val workingFile = context.copyUriToTempFile(uri) return uploadFile(workingFile, filename, mimeType, progressListener).also { tryOrNull { workingFile.delete() } } } + private suspend fun Context.copyUriToTempFile(uri: Uri): File { + return withContext(Dispatchers.IO) { + val inputStream = contentResolver.openInputStream(uri) ?: throw FileNotFoundException() + val workingFile = temporaryFileCreator.create() + workingFile.outputStream().use { + inputStream.copyTo(it) + } + workingFile + } + } + private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index a19832c523..caf4158657 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -68,7 +68,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto } override suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) { - withContext(coroutineDispatchers.main) { + withContext(coroutineDispatchers.io) { val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg) setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) userStore.updateAvatar(userId, response.contentUri) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index 3faa0c9488..b6ea7a68f7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.logger.LoggerTag @@ -71,6 +72,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private var isStarted = false private var isTokenValid = true private var retryNoNetworkTask: TimerTask? = null + private var previousSyncResponseHasToDevice = false private val activeCallListObserver = Observer> { activeCalls -> if (activeCalls.isEmpty() && backgroundDetectionObserver.isInBackground) { @@ -171,12 +173,15 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (state !is SyncState.Running) { updateStateTo(SyncState.Running(afterPause = true)) } - // No timeout after a pause - val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } + val timeout = when { + previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */ + state.let { it is SyncState.Running && it.afterPause } -> 0L /* No timeout after a pause */ + else -> DEFAULT_LONG_POOL_TIMEOUT + } Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout") val params = SyncTask.Params(timeout, SyncPresence.Online) val sync = syncScope.launch { - doSync(params) + previousSyncResponseHasToDevice = doSync(params) } runBlocking { sync.join() @@ -203,10 +208,14 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } } - private suspend fun doSync(params: SyncTask.Params) { - try { + /** + * Will return true if the sync response contains some toDevice events. + */ + private suspend fun doSync(params: SyncTask.Params): Boolean { + return try { val syncResponse = syncTask.execute(params) _syncFlow.emit(syncResponse) + syncResponse.toDevice?.events?.isNotEmpty().orFalse() } catch (failure: Throwable) { if (failure is Failure.NetworkConnection) { canReachServer = false @@ -229,6 +238,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, delay(RETRY_WAIT_TIME_MS) } } + false } finally { state.let { if (it is SyncState.Running && it.afterPause) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt index 763cd55714..2f1241f4d8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -20,6 +20,7 @@ import androidx.work.BackoffPolicy import androidx.work.ExistingWorkPolicy import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.di.WorkManagerProvider @@ -34,8 +35,8 @@ import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject -private const val DEFAULT_LONG_POOL_TIMEOUT = 6L -private const val DEFAULT_DELAY_TIMEOUT = 30_000L +private const val DEFAULT_LONG_POOL_TIMEOUT_SECONDS = 6L +private const val DEFAULT_DELAY_MILLIS = 30_000L /** * Possible previous worker: None @@ -47,9 +48,12 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT, - val delay: Long = DEFAULT_DELAY_TIMEOUT, + // In seconds + val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT_SECONDS, + // In milliseconds + val delay: Long = DEFAULT_DELAY_MILLIS, val periodic: Boolean = false, + val forceImmediate: Boolean = false, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -65,13 +69,26 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, Timber.i("Sync work starting") return runCatching { - doSync(params.timeout) + doSync(if (params.forceImmediate) 0 else params.timeout) }.fold( - { + { hasToDeviceEvents -> Result.success().also { if (params.periodic) { - // we want to schedule another one after delay - automaticallyBackgroundSync(workManagerProvider, params.sessionId, params.timeout, params.delay) + // we want to schedule another one after a delay, or immediately if hasToDeviceEvents + automaticallyBackgroundSync( + workManagerProvider = workManagerProvider, + sessionId = params.sessionId, + serverTimeoutInSeconds = params.timeout, + delayInSeconds = params.delay, + forceImmediate = hasToDeviceEvents + ) + } else if (hasToDeviceEvents) { + // Previous response has toDevice events, request an immediate sync request + requireBackgroundSync( + workManagerProvider = workManagerProvider, + sessionId = params.sessionId, + serverTimeoutInSeconds = 0 + ) } } }, @@ -92,16 +109,29 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) } - private suspend fun doSync(timeout: Long) { + /** + * Will return true if the sync response contains some toDevice events. + */ + private suspend fun doSync(timeout: Long): Boolean { val taskParams = SyncTask.Params(timeout * 1000, SyncPresence.Offline) - syncTask.execute(taskParams) + val syncResponse = syncTask.execute(taskParams) + return syncResponse.toDevice?.events?.isNotEmpty().orFalse() } companion object { private const val BG_SYNC_WORK_NAME = "BG_SYNCP" - fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) { - val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, 0L, false)) + fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, + sessionId: String, + serverTimeoutInSeconds: Long = 0) { + val data = WorkerParamsFactory.toData( + Params( + sessionId = sessionId, + timeout = serverTimeoutInSeconds, + delay = 0L, + periodic = false + ) + ) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) @@ -111,13 +141,24 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) } - fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delayInSeconds: Long = 30) { - val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, delayInSeconds, true)) + fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, + sessionId: String, + serverTimeoutInSeconds: Long = 0, + delayInSeconds: Long = 30, + forceImmediate: Boolean = false) { + val data = WorkerParamsFactory.toData( + Params( + sessionId = sessionId, + timeout = serverTimeoutInSeconds, + delay = delayInSeconds, + forceImmediate = forceImmediate + ) + ) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .setInitialDelay(delayInSeconds, TimeUnit.SECONDS) + .setInitialDelay(if (forceImmediate) 0 else delayInSeconds, TimeUnit.SECONDS) .build() // Avoid risking multiple chains of syncs by replacing the existing chain workManagerProvider.workManager diff --git a/vector/build.gradle b/vector/build.gradle index 18015fdf3b..4a26717782 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -140,7 +140,7 @@ android { buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\"" resValue "string", "build_number", "\"${buildNumber}\"" - buildConfigField "im.vector.app.features.VectorFeatures.LoginVersion", "LOGIN_VERSION", "im.vector.app.features.VectorFeatures.LoginVersion.V1" + buildConfigField "im.vector.app.features.VectorFeatures.LoginVariant", "LOGIN_VARIANT", "im.vector.app.features.VectorFeatures.LoginVariant.LEGACY" buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" @@ -362,7 +362,7 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.39' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40' // FlowBinding implementation libs.github.flowBinding diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 8d22fc599f..ca5d26aaeb 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -28,8 +28,8 @@ class DebugFeaturesStateFactory @Inject constructor( return FeaturesState(listOf( createEnumFeature( label = "Login version", - selection = debugFeatures.loginVersion(), - default = defaultFeatures.loginVersion() + selection = debugFeatures.loginVariant(), + default = defaultFeatures.loginVariant() ) )) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 0831609e4f..638509e76b 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -38,8 +38,8 @@ class DebugVectorFeatures( private val dataStore = context.dataStore - override fun loginVersion(): VectorFeatures.LoginVersion { - return readPreferences().getEnum() ?: vectorFeatures.loginVersion() + override fun loginVariant(): VectorFeatures.LoginVariant { + return readPreferences().getEnum() ?: vectorFeatures.loginVariant() } fun > hasEnumOverride(type: KClass) = readPreferences().containsEnum(type) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 91fb8bee3b..667c9e4fa8 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -137,7 +137,7 @@ android:windowSoftInputMode="adjustResize" /> diff --git a/vector/src/main/java/im/vector/app/core/extensions/MavericksViewModel.kt b/vector/src/main/java/im/vector/app/core/extensions/MavericksViewModel.kt new file mode 100644 index 0000000000..6120a84d7c --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/MavericksViewModel.kt @@ -0,0 +1,35 @@ +/* + * 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.core.extensions + +import androidx.activity.ComponentActivity +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Mavericks +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelProvider + +inline fun , reified S : MavericksState> ComponentActivity.lazyViewModel(): Lazy { + return lazy(mode = LazyThreadSafetyMode.NONE) { + MavericksViewModelProvider.get( + viewModelClass = VM::class.java, + stateClass = S::class.java, + viewModelContext = ActivityViewModelContext(this, intent.extras?.get(Mavericks.KEY_ARG)), + key = VM::class.java.name + ) + } +} diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 57a3f53373..21419d55cf 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -105,7 +105,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver protected val viewModelProvider get() = ViewModelProvider(this, viewModelFactory) - protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { + fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { viewEvents .stream() .onEach { diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index e106f7f75f..58594be293 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -20,11 +20,12 @@ import im.vector.app.BuildConfig interface VectorFeatures { - fun loginVersion(): LoginVersion + fun loginVariant(): LoginVariant - enum class LoginVersion { - V1, - V2 + enum class LoginVariant { + LEGACY, + FTUE, + FTUE_WIP } enum class NotificationSettingsVersion { @@ -34,5 +35,5 @@ interface VectorFeatures { } class DefaultVectorFeatures : VectorFeatures { - override fun loginVersion(): VectorFeatures.LoginVersion = BuildConfig.LOGIN_VERSION + override fun loginVariant(): VectorFeatures.LoginVariant = BuildConfig.LOGIN_VARIANT } diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index ccc07ef118..c56b3ac832 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -26,24 +26,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewAnimationUtils import android.view.animation.Animation -import android.view.animation.AnimationSet -import android.view.animation.OvershootInterpolator -import android.view.animation.ScaleAnimation import android.view.animation.TranslateAnimation import android.widget.ImageButton import android.widget.LinearLayout import android.widget.PopupWindow import androidx.core.view.doOnNextLayout import androidx.core.view.isVisible -import com.amulyakhare.textdrawable.TextDrawable -import com.amulyakhare.textdrawable.util.ColorGenerator import im.vector.app.R -import im.vector.app.core.extensions.getMeasurements +import im.vector.app.core.epoxy.onClick import im.vector.app.core.utils.PERMISSIONS_EMPTY import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding -import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback import kotlin.math.max private const val ANIMATION_DURATION = 250 @@ -52,17 +46,16 @@ private const val ANIMATION_DURATION = 250 * This class is the view presenting choices for picking attachments. * It will return result through [Callback]. */ + class AttachmentTypeSelectorView(context: Context, inflater: LayoutInflater, - var callback: Callback?) : - PopupWindow(context) { + var callback: Callback? +) : PopupWindow(context) { interface Callback { fun onTypeSelected(type: Type) } - private val iconColorGenerator = ColorGenerator.MATERIAL - private val views: ViewAttachmentTypeSelectorBinding private var anchor: View? = null @@ -85,35 +78,40 @@ class AttachmentTypeSelectorView(context: Context, inputMethodMode = INPUT_METHOD_NOT_NEEDED isFocusable = true isTouchable = true + + views.attachmentCloseButton.onClick { + dismiss() + } } - fun show(anchor: View, isKeyboardOpen: Boolean) { + private fun animateOpen() { + views.attachmentCloseButton.animate() + .setDuration(200) + .rotation(135f) + } + + private fun animateClose() { + views.attachmentCloseButton.animate() + .setDuration(200) + .rotation(0f) + } + + fun show(anchor: View) { + animateOpen() + this.anchor = anchor val anchorCoordinates = IntArray(2) anchor.getLocationOnScreen(anchorCoordinates) - if (isKeyboardOpen) { - showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height) - } else { - val contentViewHeight = if (contentView.height == 0) { - contentView.getMeasurements().second - } else { - contentView.height - } - showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight) - } + showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1]) + contentView.doOnNextLayout { animateWindowInCircular(anchor, contentView) } - animateButtonIn(views.attachmentGalleryButton, ANIMATION_DURATION / 2) - animateButtonIn(views.attachmentCameraButton, ANIMATION_DURATION / 4) - animateButtonIn(views.attachmentFileButton, ANIMATION_DURATION / 2) - animateButtonIn(views.attachmentAudioButton, 0) - animateButtonIn(views.attachmentContactButton, ANIMATION_DURATION / 4) - animateButtonIn(views.attachmentStickersButton, ANIMATION_DURATION / 2) - animateButtonIn(views.attachmentPollButton, ANIMATION_DURATION / 4) } override fun dismiss() { + animateClose() + val capturedAnchor = anchor if (capturedAnchor != null) { animateWindowOutCircular(capturedAnchor, contentView) @@ -124,28 +122,18 @@ class AttachmentTypeSelectorView(context: Context, fun setAttachmentVisibility(type: Type, isVisible: Boolean) { when (type) { - Type.CAMERA -> views.attachmentCameraButtonContainer - Type.GALLERY -> views.attachmentGalleryButtonContainer - Type.FILE -> views.attachmentFileButtonContainer - Type.STICKER -> views.attachmentStickersButtonContainer - Type.AUDIO -> views.attachmentAudioButtonContainer - Type.CONTACT -> views.attachmentContactButtonContainer - Type.POLL -> views.attachmentPollButtonContainer + Type.CAMERA -> views.attachmentCameraButton + Type.GALLERY -> views.attachmentGalleryButton + Type.FILE -> views.attachmentFileButton + Type.STICKER -> views.attachmentStickersButton + Type.AUDIO -> views.attachmentAudioButton + Type.CONTACT -> views.attachmentContactButton + Type.POLL -> views.attachmentPollButton }.let { it.isVisible = isVisible } } - private fun animateButtonIn(button: View, delay: Int) { - val animation = AnimationSet(true) - val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f) - animation.addAnimation(scale) - animation.interpolator = OvershootInterpolator(1f) - animation.duration = ANIMATION_DURATION.toLong() - animation.startOffset = delay.toLong() - button.startAnimation(animation) - } - private fun animateWindowInCircular(anchor: View, contentView: View) { val coordinates = getClickCoordinates(anchor, contentView) val animator = ViewAnimationUtils.createCircularReveal(contentView, @@ -157,12 +145,6 @@ class AttachmentTypeSelectorView(context: Context, animator.start() } - private fun animateWindowInTranslate(contentView: View) { - val animation = TranslateAnimation(0f, 0f, contentView.height.toFloat(), 0f) - animation.duration = ANIMATION_DURATION.toLong() - getContentView().startAnimation(animation) - } - private fun animateWindowOutCircular(anchor: View, contentView: View) { val coordinates = getClickCoordinates(anchor, contentView) val animator = ViewAnimationUtils.createCircularReveal(getContentView(), @@ -207,7 +189,6 @@ class AttachmentTypeSelectorView(context: Context, } private fun ImageButton.configure(type: Type): ImageButton { - this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal)) this.setOnClickListener(TypeClickListener(type)) return this } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index 3221a5bf66..f73799d0e9 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -168,11 +168,8 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { } } - private fun renderCreationSuccess(roomId: String?) { - // Navigate to freshly created room - if (roomId != null) { - navigator.openRoom(this, roomId) - } + private fun renderCreationSuccess(roomId: String) { + navigator.openRoom(this, roomId) finish() } diff --git a/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt b/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt new file mode 100644 index 0000000000..98b1f98df0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt @@ -0,0 +1,367 @@ +/* + * 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.ftue + +import android.content.Intent +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R +import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.extensions.addFragmentToBackstack +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivityLoginBinding +import im.vector.app.features.home.HomeActivity +import im.vector.app.features.login.LoginAction +import im.vector.app.features.login.LoginCaptchaFragment +import im.vector.app.features.login.LoginCaptchaFragmentArgument +import im.vector.app.features.login.LoginConfig +import im.vector.app.features.login.LoginFragment +import im.vector.app.features.login.LoginGenericTextInputFormFragment +import im.vector.app.features.login.LoginGenericTextInputFormFragmentArgument +import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.LoginResetPasswordFragment +import im.vector.app.features.login.LoginResetPasswordMailConfirmationFragment +import im.vector.app.features.login.LoginResetPasswordSuccessFragment +import im.vector.app.features.login.LoginServerSelectionFragment +import im.vector.app.features.login.LoginServerUrlFormFragment +import im.vector.app.features.login.LoginSignUpSignInSelectionFragment +import im.vector.app.features.login.LoginSplashFragment +import im.vector.app.features.login.LoginViewEvents +import im.vector.app.features.login.LoginViewModel +import im.vector.app.features.login.LoginViewState +import im.vector.app.features.login.LoginWaitForEmailFragment +import im.vector.app.features.login.LoginWaitForEmailFragmentArgument +import im.vector.app.features.login.LoginWebFragment +import im.vector.app.features.login.ServerType +import im.vector.app.features.login.SignMode +import im.vector.app.features.login.TextInputFormFragmentMode +import im.vector.app.features.login.isSupported +import im.vector.app.features.login.terms.LoginTermsFragment +import im.vector.app.features.login.terms.LoginTermsFragmentArgument +import im.vector.app.features.login.terms.toLocalizedLoginTerms +import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.Stage +import org.matrix.android.sdk.api.extensions.tryOrNull + +private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" +private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" + +class DefaultFTUEVariant( + private val views: ActivityLoginBinding, + private val loginViewModel: LoginViewModel, + private val activity: VectorBaseActivity, + private val supportFragmentManager: FragmentManager +) : FTUEVariant { + + private val enterAnim = R.anim.enter_fade_in + private val exitAnim = R.anim.exit_fade_out + + private val popEnterAnim = R.anim.no_anim + private val popExitAnim = R.anim.exit_fade_out + + private val topFragment: Fragment? + get() = supportFragmentManager.findFragmentById(views.loginFragmentContainer.id) + + private val commonOption: (FragmentTransaction) -> Unit = { ft -> + // Find the loginLogo on the current Fragment, this should not return null + (topFragment?.view as? ViewGroup) + // Find findViewById does not work, I do not know why + // findViewById(R.id.loginLogo) + ?.children + ?.firstOrNull { it.id == R.id.loginLogo } + ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + } + + override fun initUiAndData(isFirstCreation: Boolean) { + if (isFirstCreation) { + addFirstFragment() + } + + with(activity) { + loginViewModel.onEach { + updateWithState(it) + } + loginViewModel.observeViewEvents { handleLoginViewEvents(it) } + } + + // Get config extra + val loginConfig = activity.intent.getParcelableExtra(FTUEActivity.EXTRA_CONFIG) + if (isFirstCreation) { + loginViewModel.handle(LoginAction.InitWith(loginConfig)) + } + } + + override fun setIsLoading(isLoading: Boolean) { + // do nothing + } + + private fun addFirstFragment() { + activity.addFragment(views.loginFragmentContainer, LoginSplashFragment::class.java) + } + + private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { + when (loginViewEvents) { + is LoginViewEvents.RegistrationFlowResult -> { + // Check that all flows are supported by the application + if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { + // Display a popup to propose use web fallback + onRegistrationStageNotSupported() + } else { + if (loginViewEvents.isRegistrationStarted) { + // Go on with registration flow + handleRegistrationNavigation(loginViewEvents.flowResult) + } else { + // First ask for login and password + // I add a tag to indicate that this fragment is a registration stage. + // This way it will be automatically popped in when starting the next registration stage + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption + ) + } + } + } + is LoginViewEvents.OutdatedHomeserver -> { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.login_error_outdated_homeserver_title) + .setMessage(R.string.login_error_outdated_homeserver_warning_content) + .setPositiveButton(R.string.ok, null) + .show() + Unit + } + is LoginViewEvents.OpenServerSelection -> + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginServerSelectionFragment::class.java, + option = { ft -> + activity.findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // Disable transition of text + // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // No transition here now actually + // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // TODO Disabled because it provokes a flickering + // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + }) + is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone(loginViewEvents) + is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents) + is LoginViewEvents.OnLoginFlowRetrieved -> + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginSignUpSignInSelectionFragment::class.java, + option = commonOption) + is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) + is LoginViewEvents.OnForgetPasswordClicked -> + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginResetPasswordFragment::class.java, + option = commonOption) + is LoginViewEvents.OnResetPasswordSendThreePidDone -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginResetPasswordMailConfirmationFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginResetPasswordSuccessFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { + // Go back to the login fragment + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + } + is LoginViewEvents.OnSendEmailSuccess -> { + // Pop the enter email Fragment + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginWaitForEmailFragment::class.java, + LoginWaitForEmailFragmentArgument(loginViewEvents.email), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + } + is LoginViewEvents.OnSendMsisdnSuccess -> { + // Pop the enter Msisdn Fragment + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + } + is LoginViewEvents.Failure, + is LoginViewEvents.Loading -> + // This is handled by the Fragments + Unit + }.exhaustive + } + + private fun updateWithState(loginViewState: LoginViewState) { + if (loginViewState.isUserLogged()) { + val intent = HomeActivity.newIntent( + activity, + accountCreation = loginViewState.signMode == SignMode.SignUp + ) + activity.startActivity(intent) + activity.finish() + return + } + + // Loading + views.loginLoading.isVisible = loginViewState.isLoading() + } + + private fun onWebLoginError(onWebLoginError: LoginViewEvents.OnWebLoginError) { + // Pop the backstack + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + + // And inform the user + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.dialog_title_error) + .setMessage(activity.getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode)) + .setPositiveButton(R.string.ok, null) + .show() + } + + private fun onServerSelectionDone(loginViewEvents: LoginViewEvents.OnServerSelectionDone) { + when (loginViewEvents.serverType) { + ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow + ServerType.EMS, + ServerType.Other -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginServerUrlFormFragment::class.java, + option = commonOption) + ServerType.Unknown -> Unit /* Should not happen */ + } + } + + private fun onSignModeSelected(loginViewEvents: LoginViewEvents.OnSignModeSelected) = withState(loginViewModel) { state -> + // state.signMode could not be ready yet. So use value from the ViewEvent + when (loginViewEvents.signMode) { + SignMode.Unknown -> error("Sign mode has to be set before calling this method") + SignMode.SignUp -> { + // This is managed by the LoginViewEvents + } + SignMode.SignIn -> { + // It depends on the LoginMode + when (state.loginMode) { + LoginMode.Unknown, + is LoginMode.Sso -> error("Developer error") + is LoginMode.SsoAndPassword, + LoginMode.Password -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption) + LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) + }.exhaustive + } + SignMode.SignInWithMatrixId -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption) + }.exhaustive + } + + /** + * Handle the SSO redirection here + */ + override fun onNewIntent(intent: Intent?) { + intent?.data + ?.let { tryOrNull { it.getQueryParameter("loginToken") } } + ?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) } + } + + private fun onRegistrationStageNotSupported() { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.app_name) + .setMessage(activity.getString(R.string.login_registration_not_supported)) + .setPositiveButton(R.string.yes) { _, _ -> + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginWebFragment::class.java, + option = commonOption) + } + .setNegativeButton(R.string.no, null) + .show() + } + + private fun onLoginModeNotSupported(supportedTypes: List) { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.app_name) + .setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) + .setPositiveButton(R.string.yes) { _, _ -> + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginWebFragment::class.java, + option = commonOption) + } + .setNegativeButton(R.string.no, null) + .show() + } + + private fun handleRegistrationNavigation(flowResult: FlowResult) { + // Complete all mandatory stages first + val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } + + if (mandatoryStage != null) { + doStage(mandatoryStage) + } else { + // Consider optional stages + val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy } + if (optionalStage == null) { + // Should not happen... + } else { + doStage(optionalStage) + } + } + } + + private fun doStage(stage: Stage) { + // Ensure there is no fragment for registration stage in the backstack + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + + when (stage) { + is Stage.ReCaptcha -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginCaptchaFragment::class.java, + LoginCaptchaFragmentArgument(stage.publicKey), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Email -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Msisdn -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Terms -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginTermsFragment::class.java, + LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + else -> Unit // Should not happen + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt new file mode 100644 index 0000000000..805e39c48d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt @@ -0,0 +1,85 @@ +/* + * 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.ftue + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.lazyViewModel +import im.vector.app.core.platform.ToolbarConfigurable +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.platform.lifecycleAwareLazy +import im.vector.app.databinding.ActivityLoginBinding +import im.vector.app.features.login.LoginConfig +import im.vector.app.features.pin.UnlockedActivity +import javax.inject.Inject + +@AndroidEntryPoint +class FTUEActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { + + private val ftueVariant by lifecycleAwareLazy { + ftueVariantFactory.create(this, loginViewModel = lazyViewModel(), loginViewModel2 = lazyViewModel()) + } + + @Inject lateinit var ftueVariantFactory: FTUEVariantFactory + + override fun getBinding() = ActivityLoginBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun configure(toolbar: MaterialToolbar) { + configureToolbar(toolbar) + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + ftueVariant.onNewIntent(intent) + } + + override fun initUiAndData() { + ftueVariant.initUiAndData(isFirstCreation()) + } + + // Hack for AccountCreatedFragment + fun setIsLoading(isLoading: Boolean) { + ftueVariant.setIsLoading(isLoading) + } + + companion object { + const val EXTRA_CONFIG = "EXTRA_CONFIG" + + fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { + return Intent(context, FTUEActivity::class.java).apply { + putExtra(EXTRA_CONFIG, loginConfig) + } + } + + fun redirectIntent(context: Context, data: Uri?): Intent { + return Intent(context, FTUEActivity::class.java).apply { + setData(data) + } + } + } +} + +interface FTUEVariant { + fun onNewIntent(intent: Intent?) + fun initUiAndData(isFirstCreation: Boolean) + fun setIsLoading(isLoading: Boolean) +} diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt new file mode 100644 index 0000000000..7efd6023fe --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt @@ -0,0 +1,43 @@ +/* + * 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.ftue + +import im.vector.app.features.VectorFeatures +import im.vector.app.features.login.LoginViewModel +import im.vector.app.features.login2.LoginViewModel2 +import javax.inject.Inject + +class FTUEVariantFactory @Inject constructor( + private val vectorFeatures: VectorFeatures, +) { + + fun create(activity: FTUEActivity, loginViewModel: Lazy, loginViewModel2: Lazy) = when (vectorFeatures.loginVariant()) { + VectorFeatures.LoginVariant.LEGACY -> error("Legacy is not supported by the FTUE") + VectorFeatures.LoginVariant.FTUE -> DefaultFTUEVariant( + views = activity.getBinding(), + loginViewModel = loginViewModel.value, + activity = activity, + supportFragmentManager = activity.supportFragmentManager + ) + VectorFeatures.LoginVariant.FTUE_WIP -> FTUEWipVariant( + views = activity.getBinding(), + loginViewModel = loginViewModel2.value, + activity = activity, + supportFragmentManager = activity.supportFragmentManager + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEWipVariant.kt similarity index 70% rename from vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt rename to vector/src/main/java/im/vector/app/features/ftue/FTUEWipVariant.kt index ce9d9f762e..c1fc49db00 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEWipVariant.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * 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 + * 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, @@ -14,11 +14,9 @@ * limitations under the License. */ -package im.vector.app.features.login2 +package im.vector.app.features.ftue -import android.content.Context import android.content.Intent -import android.net.Uri import android.view.View import android.view.ViewGroup import androidx.core.view.ViewCompat @@ -27,17 +25,13 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction -import com.airbnb.mvrx.viewModel -import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.resetBackstack -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityLoginBinding import im.vector.app.features.home.HomeActivity @@ -49,20 +43,41 @@ import im.vector.app.features.login.TextInputFormFragmentMode import im.vector.app.features.login.isSupported import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.login.terms.toLocalizedLoginTerms +import im.vector.app.features.login2.LoginAction2 +import im.vector.app.features.login2.LoginCaptchaFragment2 +import im.vector.app.features.login2.LoginFragmentSigninPassword2 +import im.vector.app.features.login2.LoginFragmentSigninUsername2 +import im.vector.app.features.login2.LoginFragmentSignupPassword2 +import im.vector.app.features.login2.LoginFragmentSignupUsername2 +import im.vector.app.features.login2.LoginFragmentToAny2 +import im.vector.app.features.login2.LoginGenericTextInputFormFragment2 +import im.vector.app.features.login2.LoginResetPasswordFragment2 +import im.vector.app.features.login2.LoginResetPasswordMailConfirmationFragment2 +import im.vector.app.features.login2.LoginResetPasswordSuccessFragment2 +import im.vector.app.features.login2.LoginServerSelectionFragment2 +import im.vector.app.features.login2.LoginServerUrlFormFragment2 +import im.vector.app.features.login2.LoginSplashSignUpSignInSelectionFragment2 +import im.vector.app.features.login2.LoginSsoOnlyFragment2 +import im.vector.app.features.login2.LoginViewEvents2 +import im.vector.app.features.login2.LoginViewModel2 +import im.vector.app.features.login2.LoginViewState2 +import im.vector.app.features.login2.LoginWaitForEmailFragment2 +import im.vector.app.features.login2.LoginWebFragment2 import im.vector.app.features.login2.created.AccountCreatedFragment import im.vector.app.features.login2.terms.LoginTermsFragment2 -import im.vector.app.features.pin.UnlockedActivity import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.extensions.tryOrNull -/** - * The LoginActivity manages the fragment navigation and also display the loading View - */ -@AndroidEntryPoint -open class LoginActivity2 : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { +private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" +private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" - private val loginViewModel: LoginViewModel2 by viewModel() +class FTUEWipVariant( + private val views: ActivityLoginBinding, + private val loginViewModel: LoginViewModel2, + private val activity: VectorBaseActivity, + private val supportFragmentManager: FragmentManager +) : FTUEVariant { private val enterAnim = R.anim.enter_fade_in private val exitAnim = R.anim.exit_fade_out @@ -76,39 +91,36 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC private val commonOption: (FragmentTransaction) -> Unit = { ft -> // Find the loginLogo on the current Fragment, this should not return null (topFragment?.view as? ViewGroup) - // Find findViewById does not work, I do not know why - // findViewById(R.id.loginLogo) + // Find activity.findViewById does not work, I do not know why + // activity.findViewById(views.loginLogo) ?.children ?.firstOrNull { it.id == R.id.loginLogo } ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) } - final override fun getBinding() = ActivityLoginBinding.inflate(layoutInflater) - - override fun getCoordinatorLayout() = views.coordinatorLayout - - override fun initUiAndData() { - if (isFirstCreation()) { + override fun initUiAndData(isFirstCreation: Boolean) { + if (isFirstCreation) { addFirstFragment() } - loginViewModel.onEach { - updateWithState(it) + with(activity) { + loginViewModel.onEach { + updateWithState(it) + } + loginViewModel.observeViewEvents { handleLoginViewEvents(it) } } - loginViewModel.observeViewEvents { handleLoginViewEvents(it) } - // Get config extra - val loginConfig = intent.getParcelableExtra(EXTRA_CONFIG) - if (isFirstCreation()) { + val loginConfig = activity.intent.getParcelableExtra(FTUEActivity.EXTRA_CONFIG) + if (isFirstCreation) { // TODO Check this loginViewModel.handle(LoginAction2.InitWith(loginConfig)) } } - protected open fun addFirstFragment() { - addFragment(views.loginFragmentContainer, LoginSplashSignUpSignInSelectionFragment2::class.java) + private fun addFirstFragment() { + activity.addFragment(views.loginFragmentContainer, LoginSplashSignUpSignInSelectionFragment2::class.java) } private fun handleLoginViewEvents(event: LoginViewEvents2) { @@ -127,7 +139,7 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC // First ask for login and password // I add a tag to indicate that this fragment is a registration stage. // This way it will be automatically popped in when starting the next registration stage - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragment2::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption @@ -138,7 +150,7 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC } } is LoginViewEvents2.OutdatedHomeserver -> { - MaterialAlertDialogBuilder(this) + MaterialAlertDialogBuilder(activity) .setTitle(R.string.login_error_outdated_homeserver_title) .setMessage(R.string.login_error_outdated_homeserver_warning_content) .setPositiveButton(R.string.ok, null) @@ -146,54 +158,54 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC Unit } is LoginViewEvents2.OpenServerSelection -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginServerSelectionFragment2::class.java, option = { ft -> - findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + activity.findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // Disable transition of text - // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // activity.findViewById(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // No transition here now actually - // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // activity.findViewById(views.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // TODO Disabled because it provokes a flickering // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) }) is LoginViewEvents2.OpenHomeServerUrlFormScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginServerUrlFormFragment2::class.java, option = commonOption) } is LoginViewEvents2.OpenSignInEnterIdentifierScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragmentSigninUsername2::class.java, option = { ft -> - findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + activity.findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // Disable transition of text - // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // activity.findViewById(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // No transition here now actually - // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // activity.findViewById(views.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // TODO Disabled because it provokes a flickering // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) }) } is LoginViewEvents2.OpenSsoOnlyScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginSsoOnlyFragment2::class.java, option = commonOption) } is LoginViewEvents2.OnWebLoginError -> onWebLoginError(event) is LoginViewEvents2.OpenResetPasswordScreen -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginResetPasswordFragment2::class.java, option = commonOption) is LoginViewEvents2.OnResetPasswordSendThreePidDone -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginResetPasswordMailConfirmationFragment2::class.java, option = commonOption) } is LoginViewEvents2.OnResetPasswordMailConfirmationSuccess -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginResetPasswordSuccessFragment2::class.java, option = commonOption) } @@ -202,37 +214,37 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) } is LoginViewEvents2.OnSendEmailSuccess -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginWaitForEmailFragment2::class.java, LoginWaitForEmailFragmentArgument(event.email), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) is LoginViewEvents2.OpenSigninPasswordScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragmentSigninPassword2::class.java, tag = FRAGMENT_LOGIN_TAG, option = commonOption) } is LoginViewEvents2.OpenSignupPasswordScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragmentSignupPassword2::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragmentSignupUsername2::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } is LoginViewEvents2.OpenSignInWithAnythingScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragmentToAny2::class.java, tag = FRAGMENT_LOGIN_TAG, option = commonOption) } is LoginViewEvents2.OnSendMsisdnSuccess -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, event.msisdn), tag = FRAGMENT_REGISTRATION_STAGE_TAG, @@ -250,14 +262,14 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC private fun handleCancelRegistration() { // Cleanup the back stack - resetBackstack() + activity.resetBackstack() } private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) { if (event.newAccount) { // Propose to set avatar and display name // Back on this Fragment will finish the Activity - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, AccountCreatedFragment::class.java, option = commonOption) } else { @@ -267,11 +279,11 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC private fun terminate(newAccount: Boolean) { val intent = HomeActivity.newIntent( - this, + activity, accountCreation = newAccount ) - startActivity(intent) - finish() + activity.startActivity(intent) + activity.finish() } private fun updateWithState(LoginViewState2: LoginViewState2) { @@ -280,7 +292,7 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC } // Hack for AccountCreatedFragment - fun setIsLoading(isLoading: Boolean) { + override fun setIsLoading(isLoading: Boolean) { views.loginLoading.isVisible = isLoading } @@ -289,9 +301,9 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) // And inform the user - MaterialAlertDialogBuilder(this) + MaterialAlertDialogBuilder(activity) .setTitle(R.string.dialog_title_error) - .setMessage(getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode)) + .setMessage(activity.getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode)) .setPositiveButton(R.string.ok, null) .show() } @@ -300,19 +312,17 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC * Handle the SSO redirection here */ override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) - intent?.data ?.let { tryOrNull { it.getQueryParameter("loginToken") } } ?.let { loginViewModel.handle(LoginAction2.LoginWithToken(it)) } } private fun onRegistrationStageNotSupported() { - MaterialAlertDialogBuilder(this) + MaterialAlertDialogBuilder(activity) .setTitle(R.string.app_name) - .setMessage(getString(R.string.login_registration_not_supported)) + .setMessage(activity.getString(R.string.login_registration_not_supported)) .setPositiveButton(R.string.yes) { _, _ -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginWebFragment2::class.java, option = commonOption) } @@ -321,11 +331,11 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC } private fun onLoginModeNotSupported(supportedTypes: List) { - MaterialAlertDialogBuilder(this) + MaterialAlertDialogBuilder(activity) .setTitle(R.string.app_name) - .setMessage(getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) + .setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) .setPositiveButton(R.string.yes) { _, _ -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginWebFragment2::class.java, option = commonOption) } @@ -355,53 +365,27 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) when (stage) { - is Stage.ReCaptcha -> addFragmentToBackstack(views.loginFragmentContainer, + is Stage.ReCaptcha -> activity.addFragmentToBackstack(views.loginFragmentContainer, LoginCaptchaFragment2::class.java, LoginCaptchaFragmentArgument(stage.publicKey), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is Stage.Email -> addFragmentToBackstack(views.loginFragmentContainer, + is Stage.Email -> activity.addFragmentToBackstack(views.loginFragmentContainer, LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is Stage.Msisdn -> addFragmentToBackstack(views.loginFragmentContainer, + is Stage.Msisdn -> activity.addFragmentToBackstack(views.loginFragmentContainer, LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is Stage.Terms -> addFragmentToBackstack(views.loginFragmentContainer, + is Stage.Terms -> activity.addFragmentToBackstack(views.loginFragmentContainer, LoginTermsFragment2::class.java, - LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))), + LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) else -> Unit // Should not happen } } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - - companion object { - private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" - private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" - - private const val EXTRA_CONFIG = "EXTRA_CONFIG" - - // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string - const val VECTOR_REDIRECT_URL = "element://connect" - - fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { - return Intent(context, LoginActivity2::class.java).apply { - putExtra(EXTRA_CONFIG, loginConfig) - } - } - - fun redirectIntent(context: Context, data: Uri?): Intent { - return Intent(context, LoginActivity2::class.java).apply { - setData(data) - } - } - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 652451c1ff..d9475a5e71 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1450,7 +1450,7 @@ class TimelineFragment @Inject constructor( AttachmentTypeSelectorView.Type.POLL, vectorPreferences.labsEnablePolls() && !isThreadTimeLine()) } - attachmentTypeSelector.show(views.composerLayout.views.attachmentButton, keyboardStateUtils.isKeyboardShowing) + attachmentTypeSelector.show(views.composerLayout.views.attachmentButton) } override fun onSendMessage(text: CharSequence) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt index 03107fd3f7..c751053cdf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt @@ -24,18 +24,21 @@ import android.text.Editable import android.util.AttributeSet import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection +import androidx.appcompat.widget.AppCompatEditText import androidx.core.view.OnReceiveContentListener import androidx.core.view.ViewCompat import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat -import com.vanniktech.emoji.EmojiEditText import im.vector.app.core.extensions.ooi import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.features.html.PillImageSpan import timber.log.Timber -class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle) : - EmojiEditText(context, attrs, defStyleAttr) { +class ComposerEditText @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.editTextStyle +) : AppCompatEditText(context, attrs, defStyleAttr) { interface Callback { fun onRichContentSelected(contentUri: Uri): Boolean diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 1d30136f27..14d9cce28a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -44,7 +44,7 @@ class EncryptionItemFactory @Inject constructor( if (!event.root.isStateEvent()) { return null } - val algorithm = event.root.getClearContent().toModel()?.algorithm + val algorithm = event.root.content.toModel()?.algorithm val informationData = informationDataFactory.create(params) val attributes = messageItemAttributesFactory.create(null, informationData, params.callback) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt index 382962f98d..523fb8e682 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -34,7 +34,7 @@ class RoomCreateItemFactory @Inject constructor(private val stringProvider: Stri fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { val event = params.event - val createRoomContent = event.root.getClearContent().toModel() ?: return null + val createRoomContent = event.root.content.toModel() ?: return null val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(params) val roomLink = session.permalinkService().createRoomPermalink(predecessorId) ?: return null val text = span { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 1e915d2b29..861f5dd546 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -54,67 +54,83 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me params.rootThreadEventId, params.isFromThreadTimeline()) } - when (event.root.getClearType()) { - // Message itemsX - EventType.STICKER, - EventType.POLL_START, - EventType.MESSAGE -> messageItemFactory.create(params) - EventType.STATE_ROOM_TOMBSTONE, - EventType.STATE_ROOM_NAME, - EventType.STATE_ROOM_TOPIC, - EventType.STATE_ROOM_AVATAR, - EventType.STATE_ROOM_MEMBER, - EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STATE_ROOM_CANONICAL_ALIAS, - EventType.STATE_ROOM_JOIN_RULES, - EventType.STATE_ROOM_HISTORY_VISIBILITY, - EventType.STATE_ROOM_SERVER_ACL, - EventType.STATE_ROOM_GUEST_ACCESS, - EventType.REDACTION, - EventType.STATE_ROOM_ALIASES, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_KEY, - EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_MAC, - EventType.CALL_CANDIDATES, - EventType.CALL_REPLACES, - EventType.CALL_SELECT_ANSWER, - EventType.CALL_NEGOTIATE, - EventType.REACTION, - EventType.STATE_SPACE_CHILD, - EventType.STATE_SPACE_PARENT, - EventType.STATE_ROOM_POWER_LEVELS, - EventType.POLL_RESPONSE, - EventType.POLL_END -> noticeItemFactory.create(params) - EventType.STATE_ROOM_WIDGET_LEGACY, - EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params) - EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params) - // State room create - EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params) - // Calls - EventType.CALL_INVITE, - EventType.CALL_HANGUP, - EventType.CALL_REJECT, - EventType.CALL_ANSWER -> callItemFactory.create(params) - // Crypto - EventType.ENCRYPTED -> { - if (event.root.isRedacted()) { - // Redacted event, let the MessageItemFactory handle it - messageItemFactory.create(params) - } else { - encryptedItemFactory.create(params) + + // Manage state event differently, to check validity + if (event.root.isStateEvent()) { + // state event are not e2e + when (event.root.type) { + EventType.STATE_ROOM_TOMBSTONE, + EventType.STATE_ROOM_NAME, + EventType.STATE_ROOM_TOPIC, + EventType.STATE_ROOM_AVATAR, + EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_THIRD_PARTY_INVITE, + EventType.STATE_ROOM_CANONICAL_ALIAS, + EventType.STATE_ROOM_JOIN_RULES, + EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.STATE_ROOM_SERVER_ACL, + EventType.STATE_ROOM_GUEST_ACCESS, + EventType.STATE_ROOM_ALIASES, + EventType.STATE_SPACE_CHILD, + EventType.STATE_SPACE_PARENT, + EventType.STATE_ROOM_POWER_LEVELS -> { + noticeItemFactory.create(params) + } + EventType.STATE_ROOM_WIDGET_LEGACY, + EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params) + EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params) + // State room create + EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params) + // Unhandled state event types + else -> { + // Should only happen when shouldShowHiddenEvents() settings is ON + Timber.v("State event type ${event.root.type} not handled") + defaultItemFactory.create(params) } } - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_DONE -> { - verificationConclusionItemFactory.create(params) - } - // Unhandled event types - else -> { - // Should only happen when shouldShowHiddenEvents() settings is ON - Timber.v("Type ${event.root.getClearType()} not handled") - defaultItemFactory.create(params) + } else { + when (event.root.getClearType()) { + // Message itemsX + EventType.STICKER, + EventType.POLL_START, + EventType.MESSAGE -> messageItemFactory.create(params) + EventType.REDACTION, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_MAC, + EventType.CALL_CANDIDATES, + EventType.CALL_REPLACES, + EventType.CALL_SELECT_ANSWER, + EventType.CALL_NEGOTIATE, + EventType.REACTION, + EventType.POLL_RESPONSE, + EventType.POLL_END -> noticeItemFactory.create(params) + // Calls + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_REJECT, + EventType.CALL_ANSWER -> callItemFactory.create(params) + // Crypto + EventType.ENCRYPTED -> { + if (event.root.isRedacted()) { + // Redacted event, let the MessageItemFactory handle it + messageItemFactory.create(params) + } else { + encryptedItemFactory.create(params) + } + } + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_DONE -> { + verificationConclusionItemFactory.create(params) + } + // Unhandled event types + else -> { + // Should only happen when shouldShowHiddenEvents() settings is ON + Timber.v("Type ${event.root.getClearType()} not handled") + defaultItemFactory.create(params) + } } } } catch (throwable: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt index 52f72810c9..a08383315c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt @@ -41,7 +41,7 @@ class WidgetItemFactory @Inject constructor( fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { val event = params.event - val widgetContent: WidgetContent = event.root.getClearContent().toModel() ?: return null + val widgetContent: WidgetContent = event.root.content.toModel() ?: return null val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel() return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 3dc46c9d70..d39b8aec5e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -114,7 +114,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? { - val powerLevelsContent: PowerLevelsContent = event.getClearContent().toModel() ?: return null + val powerLevelsContent: PowerLevelsContent = event.content.toModel() ?: return null val previousPowerLevelsContent: PowerLevelsContent = event.resolvedPrevContent().toModel() ?: return null val userIds = HashSet() userIds.addAll(powerLevelsContent.users.orEmpty().keys) @@ -142,7 +142,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatWidgetEvent(event: Event, disambiguatedDisplayName: String): CharSequence? { - val widgetContent: WidgetContent = event.getClearContent().toModel() ?: return null + val widgetContent: WidgetContent = event.content.toModel() ?: return null val previousWidgetContent: WidgetContent? = event.resolvedPrevContent().toModel() return if (widgetContent.isActive()) { val widgetName = widgetContent.getHumanName() @@ -198,7 +198,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? { - return event.getClearContent().toModel() + return event.content.toModel() ?.takeIf { it.creator.isNullOrBlank().not() } ?.let { if (event.isSentByCurrentUser()) { @@ -210,7 +210,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? { - val content = event.getClearContent().toModel() ?: return null + val content = event.content.toModel() ?: return null return if (content.name.isNullOrBlank()) { if (event.isSentByCurrentUser()) { sp.getString(R.string.notice_room_name_removed_by_you) @@ -235,7 +235,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? { - val content = event.getClearContent().toModel() ?: return null + val content = event.content.toModel() ?: return null return if (content.topic.isNullOrEmpty()) { if (event.isSentByCurrentUser()) { sp.getString(R.string.notice_room_topic_removed_by_you) @@ -252,7 +252,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomAvatarEvent(event: Event, senderName: String?): CharSequence? { - val content = event.getClearContent().toModel() ?: return null + val content = event.content.toModel() ?: return null return if (content.avatarUrl.isNullOrEmpty()) { if (event.isSentByCurrentUser()) { sp.getString(R.string.notice_room_avatar_removed_by_you) @@ -269,7 +269,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? { - val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null + val historyVisibility = event.content.toModel()?.historyVisibility ?: return null val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility) return if (event.isSentByCurrentUser()) { @@ -282,7 +282,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, isDm: Boolean): CharSequence? { - val content = event.getClearContent().toModel() + val content = event.content.toModel() val prevContent = event.resolvedPrevContent()?.toModel() return when { @@ -363,7 +363,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomMemberEvent(event: Event, senderName: String?, isDm: Boolean): String? { - val eventContent: RoomMemberContent? = event.getClearContent().toModel() + val eventContent: RoomMemberContent? = event.content.toModel() val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel() val isMembershipEvent = prevEventContent?.membership != eventContent?.membership || eventContent?.membership == Membership.LEAVE @@ -375,7 +375,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomAliasesEvent(event: Event, senderName: String?): String? { - val eventContent: RoomAliasesContent? = event.getClearContent().toModel() + val eventContent: RoomAliasesContent? = event.content.toModel() val prevEventContent: RoomAliasesContent? = event.resolvedPrevContent()?.toModel() val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty() @@ -408,7 +408,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomServerAclEvent(event: Event, senderName: String?): String? { - val eventContent = event.getClearContent().toModel() ?: return null + val eventContent = event.content.toModel() ?: return null val prevEventContent = event.resolvedPrevContent()?.toModel() return buildString { @@ -481,7 +481,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomCanonicalAliasEvent(event: Event, senderName: String?): String? { - val eventContent: RoomCanonicalAliasContent? = event.getClearContent().toModel() + val eventContent: RoomCanonicalAliasContent? = event.content.toModel() val prevContent: RoomCanonicalAliasContent? = event.resolvedPrevContent().toModel() val canonicalAlias = eventContent?.canonicalAlias?.takeIf { it.isNotEmpty() } val prevCanonicalAlias = prevContent?.canonicalAlias?.takeIf { it.isNotEmpty() } @@ -551,7 +551,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, isDm: Boolean): String? { - val eventContent: RoomGuestAccessContent? = event.getClearContent().toModel() + val eventContent: RoomGuestAccessContent? = event.content.toModel() return when (eventContent?.guestAccess) { GuestAccess.CanJoin -> if (event.isSentByCurrentUser()) { @@ -815,7 +815,7 @@ class NoticeEventFormatter @Inject constructor( } private fun formatJoinRulesEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? { - val content = event.getClearContent().toModel() ?: return null + val content = event.content.toModel() ?: return null return when (content.joinRules) { RoomJoinRules.INVITE -> if (event.isSentByCurrentUser()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 36f6dcab34..e5caaffbda 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -53,10 +53,10 @@ import timber.log.Timber class RoomListViewModel @AssistedInject constructor( @Assisted initialState: RoomListViewState, private val session: Session, - private val stringProvider: StringProvider, - private val appStateHandler: AppStateHandler, - private val vectorPreferences: VectorPreferences, - private val autoAcceptInvites: AutoAcceptInvites + stringProvider: StringProvider, + appStateHandler: AppStateHandler, + vectorPreferences: VectorPreferences, + autoAcceptInvites: AutoAcceptInvites ) : VectorViewModel(initialState) { @AssistedFactory diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt index 8663b7c73f..b18df6c9cf 100644 --- a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt @@ -88,7 +88,7 @@ abstract class AbstractSSOLoginFragment : AbstractLoginFragmen if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { // in this case we can prefetch (not other cases for privacy concerns) loginViewModel.getSsoUrl( - redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = null ) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index c46dca27b3..5ab08ffff7 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -356,9 +356,6 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo private const val EXTRA_CONFIG = "EXTRA_CONFIG" - // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string - const val VECTOR_REDIRECT_URL = "element://connect" - fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { return Intent(context, LoginActivity::class.java).apply { putExtra(EXTRA_CONFIG, loginConfig) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index 9ca8a1dbec..da61d95997 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -200,7 +200,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment if (state.loginMode is LoginMode.Sso) { loginViewModel.getSsoUrl( - redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = null ) diff --git a/vector/src/main/java/im/vector/app/features/login/SSORedirectRouterActivity.kt b/vector/src/main/java/im/vector/app/features/login/SSORedirectRouterActivity.kt index 29f8559362..19c549fd45 100644 --- a/vector/src/main/java/im/vector/app/features/login/SSORedirectRouterActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/SSORedirectRouterActivity.kt @@ -32,4 +32,9 @@ class SSORedirectRouterActivity : AppCompatActivity() { navigator.loginSSORedirect(this, intent.data) finish() } + + companion object { + // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string + const val VECTOR_REDIRECT_URL = "element://connect" + } } diff --git a/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt index 43f301d9b4..8bc531b25d 100644 --- a/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt @@ -24,6 +24,7 @@ import androidx.browser.customtabs.CustomTabsSession import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.withState import im.vector.app.core.utils.openUrlInChromeCustomTab +import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.hasSso import im.vector.app.features.login.ssoIdentityProviders @@ -90,7 +91,7 @@ abstract class AbstractSSOLoginFragment2 : AbstractLoginFragme if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { // in this case we can prefetch (not other cases for privacy concerns) loginViewModel.getSsoUrl( - redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = null ) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt index 51044ac153..f9917a4c31 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt @@ -30,6 +30,7 @@ import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginSignupUsername2Binding import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -97,7 +98,7 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { override fun onProviderSelected(id: String?) { loginViewModel.getSsoUrl( - redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = id ) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt index 48792da007..3fa0e6c549 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt @@ -31,6 +31,7 @@ import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginSigninToAny2Binding import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -124,7 +125,7 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2 loginViewModel.getSsoUrl( - redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = null ) diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt index 94784b0605..c1f45c6713 100644 --- a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt @@ -34,11 +34,11 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.FragmentLoginAccountCreatedBinding import im.vector.app.features.displayname.getBestName +import im.vector.app.features.ftue.FTUEActivity import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.login2.AbstractLoginFragment2 import im.vector.app.features.login2.LoginAction2 -import im.vector.app.features.login2.LoginActivity2 import im.vector.app.features.login2.LoginViewState2 import org.matrix.android.sdk.api.util.MatrixItem import java.util.UUID @@ -130,7 +130,7 @@ class AccountCreatedFragment @Inject constructor( private fun invalidateState(state: AccountCreatedViewState) { // Ugly hack... - (activity as? LoginActivity2)?.setIsLoading(state.isLoading) + (activity as? FTUEActivity)?.setIsLoading(state.isLoading) views.loginAccountCreatedSubtitle.text = getString(R.string.login_account_created_subtitle, state.userId) diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index e3a4766f3d..563de51371 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -51,6 +51,7 @@ import im.vector.app.features.crypto.verification.SupportedVerificationMethodsPr import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.debug.DebugMenuActivity import im.vector.app.features.devtools.RoomDevToolActivity +import im.vector.app.features.ftue.FTUEActivity import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.search.SearchActivity @@ -62,7 +63,6 @@ import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.invite.InviteUsersToRoomActivity import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginConfig -import im.vector.app.features.login2.LoginActivity2 import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.BigImageViewerActivity @@ -84,7 +84,6 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData import im.vector.app.features.signout.soft.SoftLogoutActivity -import im.vector.app.features.signout.soft.SoftLogoutActivity2 import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet import im.vector.app.features.spaces.SpaceExploreActivity import im.vector.app.features.spaces.SpacePreviewActivity @@ -115,27 +114,26 @@ class DefaultNavigator @Inject constructor( ) : Navigator { override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) { - val intent = when (features.loginVersion()) { - VectorFeatures.LoginVersion.V1 -> LoginActivity.newIntent(context, loginConfig) - VectorFeatures.LoginVersion.V2 -> LoginActivity2.newIntent(context, loginConfig) + val intent = when (features.loginVariant()) { + VectorFeatures.LoginVariant.LEGACY -> LoginActivity.newIntent(context, loginConfig) + VectorFeatures.LoginVariant.FTUE, + VectorFeatures.LoginVariant.FTUE_WIP -> FTUEActivity.newIntent(context, loginConfig) } intent.addFlags(flags) context.startActivity(intent) } override fun loginSSORedirect(context: Context, data: Uri?) { - val intent = when (features.loginVersion()) { - VectorFeatures.LoginVersion.V1 -> LoginActivity.redirectIntent(context, data) - VectorFeatures.LoginVersion.V2 -> LoginActivity2.redirectIntent(context, data) + val intent = when (features.loginVariant()) { + VectorFeatures.LoginVariant.LEGACY -> LoginActivity.redirectIntent(context, data) + VectorFeatures.LoginVariant.FTUE, + VectorFeatures.LoginVariant.FTUE_WIP -> FTUEActivity.redirectIntent(context, data) } context.startActivity(intent) } override fun softLogout(context: Context) { - val intent = when (features.loginVersion()) { - VectorFeatures.LoginVersion.V1 -> SoftLogoutActivity.newIntent(context) - VectorFeatures.LoginVersion.V2 -> SoftLogoutActivity2.newIntent(context) - } + val intent = SoftLogoutActivity.newIntent(context) context.startActivity(intent) } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index 87b31fa92a..f73e2ab0c3 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -66,12 +66,10 @@ class NotifiableEventResolver @Inject constructor( return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy) } val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null - when (event.getClearType()) { - EventType.MESSAGE -> { - return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) - } + return when (event.getClearType()) { + EventType.MESSAGE, EventType.ENCRYPTED -> { - return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) + resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) } else -> { // If the event can be displayed, display it as is @@ -79,7 +77,7 @@ class NotifiableEventResolver @Inject constructor( // TODO Better event text display val bodyPreview = event.type ?: EventType.MISSING_TYPE - return SimpleNotifiableEvent( + SimpleNotifiableEvent( session.myUserId, eventId = event.eventId!!, editedEventId = timelineEvent.getEditedEventId(), @@ -126,18 +124,18 @@ class NotifiableEventResolver @Inject constructor( } } - private suspend fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent { + private suspend fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? { // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/) - if (room == null) { + return if (room == null) { Timber.e("## Unable to resolve room for eventId [$event]") // Ok room is not known in store, but we can still display something val body = displayableEventFormatter.format(event, isDm = false, appendAuthor = false) val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val senderDisplayName = event.senderInfo.disambiguatedDisplayName - return NotifiableMessageEvent( + NotifiableMessageEvent( eventId = event.root.eventId!!, editedEventId = event.getEditedEventId(), canBeReplaced = canBeReplaced, @@ -152,51 +150,60 @@ class NotifiableEventResolver @Inject constructor( matrixID = session.myUserId ) } else { - if (event.root.isEncrypted() && event.root.mxDecryptionResult == null) { - // TODO use a global event decryptor? attache to session and that listen to new sessionId? - // for now decrypt sync - try { - val result = session.cryptoService().decryptEvent(event.root, event.root.roomId + UUID.randomUUID().toString()) - event.root.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + event.attemptToDecryptIfNeeded(session) + // only convert encrypted messages to NotifiableMessageEvents + when (event.root.getClearType()) { + EventType.MESSAGE -> { + val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() + val roomName = room.roomSummary()?.displayName ?: "" + val senderDisplayName = event.senderInfo.disambiguatedDisplayName + + NotifiableMessageEvent( + eventId = event.root.eventId!!, + editedEventId = event.getEditedEventId(), + canBeReplaced = canBeReplaced, + timestamp = event.root.originServerTs ?: 0, + noisy = isNoisy, + senderName = senderDisplayName, + senderId = event.root.senderId, + body = body, + imageUri = event.fetchImageIfPresent(session), + roomId = event.root.roomId!!, + roomName = roomName, + roomIsDirect = room.roomSummary()?.isDirect ?: false, + roomAvatarPath = session.contentUrlResolver() + .resolveThumbnail(room.roomSummary()?.avatarUrl, + 250, + 250, + ContentUrlResolver.ThumbnailMethod.SCALE), + senderAvatarPath = session.contentUrlResolver() + .resolveThumbnail(event.senderInfo.avatarUrl, + 250, + 250, + ContentUrlResolver.ThumbnailMethod.SCALE), + matrixID = session.myUserId, + soundName = null ) - } catch (e: MXCryptoError) { } + else -> null } + } + } - val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() - val roomName = room.roomSummary()?.displayName ?: "" - val senderDisplayName = event.senderInfo.disambiguatedDisplayName - - return NotifiableMessageEvent( - eventId = event.root.eventId!!, - editedEventId = event.getEditedEventId(), - canBeReplaced = canBeReplaced, - timestamp = event.root.originServerTs ?: 0, - noisy = isNoisy, - senderName = senderDisplayName, - senderId = event.root.senderId, - body = body, - imageUri = event.fetchImageIfPresent(session), - roomId = event.root.roomId!!, - roomName = roomName, - roomIsDirect = room.roomSummary()?.isDirect ?: false, - roomAvatarPath = session.contentUrlResolver() - .resolveThumbnail(room.roomSummary()?.avatarUrl, - 250, - 250, - ContentUrlResolver.ThumbnailMethod.SCALE), - senderAvatarPath = session.contentUrlResolver() - .resolveThumbnail(event.senderInfo.avatarUrl, - 250, - 250, - ContentUrlResolver.ThumbnailMethod.SCALE), - matrixID = session.myUserId, - soundName = null - ) + private fun TimelineEvent.attemptToDecryptIfNeeded(session: Session) { + if (root.isEncrypted() && root.mxDecryptionResult == null) { + // TODO use a global event decryptor? attache to session and that listen to new sessionId? + // for now decrypt sync + try { + val result = session.cryptoService().decryptEvent(root, root.roomId + UUID.randomUUID().toString()) + root.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 64561cbc12..3436c20ce3 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -44,6 +44,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { const val SETTINGS_HOME_SERVER_PREFERENCE_KEY = "SETTINGS_HOME_SERVER_PREFERENCE_KEY" const val SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY" const val SETTINGS_DISCOVERY_PREFERENCE_KEY = "SETTINGS_DISCOVERY_PREFERENCE_KEY" + const val SETTINGS_EMAILS_AND_PHONE_NUMBERS_PREFERENCE_KEY = "SETTINGS_EMAILS_AND_PHONE_NUMBERS_PREFERENCE_KEY" const val SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY" const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY" diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt deleted file mode 100644 index 8489b2baef..0000000000 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 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.signout.soft - -import android.content.Context -import android.content.Intent -import androidx.core.view.isVisible -import androidx.fragment.app.FragmentManager -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.viewModel -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.android.AndroidEntryPoint -import im.vector.app.R -import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.extensions.replaceFragment -import im.vector.app.features.MainActivity -import im.vector.app.features.MainActivityArgs -import im.vector.app.features.login2.LoginActivity2 -import org.matrix.android.sdk.api.failure.GlobalError -import org.matrix.android.sdk.api.session.Session -import timber.log.Timber -import javax.inject.Inject - -/** - * In this screen, the user is viewing a message informing that he has been logged out - * Extends LoginActivity to get the login with SSO and forget password functionality for (nearly) free - * - * This is just a copy of SoftLogoutActivity2, which extends LoginActivity2 - */ -@AndroidEntryPoint -class SoftLogoutActivity2 : LoginActivity2() { - - private val softLogoutViewModel: SoftLogoutViewModel by viewModel() - - @Inject lateinit var session: Session - @Inject lateinit var errorFormatter: ErrorFormatter - - override fun initUiAndData() { - super.initUiAndData() - - softLogoutViewModel.onEach { - updateWithState(it) - } - - softLogoutViewModel.observeViewEvents { handleSoftLogoutViewEvents(it) } - } - - private fun handleSoftLogoutViewEvents(softLogoutViewEvents: SoftLogoutViewEvents) { - when (softLogoutViewEvents) { - is SoftLogoutViewEvents.Failure -> - showError(errorFormatter.toHumanReadable(softLogoutViewEvents.throwable)) - is SoftLogoutViewEvents.ErrorNotSameUser -> { - // Pop the backstack - supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - - // And inform the user - showError(getString( - R.string.soft_logout_sso_not_same_user_error, - softLogoutViewEvents.currentUserId, - softLogoutViewEvents.newUserId) - ) - } - is SoftLogoutViewEvents.ClearData -> { - MainActivity.restartApp(this, MainActivityArgs(clearCredentials = true)) - } - } - } - - private fun showError(message: String) { - MaterialAlertDialogBuilder(this) - .setTitle(R.string.dialog_title_error) - .setMessage(message) - .setPositiveButton(R.string.ok, null) - .show() - } - - override fun addFirstFragment() { - replaceFragment(views.loginFragmentContainer, SoftLogoutFragment::class.java) - } - - private fun updateWithState(softLogoutViewState: SoftLogoutViewState) { - if (softLogoutViewState.asyncLoginAction is Success) { - MainActivity.restartApp(this, MainActivityArgs()) - } - - views.loginLoading.isVisible = softLogoutViewState.isLoading() - } - - companion object { - fun newIntent(context: Context): Intent { - return Intent(context, SoftLogoutActivity2::class.java) - } - } - - override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { - // No op here - Timber.w("Ignoring invalid token global error") - } -} diff --git a/vector/src/main/res/drawable-hdpi/ic_attachment_stickers_white_24dp.png b/vector/src/main/res/drawable-hdpi/ic_attachment_stickers_white_24dp.png deleted file mode 100644 index d27e8f406e..0000000000 Binary files a/vector/src/main/res/drawable-hdpi/ic_attachment_stickers_white_24dp.png and /dev/null differ diff --git a/vector/src/main/res/drawable-mdpi/ic_attachment_stickers_white_24dp.png b/vector/src/main/res/drawable-mdpi/ic_attachment_stickers_white_24dp.png deleted file mode 100644 index 40d78cf9e2..0000000000 Binary files a/vector/src/main/res/drawable-mdpi/ic_attachment_stickers_white_24dp.png and /dev/null differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_attachment_stickers_white_24dp.png b/vector/src/main/res/drawable-xhdpi/ic_attachment_stickers_white_24dp.png deleted file mode 100644 index 46e23b9cdc..0000000000 Binary files a/vector/src/main/res/drawable-xhdpi/ic_attachment_stickers_white_24dp.png and /dev/null differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_attachment_stickers_white_24dp.png b/vector/src/main/res/drawable-xxhdpi/ic_attachment_stickers_white_24dp.png deleted file mode 100644 index 4058b25495..0000000000 Binary files a/vector/src/main/res/drawable-xxhdpi/ic_attachment_stickers_white_24dp.png and /dev/null differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_attachment_stickers_white_24dp.png b/vector/src/main/res/drawable-xxxhdpi/ic_attachment_stickers_white_24dp.png deleted file mode 100644 index c5b2435646..0000000000 Binary files a/vector/src/main/res/drawable-xxxhdpi/ic_attachment_stickers_white_24dp.png and /dev/null differ diff --git a/vector/src/main/res/drawable/ic_attachment_camera.xml b/vector/src/main/res/drawable/ic_attachment_camera.xml new file mode 100644 index 0000000000..8c7bedb3cf --- /dev/null +++ b/vector/src/main/res/drawable/ic_attachment_camera.xml @@ -0,0 +1,13 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_attachment_camera_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_camera_white_24dp.xml deleted file mode 100644 index 5c2920d252..0000000000 --- a/vector/src/main/res/drawable/ic_attachment_camera_white_24dp.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_attachment_file.xml b/vector/src/main/res/drawable/ic_attachment_file.xml new file mode 100644 index 0000000000..b3545e54a6 --- /dev/null +++ b/vector/src/main/res/drawable/ic_attachment_file.xml @@ -0,0 +1,13 @@ + + + diff --git a/vector/src/main/res/drawable/ic_attachment_file_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_file_white_24dp.xml deleted file mode 100644 index 4e6b9458f8..0000000000 --- a/vector/src/main/res/drawable/ic_attachment_file_white_24dp.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_attachment_gallery.xml b/vector/src/main/res/drawable/ic_attachment_gallery.xml new file mode 100644 index 0000000000..0f3432544f --- /dev/null +++ b/vector/src/main/res/drawable/ic_attachment_gallery.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/ic_attachment_gallery_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_gallery_white_24dp.xml deleted file mode 100644 index d4e68f125b..0000000000 --- a/vector/src/main/res/drawable/ic_attachment_gallery_white_24dp.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_attachment_location.xml b/vector/src/main/res/drawable/ic_attachment_location.xml new file mode 100644 index 0000000000..c2c8093e1d --- /dev/null +++ b/vector/src/main/res/drawable/ic_attachment_location.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_attachment_poll_white_24dp.xml b/vector/src/main/res/drawable/ic_attachment_poll.xml similarity index 93% rename from vector/src/main/res/drawable/ic_attachment_poll_white_24dp.xml rename to vector/src/main/res/drawable/ic_attachment_poll.xml index 8cbcc6e47c..320dccb7fc 100644 --- a/vector/src/main/res/drawable/ic_attachment_poll_white_24dp.xml +++ b/vector/src/main/res/drawable/ic_attachment_poll.xml @@ -5,6 +5,6 @@ android:viewportHeight="24"> diff --git a/vector/src/main/res/drawable/ic_attachment_sticker.xml b/vector/src/main/res/drawable/ic_attachment_sticker.xml new file mode 100644 index 0000000000..eb59eaa75d --- /dev/null +++ b/vector/src/main/res/drawable/ic_attachment_sticker.xml @@ -0,0 +1,13 @@ + + + diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml index 3378878ac6..7e926b860c 100644 --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml @@ -108,9 +108,9 @@ - + android:layout_height="@dimen/composer_min_height" + android:background="?android:colorBackground"> - + + + android:layout_marginStart="4dp" + android:scrollbars="none" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/attachmentCloseButton" + app:layout_constraintTop_toTopOf="parent"> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal"> - + - + - + - + - + - + + - - - - - - - - - - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index 53be4f07f6..9f8e58d724 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -34,7 +34,7 @@