Merge branch 'develop' into feature/aris/threads

# Conflicts:
#	library/ui-styles/src/main/res/values/dimens.xml
This commit is contained in:
ariskotsomitopoulos 2022-01-03 11:08:22 +02:00
commit 694b8de034
73 changed files with 1222 additions and 755 deletions

View File

@ -7,6 +7,8 @@ on:
jobs: jobs:
automate-project-columns: automate-project-columns:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: |
github.repository == 'vector-im/element-android' # Skip in forks
steps: steps:
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
with: with:

View File

@ -8,6 +8,8 @@ jobs:
move_needs_info_issues: move_needs_info_issues:
name: X-Needs-Info issues to Need info column on triage board name: X-Needs-Info issues to Need info column on triage board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: |
github.repository == 'vector-im/element-android' # Skip in forks
steps: steps:
- uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338 - uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338
with: with:
@ -19,7 +21,8 @@ jobs:
add_priority_design_issues_to_project: add_priority_design_issues_to_project:
name: P1 X-Needs-Design to Design project board name: P1 X-Needs-Design to Design project board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > 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, 'X-Needs-Design') &&
(contains(github.event.issue.labels.*.name, 'S-Critical') && (contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') || (contains(github.event.issue.labels.*.name, 'O-Frequent') ||
@ -50,7 +53,8 @@ jobs:
# delight_issues_to_board: # delight_issues_to_board:
# name: Spaces issues to new Delight project board # name: Spaces issues to new Delight project board
# runs-on: ubuntu-latest # runs-on: ubuntu-latest
# if: > # 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-Spaces') ||
# contains(github.event.issue.labels.*.name, 'A-Space-Settings') || # contains(github.event.issue.labels.*.name, 'A-Space-Settings') ||
# contains(github.event.issue.labels.*.name, 'A-Subspaces') # contains(github.event.issue.labels.*.name, 'A-Subspaces')
@ -75,7 +79,8 @@ jobs:
move_voice-message_issues: move_voice-message_issues:
name: A-Voice Messages to voice message board name: A-Voice Messages to voice message board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: |
github.repository == 'vector-im/element-android' && # Skip in forks
contains(github.event.issue.labels.*.name, 'A-Voice Messages') contains(github.event.issue.labels.*.name, 'A-Voice Messages')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
@ -98,7 +103,8 @@ jobs:
move_threads_issues: move_threads_issues:
name: A-Threads to Thread board name: A-Threads to Thread board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: |
github.repository == 'vector-im/element-android' && # Skip in forks
contains(github.event.issue.labels.*.name, 'A-Threads') contains(github.event.issue.labels.*.name, 'A-Threads')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
@ -121,7 +127,8 @@ jobs:
move_message_bubbles_issues: move_message_bubbles_issues:
name: A-Message-Bubbles to Message bubbles board name: A-Message-Bubbles to Message bubbles board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: |
github.repository == 'vector-im/element-android' && # Skip in forks
contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') contains(github.event.issue.labels.*.name, 'A-Message-Bubbles')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x

View File

@ -8,9 +8,9 @@ jobs:
Move_Unabeled_Issue_On_Project_Board: Move_Unabeled_Issue_On_Project_Board:
name: Move no longer X-Needs-Info issues to Triaged name: Move no longer X-Needs-Info issues to Triaged
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: |
${{ github.repository == 'vector-im/element-android' && # Skip in forks
!contains(github.event.issue.labels.*.name, 'X-Needs-Info') }} !contains(github.event.issue.labels.*.name, 'X-Needs-Info')
env: env:
BOARD_NAME: "Issue triage" BOARD_NAME: "Issue triage"
OWNER: ${{ github.repository_owner }} OWNER: ${{ github.repository_owner }}

View File

@ -7,7 +7,8 @@ on:
jobs: jobs:
p1_issues_to_team_workboard: p1_issues_to_team_workboard:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > 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') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') && !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-Dehydration') &&
@ -33,7 +34,8 @@ jobs:
P1_issues_to_crypto_team_workboard: P1_issues_to_crypto_team_workboard:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > 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') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') || 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-Dehydration') ||

View File

@ -29,7 +29,7 @@ buildscript {
// ktlint Plugin // ktlint Plugin
plugins { plugins {
id "org.jlleitschuh.gradle.ktlint" version "10.2.0" id "org.jlleitschuh.gradle.ktlint" version "10.2.1"
} }
allprojects { allprojects {

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

@ -0,0 +1 @@
Attachment picker UI improvements

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

@ -0,0 +1 @@
Workaround to fetch all the pending toDevice events from a Synapse homeserver

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

@ -0,0 +1 @@
Cleaning rendering of state events in timeline

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

@ -0,0 +1 @@
Fixes newer emojis rendering strangely when inserting from the system keyboard

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

@ -0,0 +1 @@
Fixing unable to change change avatar in some scenarios

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

@ -0,0 +1 @@
Fixing encrypted non message events showing up as notification messages (eg when a participant joins, mutes or leaves a voice call)

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=dd54e87b4d7aa8ff3c6afb0f7805aa121d4b70bca55b8c9b1b896eb103184582 distributionSha256Sum=c9490e938b221daf0094982288e4038deed954a3f12fb54cbf270ddf4e37d879
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -46,4 +46,9 @@
<dimen name="menu_item_icon_size">24dp</dimen> <dimen name="menu_item_icon_size">24dp</dimen>
<dimen name="menu_item_size">48dp</dimen> <dimen name="menu_item_size">48dp</dimen>
<dimen name="menu_item_ripple_size">48dp</dimen> <dimen name="menu_item_ripple_size">48dp</dimen>
<!-- Composer -->
<dimen name="composer_min_height">56dp</dimen>
<dimen name="composer_attachment_size">52dp</dimen>
<dimen name="composer_attachment_margin">1dp</dimen>
</resources> </resources>

View File

@ -152,6 +152,13 @@ class FlowSession(private val session: Session) {
} }
} }
fun liveUserAccountData(type: String): Flow<Optional<UserAccountDataEvent>> {
return session.accountDataService().getLiveUserAccountDataEvent(type).asFlow()
.startWith(session.coroutineDispatchers.io) {
session.accountDataService().getUserAccountDataEvent(type).toOptional()
}
}
fun liveRoomAccountData(types: Set<String>): Flow<List<RoomAccountDataEvent>> { fun liveRoomAccountData(types: Set<String>): Flow<List<RoomAccountDataEvent>> {
return session.accountDataService().getLiveRoomAccountDataEvents(types).asFlow() return session.accountDataService().getLiveRoomAccountDataEvents(types).asFlow()
.startWith(session.coroutineDispatchers.io) { .startWith(session.coroutineDispatchers.io) {

View File

@ -160,7 +160,7 @@ dependencies {
implementation libs.apache.commonsImaging implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber // 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 libs.tests.junit
testImplementation 'org.robolectric:robolectric:4.7.3' testImplementation 'org.robolectric:robolectric:4.7.3'

View File

@ -429,7 +429,17 @@ internal class DefaultCryptoService @Inject constructor(
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0 val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount) 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 // Make sure we process to-device messages before generating new one-time-keys #2782
deviceListManager.refreshOutdatedDeviceLists() deviceListManager.refreshOutdatedDeviceLists()
// The presence of device_unused_fallback_key_types indicates that the server supports fallback keys. // The presence of device_unused_fallback_key_types indicates that the server supports fallback keys.

View File

@ -109,15 +109,20 @@ internal class FileUploader @Inject constructor(
filename: String?, filename: String?,
mimeType: String?, mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val inputStream = withContext(Dispatchers.IO) { val workingFile = context.copyUriToTempFile(uri)
context.contentResolver.openInputStream(uri) return uploadFile(workingFile, filename, mimeType, progressListener).also {
} ?: throw FileNotFoundException() 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() val workingFile = temporaryFileCreator.create()
workingFile.outputStream().use { workingFile.outputStream().use {
inputStream.copyTo(it) inputStream.copyTo(it)
} }
return uploadFile(workingFile, filename, mimeType, progressListener).also { workingFile
tryOrNull { workingFile.delete() }
} }
} }

View File

@ -68,7 +68,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
} }
override suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) { override suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) {
withContext(coroutineDispatchers.main) { withContext(coroutineDispatchers.io) {
val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg) val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg)
setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri))
userStore.updateAvatar(userId, response.contentUri) userStore.updateAvatar(userId, response.contentUri)

View File

@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking 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.Failure
import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.failure.isTokenError
import org.matrix.android.sdk.api.logger.LoggerTag 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 isStarted = false
private var isTokenValid = true private var isTokenValid = true
private var retryNoNetworkTask: TimerTask? = null private var retryNoNetworkTask: TimerTask? = null
private var previousSyncResponseHasToDevice = false
private val activeCallListObserver = Observer<MutableList<MxCall>> { activeCalls -> private val activeCallListObserver = Observer<MutableList<MxCall>> { activeCalls ->
if (activeCalls.isEmpty() && backgroundDetectionObserver.isInBackground) { if (activeCalls.isEmpty() && backgroundDetectionObserver.isInBackground) {
@ -171,12 +173,15 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
if (state !is SyncState.Running) { if (state !is SyncState.Running) {
updateStateTo(SyncState.Running(afterPause = true)) updateStateTo(SyncState.Running(afterPause = true))
} }
// No timeout after a pause val timeout = when {
val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } 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") Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout")
val params = SyncTask.Params(timeout, SyncPresence.Online) val params = SyncTask.Params(timeout, SyncPresence.Online)
val sync = syncScope.launch { val sync = syncScope.launch {
doSync(params) previousSyncResponseHasToDevice = doSync(params)
} }
runBlocking { runBlocking {
sync.join() 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) val syncResponse = syncTask.execute(params)
_syncFlow.emit(syncResponse) _syncFlow.emit(syncResponse)
syncResponse.toDevice?.events?.isNotEmpty().orFalse()
} catch (failure: Throwable) { } catch (failure: Throwable) {
if (failure is Failure.NetworkConnection) { if (failure is Failure.NetworkConnection) {
canReachServer = false canReachServer = false
@ -229,6 +238,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
delay(RETRY_WAIT_TIME_MS) delay(RETRY_WAIT_TIME_MS)
} }
} }
false
} finally { } finally {
state.let { state.let {
if (it is SyncState.Running && it.afterPause) { if (it is SyncState.Running && it.afterPause) {

View File

@ -20,6 +20,7 @@ import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass 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.api.failure.isTokenError
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.di.WorkManagerProvider
@ -34,8 +35,8 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
private const val DEFAULT_LONG_POOL_TIMEOUT = 6L private const val DEFAULT_LONG_POOL_TIMEOUT_SECONDS = 6L
private const val DEFAULT_DELAY_TIMEOUT = 30_000L private const val DEFAULT_DELAY_MILLIS = 30_000L
/** /**
* Possible previous worker: None * Possible previous worker: None
@ -47,9 +48,12 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT, // In seconds
val delay: Long = DEFAULT_DELAY_TIMEOUT, val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT_SECONDS,
// In milliseconds
val delay: Long = DEFAULT_DELAY_MILLIS,
val periodic: Boolean = false, val periodic: Boolean = false,
val forceImmediate: Boolean = false,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@ -65,13 +69,26 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
Timber.i("Sync work starting") Timber.i("Sync work starting")
return runCatching { return runCatching {
doSync(params.timeout) doSync(if (params.forceImmediate) 0 else params.timeout)
}.fold( }.fold(
{ { hasToDeviceEvents ->
Result.success().also { Result.success().also {
if (params.periodic) { if (params.periodic) {
// we want to schedule another one after delay // we want to schedule another one after a delay, or immediately if hasToDeviceEvents
automaticallyBackgroundSync(workManagerProvider, params.sessionId, params.timeout, params.delay) 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) 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) val taskParams = SyncTask.Params(timeout * 1000, SyncPresence.Offline)
syncTask.execute(taskParams) val syncResponse = syncTask.execute(taskParams)
return syncResponse.toDevice?.events?.isNotEmpty().orFalse()
} }
companion object { companion object {
private const val BG_SYNC_WORK_NAME = "BG_SYNCP" private const val BG_SYNC_WORK_NAME = "BG_SYNCP"
fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) { fun requireBackgroundSync(workManagerProvider: WorkManagerProvider,
val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, 0L, false)) sessionId: String,
serverTimeoutInSeconds: Long = 0) {
val data = WorkerParamsFactory.toData(
Params(
sessionId = sessionId,
timeout = serverTimeoutInSeconds,
delay = 0L,
periodic = false
)
)
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(WorkManagerProvider.workConstraints) .setConstraints(WorkManagerProvider.workConstraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .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) .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
} }
fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delayInSeconds: Long = 30) { fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider,
val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, delayInSeconds, true)) 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<SyncWorker>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(WorkManagerProvider.workConstraints) .setConstraints(WorkManagerProvider.workConstraints)
.setInputData(data) .setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.setInitialDelay(delayInSeconds, TimeUnit.SECONDS) .setInitialDelay(if (forceImmediate) 0 else delayInSeconds, TimeUnit.SECONDS)
.build() .build()
// Avoid risking multiple chains of syncs by replacing the existing chain // Avoid risking multiple chains of syncs by replacing the existing chain
workManagerProvider.workManager workManagerProvider.workManager

View File

@ -140,7 +140,7 @@ android {
buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\"" buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\""
resValue "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" 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' implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber // Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.39' implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40'
// FlowBinding // FlowBinding
implementation libs.github.flowBinding implementation libs.github.flowBinding

View File

@ -28,8 +28,8 @@ class DebugFeaturesStateFactory @Inject constructor(
return FeaturesState(listOf( return FeaturesState(listOf(
createEnumFeature( createEnumFeature(
label = "Login version", label = "Login version",
selection = debugFeatures.loginVersion(), selection = debugFeatures.loginVariant(),
default = defaultFeatures.loginVersion() default = defaultFeatures.loginVariant()
) )
)) ))
} }

View File

@ -38,8 +38,8 @@ class DebugVectorFeatures(
private val dataStore = context.dataStore private val dataStore = context.dataStore
override fun loginVersion(): VectorFeatures.LoginVersion { override fun loginVariant(): VectorFeatures.LoginVariant {
return readPreferences().getEnum<VectorFeatures.LoginVersion>() ?: vectorFeatures.loginVersion() return readPreferences().getEnum<VectorFeatures.LoginVariant>() ?: vectorFeatures.loginVariant()
} }
fun <T : Enum<T>> hasEnumOverride(type: KClass<T>) = readPreferences().containsEnum(type) fun <T : Enum<T>> hasEnumOverride(type: KClass<T>) = readPreferences().containsEnum(type)

View File

@ -137,7 +137,7 @@
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".features.login2.LoginActivity2" android:name=".features.ftue.FTUEActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />

View File

@ -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 VM : MavericksViewModel<S>, reified S : MavericksState> ComponentActivity.lazyViewModel(): Lazy<VM> {
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
)
}
}

View File

@ -105,7 +105,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
protected val viewModelProvider protected val viewModelProvider
get() = ViewModelProvider(this, viewModelFactory) get() = ViewModelProvider(this, viewModelFactory)
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents viewEvents
.stream() .stream()
.onEach { .onEach {

View File

@ -20,11 +20,12 @@ import im.vector.app.BuildConfig
interface VectorFeatures { interface VectorFeatures {
fun loginVersion(): LoginVersion fun loginVariant(): LoginVariant
enum class LoginVersion { enum class LoginVariant {
V1, LEGACY,
V2 FTUE,
FTUE_WIP
} }
enum class NotificationSettingsVersion { enum class NotificationSettingsVersion {
@ -34,5 +35,5 @@ interface VectorFeatures {
} }
class DefaultVectorFeatures : VectorFeatures { class DefaultVectorFeatures : VectorFeatures {
override fun loginVersion(): VectorFeatures.LoginVersion = BuildConfig.LOGIN_VERSION override fun loginVariant(): VectorFeatures.LoginVariant = BuildConfig.LOGIN_VARIANT
} }

View File

@ -26,24 +26,18 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewAnimationUtils import android.view.ViewAnimationUtils
import android.view.animation.Animation 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.view.animation.TranslateAnimation
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.PopupWindow import android.widget.PopupWindow
import androidx.core.view.doOnNextLayout import androidx.core.view.doOnNextLayout
import androidx.core.view.isVisible 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.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_EMPTY
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback
import kotlin.math.max import kotlin.math.max
private const val ANIMATION_DURATION = 250 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. * This class is the view presenting choices for picking attachments.
* It will return result through [Callback]. * It will return result through [Callback].
*/ */
class AttachmentTypeSelectorView(context: Context, class AttachmentTypeSelectorView(context: Context,
inflater: LayoutInflater, inflater: LayoutInflater,
var callback: Callback?) : var callback: Callback?
PopupWindow(context) { ) : PopupWindow(context) {
interface Callback { interface Callback {
fun onTypeSelected(type: Type) fun onTypeSelected(type: Type)
} }
private val iconColorGenerator = ColorGenerator.MATERIAL
private val views: ViewAttachmentTypeSelectorBinding private val views: ViewAttachmentTypeSelectorBinding
private var anchor: View? = null private var anchor: View? = null
@ -85,35 +78,40 @@ class AttachmentTypeSelectorView(context: Context,
inputMethodMode = INPUT_METHOD_NOT_NEEDED inputMethodMode = INPUT_METHOD_NOT_NEEDED
isFocusable = true isFocusable = true
isTouchable = 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 this.anchor = anchor
val anchorCoordinates = IntArray(2) val anchorCoordinates = IntArray(2)
anchor.getLocationOnScreen(anchorCoordinates) anchor.getLocationOnScreen(anchorCoordinates)
if (isKeyboardOpen) { showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1])
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)
}
contentView.doOnNextLayout { contentView.doOnNextLayout {
animateWindowInCircular(anchor, contentView) 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() { override fun dismiss() {
animateClose()
val capturedAnchor = anchor val capturedAnchor = anchor
if (capturedAnchor != null) { if (capturedAnchor != null) {
animateWindowOutCircular(capturedAnchor, contentView) animateWindowOutCircular(capturedAnchor, contentView)
@ -124,28 +122,18 @@ class AttachmentTypeSelectorView(context: Context,
fun setAttachmentVisibility(type: Type, isVisible: Boolean) { fun setAttachmentVisibility(type: Type, isVisible: Boolean) {
when (type) { when (type) {
Type.CAMERA -> views.attachmentCameraButtonContainer Type.CAMERA -> views.attachmentCameraButton
Type.GALLERY -> views.attachmentGalleryButtonContainer Type.GALLERY -> views.attachmentGalleryButton
Type.FILE -> views.attachmentFileButtonContainer Type.FILE -> views.attachmentFileButton
Type.STICKER -> views.attachmentStickersButtonContainer Type.STICKER -> views.attachmentStickersButton
Type.AUDIO -> views.attachmentAudioButtonContainer Type.AUDIO -> views.attachmentAudioButton
Type.CONTACT -> views.attachmentContactButtonContainer Type.CONTACT -> views.attachmentContactButton
Type.POLL -> views.attachmentPollButtonContainer Type.POLL -> views.attachmentPollButton
}.let { }.let {
it.isVisible = isVisible 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) { private fun animateWindowInCircular(anchor: View, contentView: View) {
val coordinates = getClickCoordinates(anchor, contentView) val coordinates = getClickCoordinates(anchor, contentView)
val animator = ViewAnimationUtils.createCircularReveal(contentView, val animator = ViewAnimationUtils.createCircularReveal(contentView,
@ -157,12 +145,6 @@ class AttachmentTypeSelectorView(context: Context,
animator.start() 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) { private fun animateWindowOutCircular(anchor: View, contentView: View) {
val coordinates = getClickCoordinates(anchor, contentView) val coordinates = getClickCoordinates(anchor, contentView)
val animator = ViewAnimationUtils.createCircularReveal(getContentView(), val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
@ -207,7 +189,6 @@ class AttachmentTypeSelectorView(context: Context,
} }
private fun ImageButton.configure(type: Type): ImageButton { private fun ImageButton.configure(type: Type): ImageButton {
this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal))
this.setOnClickListener(TypeClickListener(type)) this.setOnClickListener(TypeClickListener(type))
return this return this
} }

View File

@ -168,11 +168,8 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
} }
} }
private fun renderCreationSuccess(roomId: String?) { private fun renderCreationSuccess(roomId: String) {
// Navigate to freshly created room
if (roomId != null) {
navigator.openRoom(this, roomId) navigator.openRoom(this, roomId)
}
finish() finish()
} }

View File

@ -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<ActivityLoginBinding>,
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<View?>(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<LoginConfig?>(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<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// Disable transition of text
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// No transition here now actually
// findViewById<View?>(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<String>) {
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
}
}
}

View File

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

View File

@ -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<LoginViewModel>, loginViewModel2: Lazy<LoginViewModel2>) = 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
)
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2021 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,11 +14,9 @@
* limitations under the License. * 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.content.Intent
import android.net.Uri
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@ -27,17 +25,13 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction 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 com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.resetBackstack 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.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityLoginBinding import im.vector.app.databinding.ActivityLoginBinding
import im.vector.app.features.home.HomeActivity 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.isSupported
import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.login.terms.LoginTermsFragmentArgument
import im.vector.app.features.login.terms.toLocalizedLoginTerms 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.created.AccountCreatedFragment
import im.vector.app.features.login2.terms.LoginTermsFragment2 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.FlowResult
import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
/** private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG"
* The LoginActivity manages the fragment navigation and also display the loading View private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG"
*/
@AndroidEntryPoint
open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarConfigurable, UnlockedActivity {
private val loginViewModel: LoginViewModel2 by viewModel() class FTUEWipVariant(
private val views: ActivityLoginBinding,
private val loginViewModel: LoginViewModel2,
private val activity: VectorBaseActivity<ActivityLoginBinding>,
private val supportFragmentManager: FragmentManager
) : FTUEVariant {
private val enterAnim = R.anim.enter_fade_in private val enterAnim = R.anim.enter_fade_in
private val exitAnim = R.anim.exit_fade_out private val exitAnim = R.anim.exit_fade_out
@ -76,39 +91,36 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
private val commonOption: (FragmentTransaction) -> Unit = { ft -> private val commonOption: (FragmentTransaction) -> Unit = { ft ->
// Find the loginLogo on the current Fragment, this should not return null // Find the loginLogo on the current Fragment, this should not return null
(topFragment?.view as? ViewGroup) (topFragment?.view as? ViewGroup)
// Find findViewById does not work, I do not know why // Find activity.findViewById does not work, I do not know why
// findViewById<View?>(R.id.loginLogo) // activity.findViewById<View?>(views.loginLogo)
?.children ?.children
?.firstOrNull { it.id == R.id.loginLogo } ?.firstOrNull { it.id == R.id.loginLogo }
?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
} }
final override fun getBinding() = ActivityLoginBinding.inflate(layoutInflater) override fun initUiAndData(isFirstCreation: Boolean) {
if (isFirstCreation) {
override fun getCoordinatorLayout() = views.coordinatorLayout
override fun initUiAndData() {
if (isFirstCreation()) {
addFirstFragment() addFirstFragment()
} }
with(activity) {
loginViewModel.onEach { loginViewModel.onEach {
updateWithState(it) updateWithState(it)
} }
loginViewModel.observeViewEvents { handleLoginViewEvents(it) } loginViewModel.observeViewEvents { handleLoginViewEvents(it) }
}
// Get config extra // Get config extra
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG) val loginConfig = activity.intent.getParcelableExtra<LoginConfig?>(FTUEActivity.EXTRA_CONFIG)
if (isFirstCreation()) { if (isFirstCreation) {
// TODO Check this // TODO Check this
loginViewModel.handle(LoginAction2.InitWith(loginConfig)) loginViewModel.handle(LoginAction2.InitWith(loginConfig))
} }
} }
protected open fun addFirstFragment() { private fun addFirstFragment() {
addFragment(views.loginFragmentContainer, LoginSplashSignUpSignInSelectionFragment2::class.java) activity.addFragment(views.loginFragmentContainer, LoginSplashSignUpSignInSelectionFragment2::class.java)
} }
private fun handleLoginViewEvents(event: LoginViewEvents2) { private fun handleLoginViewEvents(event: LoginViewEvents2) {
@ -127,7 +139,7 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
// First ask for login and password // First ask for login and password
// I add a tag to indicate that this fragment is a registration stage. // 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 // This way it will be automatically popped in when starting the next registration stage
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginFragment2::class.java, LoginFragment2::class.java,
tag = FRAGMENT_REGISTRATION_STAGE_TAG, tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption option = commonOption
@ -138,7 +150,7 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
} }
} }
is LoginViewEvents2.OutdatedHomeserver -> { is LoginViewEvents2.OutdatedHomeserver -> {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(activity)
.setTitle(R.string.login_error_outdated_homeserver_title) .setTitle(R.string.login_error_outdated_homeserver_title)
.setMessage(R.string.login_error_outdated_homeserver_warning_content) .setMessage(R.string.login_error_outdated_homeserver_warning_content)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
@ -146,54 +158,54 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
Unit Unit
} }
is LoginViewEvents2.OpenServerSelection -> is LoginViewEvents2.OpenServerSelection ->
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginServerSelectionFragment2::class.java, LoginServerSelectionFragment2::class.java,
option = { ft -> option = { ft ->
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } activity.findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// Disable transition of text // Disable transition of text
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // activity.findViewById<View?>(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// No transition here now actually // No transition here now actually
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // activity.findViewById<View?>(views.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// TODO Disabled because it provokes a flickering // TODO Disabled because it provokes a flickering
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}) })
is LoginViewEvents2.OpenHomeServerUrlFormScreen -> { is LoginViewEvents2.OpenHomeServerUrlFormScreen -> {
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginServerUrlFormFragment2::class.java, LoginServerUrlFormFragment2::class.java,
option = commonOption) option = commonOption)
} }
is LoginViewEvents2.OpenSignInEnterIdentifierScreen -> { is LoginViewEvents2.OpenSignInEnterIdentifierScreen -> {
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginFragmentSigninUsername2::class.java, LoginFragmentSigninUsername2::class.java,
option = { ft -> option = { ft ->
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } activity.findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// Disable transition of text // Disable transition of text
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // activity.findViewById<View?>(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// No transition here now actually // No transition here now actually
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // activity.findViewById<View?>(views.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// TODO Disabled because it provokes a flickering // TODO Disabled because it provokes a flickering
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}) })
} }
is LoginViewEvents2.OpenSsoOnlyScreen -> { is LoginViewEvents2.OpenSsoOnlyScreen -> {
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginSsoOnlyFragment2::class.java, LoginSsoOnlyFragment2::class.java,
option = commonOption) option = commonOption)
} }
is LoginViewEvents2.OnWebLoginError -> onWebLoginError(event) is LoginViewEvents2.OnWebLoginError -> onWebLoginError(event)
is LoginViewEvents2.OpenResetPasswordScreen -> is LoginViewEvents2.OpenResetPasswordScreen ->
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginResetPasswordFragment2::class.java, LoginResetPasswordFragment2::class.java,
option = commonOption) option = commonOption)
is LoginViewEvents2.OnResetPasswordSendThreePidDone -> { is LoginViewEvents2.OnResetPasswordSendThreePidDone -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginResetPasswordMailConfirmationFragment2::class.java, LoginResetPasswordMailConfirmationFragment2::class.java,
option = commonOption) option = commonOption)
} }
is LoginViewEvents2.OnResetPasswordMailConfirmationSuccess -> { is LoginViewEvents2.OnResetPasswordMailConfirmationSuccess -> {
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginResetPasswordSuccessFragment2::class.java, LoginResetPasswordSuccessFragment2::class.java,
option = commonOption) option = commonOption)
} }
@ -202,37 +214,37 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
} }
is LoginViewEvents2.OnSendEmailSuccess -> is LoginViewEvents2.OnSendEmailSuccess ->
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginWaitForEmailFragment2::class.java, LoginWaitForEmailFragment2::class.java,
LoginWaitForEmailFragmentArgument(event.email), LoginWaitForEmailFragmentArgument(event.email),
tag = FRAGMENT_REGISTRATION_STAGE_TAG, tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption) option = commonOption)
is LoginViewEvents2.OpenSigninPasswordScreen -> { is LoginViewEvents2.OpenSigninPasswordScreen -> {
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginFragmentSigninPassword2::class.java, LoginFragmentSigninPassword2::class.java,
tag = FRAGMENT_LOGIN_TAG, tag = FRAGMENT_LOGIN_TAG,
option = commonOption) option = commonOption)
} }
is LoginViewEvents2.OpenSignupPasswordScreen -> { is LoginViewEvents2.OpenSignupPasswordScreen -> {
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginFragmentSignupPassword2::class.java, LoginFragmentSignupPassword2::class.java,
tag = FRAGMENT_REGISTRATION_STAGE_TAG, tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption) option = commonOption)
} }
is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> { is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> {
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginFragmentSignupUsername2::class.java, LoginFragmentSignupUsername2::class.java,
tag = FRAGMENT_REGISTRATION_STAGE_TAG, tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption) option = commonOption)
} }
is LoginViewEvents2.OpenSignInWithAnythingScreen -> { is LoginViewEvents2.OpenSignInWithAnythingScreen -> {
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginFragmentToAny2::class.java, LoginFragmentToAny2::class.java,
tag = FRAGMENT_LOGIN_TAG, tag = FRAGMENT_LOGIN_TAG,
option = commonOption) option = commonOption)
} }
is LoginViewEvents2.OnSendMsisdnSuccess -> is LoginViewEvents2.OnSendMsisdnSuccess ->
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragment2::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, event.msisdn), LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, event.msisdn),
tag = FRAGMENT_REGISTRATION_STAGE_TAG, tag = FRAGMENT_REGISTRATION_STAGE_TAG,
@ -250,14 +262,14 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
private fun handleCancelRegistration() { private fun handleCancelRegistration() {
// Cleanup the back stack // Cleanup the back stack
resetBackstack() activity.resetBackstack()
} }
private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) { private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) {
if (event.newAccount) { if (event.newAccount) {
// Propose to set avatar and display name // Propose to set avatar and display name
// Back on this Fragment will finish the Activity // Back on this Fragment will finish the Activity
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
AccountCreatedFragment::class.java, AccountCreatedFragment::class.java,
option = commonOption) option = commonOption)
} else { } else {
@ -267,11 +279,11 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
private fun terminate(newAccount: Boolean) { private fun terminate(newAccount: Boolean) {
val intent = HomeActivity.newIntent( val intent = HomeActivity.newIntent(
this, activity,
accountCreation = newAccount accountCreation = newAccount
) )
startActivity(intent) activity.startActivity(intent)
finish() activity.finish()
} }
private fun updateWithState(LoginViewState2: LoginViewState2) { private fun updateWithState(LoginViewState2: LoginViewState2) {
@ -280,7 +292,7 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
} }
// Hack for AccountCreatedFragment // Hack for AccountCreatedFragment
fun setIsLoading(isLoading: Boolean) { override fun setIsLoading(isLoading: Boolean) {
views.loginLoading.isVisible = isLoading views.loginLoading.isVisible = isLoading
} }
@ -289,9 +301,9 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
// And inform the user // And inform the user
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(activity)
.setTitle(R.string.dialog_title_error) .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) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
@ -300,19 +312,17 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
* Handle the SSO redirection here * Handle the SSO redirection here
*/ */
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.data intent?.data
?.let { tryOrNull { it.getQueryParameter("loginToken") } } ?.let { tryOrNull { it.getQueryParameter("loginToken") } }
?.let { loginViewModel.handle(LoginAction2.LoginWithToken(it)) } ?.let { loginViewModel.handle(LoginAction2.LoginWithToken(it)) }
} }
private fun onRegistrationStageNotSupported() { private fun onRegistrationStageNotSupported() {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(activity)
.setTitle(R.string.app_name) .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) { _, _ -> .setPositiveButton(R.string.yes) { _, _ ->
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginWebFragment2::class.java, LoginWebFragment2::class.java,
option = commonOption) option = commonOption)
} }
@ -321,11 +331,11 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
} }
private fun onLoginModeNotSupported(supportedTypes: List<String>) { private fun onLoginModeNotSupported(supportedTypes: List<String>) {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(activity)
.setTitle(R.string.app_name) .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) { _, _ -> .setPositiveButton(R.string.yes) { _, _ ->
addFragmentToBackstack(views.loginFragmentContainer, activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginWebFragment2::class.java, LoginWebFragment2::class.java,
option = commonOption) option = commonOption)
} }
@ -355,53 +365,27 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
when (stage) { when (stage) {
is Stage.ReCaptcha -> addFragmentToBackstack(views.loginFragmentContainer, is Stage.ReCaptcha -> activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginCaptchaFragment2::class.java, LoginCaptchaFragment2::class.java,
LoginCaptchaFragmentArgument(stage.publicKey), LoginCaptchaFragmentArgument(stage.publicKey),
tag = FRAGMENT_REGISTRATION_STAGE_TAG, tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption) option = commonOption)
is Stage.Email -> addFragmentToBackstack(views.loginFragmentContainer, is Stage.Email -> activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragment2::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG, tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption) option = commonOption)
is Stage.Msisdn -> addFragmentToBackstack(views.loginFragmentContainer, is Stage.Msisdn -> activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragment2::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
tag = FRAGMENT_REGISTRATION_STAGE_TAG, tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption) option = commonOption)
is Stage.Terms -> addFragmentToBackstack(views.loginFragmentContainer, is Stage.Terms -> activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginTermsFragment2::class.java, 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, tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption) option = commonOption)
else -> Unit // Should not happen 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)
}
}
}
} }

View File

@ -1450,7 +1450,7 @@ class TimelineFragment @Inject constructor(
AttachmentTypeSelectorView.Type.POLL, AttachmentTypeSelectorView.Type.POLL,
vectorPreferences.labsEnablePolls() && !isThreadTimeLine()) vectorPreferences.labsEnablePolls() && !isThreadTimeLine())
} }
attachmentTypeSelector.show(views.composerLayout.views.attachmentButton, keyboardStateUtils.isKeyboardShowing) attachmentTypeSelector.show(views.composerLayout.views.attachmentButton)
} }
override fun onSendMessage(text: CharSequence) { override fun onSendMessage(text: CharSequence) {

View File

@ -24,18 +24,21 @@ import android.text.Editable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection import android.view.inputmethod.InputConnection
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.OnReceiveContentListener import androidx.core.view.OnReceiveContentListener
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputConnectionCompat
import com.vanniktech.emoji.EmojiEditText
import im.vector.app.core.extensions.ooi import im.vector.app.core.extensions.ooi
import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.core.platform.SimpleTextWatcher
import im.vector.app.features.html.PillImageSpan import im.vector.app.features.html.PillImageSpan
import timber.log.Timber import timber.log.Timber
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle) : class ComposerEditText @JvmOverloads constructor(
EmojiEditText(context, attrs, defStyleAttr) { context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.editTextStyle
) : AppCompatEditText(context, attrs, defStyleAttr) {
interface Callback { interface Callback {
fun onRichContentSelected(contentUri: Uri): Boolean fun onRichContentSelected(contentUri: Uri): Boolean

View File

@ -44,7 +44,7 @@ class EncryptionItemFactory @Inject constructor(
if (!event.root.isStateEvent()) { if (!event.root.isStateEvent()) {
return null return null
} }
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm val algorithm = event.root.content.toModel<EncryptionEventContent>()?.algorithm
val informationData = informationDataFactory.create(params) val informationData = informationDataFactory.create(params)
val attributes = messageItemAttributesFactory.create(null, informationData, params.callback) val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)

View File

@ -34,7 +34,7 @@ class RoomCreateItemFactory @Inject constructor(private val stringProvider: Stri
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event val event = params.event
val createRoomContent = event.root.getClearContent().toModel<RoomCreateContent>() ?: return null val createRoomContent = event.root.content.toModel<RoomCreateContent>() ?: return null
val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(params) val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(params)
val roomLink = session.permalinkService().createRoomPermalink(predecessorId) ?: return null val roomLink = session.permalinkService().createRoomPermalink(predecessorId) ?: return null
val text = span { val text = span {

View File

@ -54,11 +54,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
params.rootThreadEventId, params.rootThreadEventId,
params.isFromThreadTimeline()) params.isFromThreadTimeline())
} }
when (event.root.getClearType()) {
// Message itemsX // Manage state event differently, to check validity
EventType.STICKER, if (event.root.isStateEvent()) {
EventType.POLL_START, // state event are not e2e
EventType.MESSAGE -> messageItemFactory.create(params) when (event.root.type) {
EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,
@ -70,8 +70,31 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_SERVER_ACL,
EventType.STATE_ROOM_GUEST_ACCESS, EventType.STATE_ROOM_GUEST_ACCESS,
EventType.REDACTION,
EventType.STATE_ROOM_ALIASES, 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)
}
}
} 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_ACCEPT,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
@ -82,16 +105,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.CALL_SELECT_ANSWER, EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE, EventType.CALL_NEGOTIATE,
EventType.REACTION, EventType.REACTION,
EventType.STATE_SPACE_CHILD,
EventType.STATE_SPACE_PARENT,
EventType.STATE_ROOM_POWER_LEVELS,
EventType.POLL_RESPONSE, EventType.POLL_RESPONSE,
EventType.POLL_END -> noticeItemFactory.create(params) 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 // Calls
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
@ -117,6 +132,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
defaultItemFactory.create(params) defaultItemFactory.create(params)
} }
} }
}
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.e(throwable, "failed to create message item") Timber.e(throwable, "failed to create message item")
defaultItemFactory.create(params, throwable) defaultItemFactory.create(params, throwable)

View File

@ -41,7 +41,7 @@ class WidgetItemFactory @Inject constructor(
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event 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() val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel()
return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) { return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) {

View File

@ -114,7 +114,7 @@ class NoticeEventFormatter @Inject constructor(
} }
private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? { 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 previousPowerLevelsContent: PowerLevelsContent = event.resolvedPrevContent().toModel() ?: return null
val userIds = HashSet<String>() val userIds = HashSet<String>()
userIds.addAll(powerLevelsContent.users.orEmpty().keys) userIds.addAll(powerLevelsContent.users.orEmpty().keys)
@ -142,7 +142,7 @@ class NoticeEventFormatter @Inject constructor(
} }
private fun formatWidgetEvent(event: Event, disambiguatedDisplayName: String): CharSequence? { 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() val previousWidgetContent: WidgetContent? = event.resolvedPrevContent().toModel()
return if (widgetContent.isActive()) { return if (widgetContent.isActive()) {
val widgetName = widgetContent.getHumanName() val widgetName = widgetContent.getHumanName()
@ -198,7 +198,7 @@ class NoticeEventFormatter @Inject constructor(
} }
private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? { private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? {
return event.getClearContent().toModel<RoomCreateContent>() return event.content.toModel<RoomCreateContent>()
?.takeIf { it.creator.isNullOrBlank().not() } ?.takeIf { it.creator.isNullOrBlank().not() }
?.let { ?.let {
if (event.isSentByCurrentUser()) { if (event.isSentByCurrentUser()) {
@ -210,7 +210,7 @@ class NoticeEventFormatter @Inject constructor(
} }
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? { private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
val content = event.getClearContent().toModel<RoomNameContent>() ?: return null val content = event.content.toModel<RoomNameContent>() ?: return null
return if (content.name.isNullOrBlank()) { return if (content.name.isNullOrBlank()) {
if (event.isSentByCurrentUser()) { if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_room_name_removed_by_you) 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? { private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
val content = event.getClearContent().toModel<RoomTopicContent>() ?: return null val content = event.content.toModel<RoomTopicContent>() ?: return null
return if (content.topic.isNullOrEmpty()) { return if (content.topic.isNullOrEmpty()) {
if (event.isSentByCurrentUser()) { if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_room_topic_removed_by_you) 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? { private fun formatRoomAvatarEvent(event: Event, senderName: String?): CharSequence? {
val content = event.getClearContent().toModel<RoomAvatarContent>() ?: return null val content = event.content.toModel<RoomAvatarContent>() ?: return null
return if (content.avatarUrl.isNullOrEmpty()) { return if (content.avatarUrl.isNullOrEmpty()) {
if (event.isSentByCurrentUser()) { if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_room_avatar_removed_by_you) 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? { private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null val historyVisibility = event.content.toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility) val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility)
return if (event.isSentByCurrentUser()) { return if (event.isSentByCurrentUser()) {
@ -282,7 +282,7 @@ class NoticeEventFormatter @Inject constructor(
} }
private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, isDm: Boolean): CharSequence? { private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
val content = event.getClearContent().toModel<RoomThirdPartyInviteContent>() val content = event.content.toModel<RoomThirdPartyInviteContent>()
val prevContent = event.resolvedPrevContent()?.toModel<RoomThirdPartyInviteContent>() val prevContent = event.resolvedPrevContent()?.toModel<RoomThirdPartyInviteContent>()
return when { return when {
@ -363,7 +363,7 @@ class NoticeEventFormatter @Inject constructor(
} }
private fun formatRoomMemberEvent(event: Event, senderName: String?, isDm: Boolean): String? { 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 prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel()
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership || val isMembershipEvent = prevEventContent?.membership != eventContent?.membership ||
eventContent?.membership == Membership.LEAVE eventContent?.membership == Membership.LEAVE
@ -375,7 +375,7 @@ class NoticeEventFormatter @Inject constructor(
} }
private fun formatRoomAliasesEvent(event: Event, senderName: String?): String? { 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 prevEventContent: RoomAliasesContent? = event.resolvedPrevContent()?.toModel()
val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty() val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty()
@ -408,7 +408,7 @@ class NoticeEventFormatter @Inject constructor(
} }
private fun formatRoomServerAclEvent(event: Event, senderName: String?): String? { private fun formatRoomServerAclEvent(event: Event, senderName: String?): String? {
val eventContent = event.getClearContent().toModel<RoomServerAclContent>() ?: return null val eventContent = event.content.toModel<RoomServerAclContent>() ?: return null
val prevEventContent = event.resolvedPrevContent()?.toModel<RoomServerAclContent>() val prevEventContent = event.resolvedPrevContent()?.toModel<RoomServerAclContent>()
return buildString { return buildString {
@ -481,7 +481,7 @@ class NoticeEventFormatter @Inject constructor(
} }
private fun formatRoomCanonicalAliasEvent(event: Event, senderName: String?): String? { 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 prevContent: RoomCanonicalAliasContent? = event.resolvedPrevContent().toModel()
val canonicalAlias = eventContent?.canonicalAlias?.takeIf { it.isNotEmpty() } val canonicalAlias = eventContent?.canonicalAlias?.takeIf { it.isNotEmpty() }
val prevCanonicalAlias = prevContent?.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? { 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) { return when (eventContent?.guestAccess) {
GuestAccess.CanJoin -> GuestAccess.CanJoin ->
if (event.isSentByCurrentUser()) { if (event.isSentByCurrentUser()) {
@ -815,7 +815,7 @@ class NoticeEventFormatter @Inject constructor(
} }
private fun formatJoinRulesEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? { private fun formatJoinRulesEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
val content = event.getClearContent().toModel<RoomJoinRulesContent>() ?: return null val content = event.content.toModel<RoomJoinRulesContent>() ?: return null
return when (content.joinRules) { return when (content.joinRules) {
RoomJoinRules.INVITE -> RoomJoinRules.INVITE ->
if (event.isSentByCurrentUser()) { if (event.isSentByCurrentUser()) {

View File

@ -53,10 +53,10 @@ import timber.log.Timber
class RoomListViewModel @AssistedInject constructor( class RoomListViewModel @AssistedInject constructor(
@Assisted initialState: RoomListViewState, @Assisted initialState: RoomListViewState,
private val session: Session, private val session: Session,
private val stringProvider: StringProvider, stringProvider: StringProvider,
private val appStateHandler: AppStateHandler, appStateHandler: AppStateHandler,
private val vectorPreferences: VectorPreferences, vectorPreferences: VectorPreferences,
private val autoAcceptInvites: AutoAcceptInvites autoAcceptInvites: AutoAcceptInvites
) : VectorViewModel<RoomListViewState, RoomListAction, RoomListViewEvents>(initialState) { ) : VectorViewModel<RoomListViewState, RoomListAction, RoomListViewEvents>(initialState) {
@AssistedFactory @AssistedFactory

View File

@ -88,7 +88,7 @@ abstract class AbstractSSOLoginFragment<VB : ViewBinding> : AbstractLoginFragmen
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
// in this case we can prefetch (not other cases for privacy concerns) // in this case we can prefetch (not other cases for privacy concerns)
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = null providerId = null
) )

View File

@ -356,9 +356,6 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), ToolbarCo
private const val EXTRA_CONFIG = "EXTRA_CONFIG" 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 { fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
return Intent(context, LoginActivity::class.java).apply { return Intent(context, LoginActivity::class.java).apply {
putExtra(EXTRA_CONFIG, loginConfig) putExtra(EXTRA_CONFIG, loginConfig)

View File

@ -200,7 +200,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) { override fun onProviderSelected(id: String?) {
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = id providerId = id
) )

View File

@ -76,7 +76,7 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) { override fun onProviderSelected(id: String?) {
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = id providerId = id
) )
@ -109,7 +109,7 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi
private fun submit() = withState(loginViewModel) { state -> private fun submit() = withState(loginViewModel) { state ->
if (state.loginMode is LoginMode.Sso) { if (state.loginMode is LoginMode.Sso) {
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = null providerId = null
) )

View File

@ -32,4 +32,9 @@ class SSORedirectRouterActivity : AppCompatActivity() {
navigator.loginSSORedirect(this, intent.data) navigator.loginSSORedirect(this, intent.data)
finish() 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"
}
} }

View File

@ -24,6 +24,7 @@ import androidx.browser.customtabs.CustomTabsSession
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.core.utils.openUrlInChromeCustomTab 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.hasSso
import im.vector.app.features.login.ssoIdentityProviders import im.vector.app.features.login.ssoIdentityProviders
@ -90,7 +91,7 @@ abstract class AbstractSSOLoginFragment2<VB : ViewBinding> : AbstractLoginFragme
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
// in this case we can prefetch (not other cases for privacy concerns) // in this case we can prefetch (not other cases for privacy concerns)
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = null providerId = null
) )

View File

@ -30,6 +30,7 @@ import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.toReducedUrl import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginSignupUsername2Binding import im.vector.app.databinding.FragmentLoginSignupUsername2Binding
import im.vector.app.features.login.LoginMode import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.SocialLoginButtonsView import im.vector.app.features.login.SocialLoginButtonsView
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -97,7 +98,7 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) { override fun onProviderSelected(id: String?) {
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = id providerId = id
) )

View File

@ -31,6 +31,7 @@ import im.vector.app.core.extensions.hidePassword
import im.vector.app.core.extensions.toReducedUrl import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginSigninToAny2Binding import im.vector.app.databinding.FragmentLoginSigninToAny2Binding
import im.vector.app.features.login.LoginMode import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.SocialLoginButtonsView import im.vector.app.features.login.SocialLoginButtonsView
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -124,7 +125,7 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<Frag
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) { override fun onProviderSelected(id: String?) {
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = id providerId = id
) )

View File

@ -24,6 +24,7 @@ import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.toReducedUrl import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginSsoOnly2Binding import im.vector.app.databinding.FragmentLoginSsoOnly2Binding
import im.vector.app.features.login.SSORedirectRouterActivity
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -51,7 +52,7 @@ class LoginSsoOnlyFragment2 @Inject constructor() : AbstractSSOLoginFragment2<Fr
private fun submit() = withState(loginViewModel) { state -> private fun submit() = withState(loginViewModel) { state ->
loginViewModel.getSsoUrl( loginViewModel.getSsoUrl(
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId, deviceId = state.deviceId,
providerId = null providerId = null
) )

View File

@ -34,11 +34,11 @@ import im.vector.app.core.resources.ColorProvider
import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.DialogBaseEditTextBinding
import im.vector.app.databinding.FragmentLoginAccountCreatedBinding import im.vector.app.databinding.FragmentLoginAccountCreatedBinding
import im.vector.app.features.displayname.getBestName 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.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.login2.AbstractLoginFragment2 import im.vector.app.features.login2.AbstractLoginFragment2
import im.vector.app.features.login2.LoginAction2 import im.vector.app.features.login2.LoginAction2
import im.vector.app.features.login2.LoginActivity2
import im.vector.app.features.login2.LoginViewState2 import im.vector.app.features.login2.LoginViewState2
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import java.util.UUID import java.util.UUID
@ -130,7 +130,7 @@ class AccountCreatedFragment @Inject constructor(
private fun invalidateState(state: AccountCreatedViewState) { private fun invalidateState(state: AccountCreatedViewState) {
// Ugly hack... // 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) views.loginAccountCreatedSubtitle.text = getString(R.string.login_account_created_subtitle, state.userId)

View File

@ -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.crypto.verification.VerificationBottomSheet
import im.vector.app.features.debug.DebugMenuActivity import im.vector.app.features.debug.DebugMenuActivity
import im.vector.app.features.devtools.RoomDevToolActivity 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.RoomDetailActivity
import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.arguments.TimelineArgs
import im.vector.app.features.home.room.detail.search.SearchActivity 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.invite.InviteUsersToRoomActivity
import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginActivity
import im.vector.app.features.login.LoginConfig 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.matrixto.MatrixToBottomSheet
import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.AttachmentData
import im.vector.app.features.media.BigImageViewerActivity 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.settings.VectorSettingsActivity
import im.vector.app.features.share.SharedData import im.vector.app.features.share.SharedData
import im.vector.app.features.signout.soft.SoftLogoutActivity 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.InviteRoomSpaceChooserBottomSheet
import im.vector.app.features.spaces.SpaceExploreActivity import im.vector.app.features.spaces.SpaceExploreActivity
import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.spaces.SpacePreviewActivity
@ -115,27 +114,26 @@ class DefaultNavigator @Inject constructor(
) : Navigator { ) : Navigator {
override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) { override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) {
val intent = when (features.loginVersion()) { val intent = when (features.loginVariant()) {
VectorFeatures.LoginVersion.V1 -> LoginActivity.newIntent(context, loginConfig) VectorFeatures.LoginVariant.LEGACY -> LoginActivity.newIntent(context, loginConfig)
VectorFeatures.LoginVersion.V2 -> LoginActivity2.newIntent(context, loginConfig) VectorFeatures.LoginVariant.FTUE,
VectorFeatures.LoginVariant.FTUE_WIP -> FTUEActivity.newIntent(context, loginConfig)
} }
intent.addFlags(flags) intent.addFlags(flags)
context.startActivity(intent) context.startActivity(intent)
} }
override fun loginSSORedirect(context: Context, data: Uri?) { override fun loginSSORedirect(context: Context, data: Uri?) {
val intent = when (features.loginVersion()) { val intent = when (features.loginVariant()) {
VectorFeatures.LoginVersion.V1 -> LoginActivity.redirectIntent(context, data) VectorFeatures.LoginVariant.LEGACY -> LoginActivity.redirectIntent(context, data)
VectorFeatures.LoginVersion.V2 -> LoginActivity2.redirectIntent(context, data) VectorFeatures.LoginVariant.FTUE,
VectorFeatures.LoginVariant.FTUE_WIP -> FTUEActivity.redirectIntent(context, data)
} }
context.startActivity(intent) context.startActivity(intent)
} }
override fun softLogout(context: Context) { override fun softLogout(context: Context) {
val intent = when (features.loginVersion()) { val intent = SoftLogoutActivity.newIntent(context)
VectorFeatures.LoginVersion.V1 -> SoftLogoutActivity.newIntent(context)
VectorFeatures.LoginVersion.V2 -> SoftLogoutActivity2.newIntent(context)
}
context.startActivity(intent) context.startActivity(intent)
} }

View File

@ -66,12 +66,10 @@ class NotifiableEventResolver @Inject constructor(
return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy) return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy)
} }
val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null
when (event.getClearType()) { return when (event.getClearType()) {
EventType.MESSAGE -> { EventType.MESSAGE,
return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
}
EventType.ENCRYPTED -> { EventType.ENCRYPTED -> {
return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
} }
else -> { else -> {
// If the event can be displayed, display it as is // If the event can be displayed, display it as is
@ -79,7 +77,7 @@ class NotifiableEventResolver @Inject constructor(
// TODO Better event text display // TODO Better event text display
val bodyPreview = event.type ?: EventType.MISSING_TYPE val bodyPreview = event.type ?: EventType.MISSING_TYPE
return SimpleNotifiableEvent( SimpleNotifiableEvent(
session.myUserId, session.myUserId,
eventId = event.eventId!!, eventId = event.eventId!!,
editedEventId = timelineEvent.getEditedEventId(), 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...) // 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*/) 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]") Timber.e("## Unable to resolve room for eventId [$event]")
// Ok room is not known in store, but we can still display something // Ok room is not known in store, but we can still display something
val body = displayableEventFormatter.format(event, isDm = false, appendAuthor = false) val body = displayableEventFormatter.format(event, isDm = false, appendAuthor = false)
val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.senderInfo.disambiguatedDisplayName val senderDisplayName = event.senderInfo.disambiguatedDisplayName
return NotifiableMessageEvent( NotifiableMessageEvent(
eventId = event.root.eventId!!, eventId = event.root.eventId!!,
editedEventId = event.getEditedEventId(), editedEventId = event.getEditedEventId(),
canBeReplaced = canBeReplaced, canBeReplaced = canBeReplaced,
@ -152,26 +150,15 @@ class NotifiableEventResolver @Inject constructor(
matrixID = session.myUserId matrixID = session.myUserId
) )
} else { } else {
if (event.root.isEncrypted() && event.root.mxDecryptionResult == null) { event.attemptToDecryptIfNeeded(session)
// TODO use a global event decryptor? attache to session and that listen to new sessionId? // only convert encrypted messages to NotifiableMessageEvents
// for now decrypt sync when (event.root.getClearType()) {
try { EventType.MESSAGE -> {
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
)
} catch (e: MXCryptoError) {
}
}
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
val roomName = room.roomSummary()?.displayName ?: "" val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderInfo.disambiguatedDisplayName val senderDisplayName = event.senderInfo.disambiguatedDisplayName
return NotifiableMessageEvent( NotifiableMessageEvent(
eventId = event.root.eventId!!, eventId = event.root.eventId!!,
editedEventId = event.getEditedEventId(), editedEventId = event.getEditedEventId(),
canBeReplaced = canBeReplaced, canBeReplaced = canBeReplaced,
@ -198,6 +185,26 @@ class NotifiableEventResolver @Inject constructor(
soundName = null soundName = null
) )
} }
else -> 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) {
}
}
} }
private suspend fun TimelineEvent.fetchImageIfPresent(session: Session): Uri? { private suspend fun TimelineEvent.fetchImageIfPresent(session: Session): Uri? {

View File

@ -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_HOME_SERVER_PREFERENCE_KEY = "SETTINGS_HOME_SERVER_PREFERENCE_KEY"
const val SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_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_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_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY"
const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY" const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY"

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9.928,3.2917C8.6063,3.2917 7.4872,4.2283 7.2322,5.4975C7.194,5.6876 7.1204,5.8713 6.9925,6.0171L6.4405,6.6463C6.2665,6.8447 6.0153,6.9584 5.7514,6.9584H2.8333C1.8208,6.9584 1,7.7792 1,8.7917V18.8751C1,19.8876 1.8208,20.7084 2.8333,20.7084H21.1667C22.1792,20.7084 23,19.8876 23,18.8751V8.7917C23,7.7792 22.1792,6.9584 21.1667,6.9584H18.2486C17.9846,6.9584 17.7335,6.8447 17.5595,6.6463L17.0075,6.0171C16.8796,5.8713 16.806,5.6876 16.7678,5.4975C16.5128,4.2283 15.3937,3.2917 14.072,3.2917H9.928ZM15.6667,13.375C15.6667,15.4 14.025,17.0417 12,17.0417C9.975,17.0417 8.3333,15.4 8.3333,13.375C8.3333,11.35 9.975,9.7083 12,9.7083C14.025,9.7083 15.6667,11.35 15.6667,13.375Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M3.2917,5.5833C3.0385,5.5833 2.8333,5.7885 2.8333,6.0417C2.8333,6.2948 3.0385,6.5 3.2917,6.5H5.125C5.3781,6.5 5.5833,6.2948 5.5833,6.0417C5.5833,5.7885 5.3781,5.5833 5.125,5.5833H3.2917Z"
android:fillColor="#0DBD8B"/>
</vector>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M9.4,10.5l4.77,-8.26C13.47,2.09 12.75,2 12,2c-2.4,0 -4.6,0.85 -6.32,2.25l3.66,6.35 0.06,-0.1zM21.54,9c-0.92,-2.92 -3.15,-5.26 -6,-6.34L11.88,9h9.66zM21.8,10h-7.49l0.29,0.5 4.76,8.25C21,16.97 22,14.61 22,12c0,-0.69 -0.07,-1.35 -0.2,-2zM8.54,12l-3.9,-6.75C3.01,7.03 2,9.39 2,12c0,0.69 0.07,1.35 0.2,2h7.49l-1.15,-2zM2.46,15c0.92,2.92 3.15,5.26 6,6.34L12.12,15L2.46,15zM13.73,15l-3.9,6.76c0.7,0.15 1.42,0.24 2.17,0.24 2.4,0 4.6,-0.85 6.32,-2.25l-3.66,-6.35 -0.93,1.6z"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M17.8155,15.0336L13.2282,19.4193C11.082,21.45 7.5301,21.6024 5.4562,19.4193C3.4888,17.3484 3.4841,14.0136 5.6303,11.9829L13.8691,4.106C15.2999,2.7522 17.5435,2.535 18.984,4.0515C20.5968,5.7491 20.1298,7.9906 18.699,9.3443L10.6284,16.9682C9.913,17.645 8.7551,17.7233 8.0377,16.9682C7.3484,16.2426 7.4597,15.0625 8.1751,14.3856L12.9045,9.864"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#0DBD8B"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M20.5034,20.0373L20.8729,19.5428L20.383,19.1672L8.7444,10.2443C8.2017,9.8282 7.4428,9.8451 6.9192,10.285L3.2647,13.3548L3.0417,13.5421V13.8333V18.6667C3.0417,19.9323 4.0677,20.9583 5.3333,20.9583H18.6667C19.419,20.9583 20.0866,20.5952 20.5034,20.0373ZM2.625,5.3333C2.625,3.8376 3.8376,2.625 5.3333,2.625H18.6667C20.1624,2.625 21.375,3.8376 21.375,5.3333V18.6667C21.375,20.1624 20.1624,21.375 18.6667,21.375H5.3333C3.8376,21.375 2.625,20.1624 2.625,18.6667V5.3333ZM13.875,8.25C13.875,9.7458 15.0876,10.9583 16.5833,10.9583C16.9896,10.9583 17.3765,10.8685 17.724,10.707C18.6485,10.2772 19.2917,9.3393 19.2917,8.25C19.2917,6.7542 18.0791,5.5417 16.5833,5.5417C15.0876,5.5417 13.875,6.7542 13.875,8.25Z"
android:strokeWidth="1.25"
android:fillColor="#0DBD8B"
android:strokeColor="#0DBD8B"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,2C8.13,2 5,5.2152 5,9.1905C5,13.4741 9.42,19.3806 11.24,21.6302C11.64,22.1233 12.37,22.1233 12.77,21.6302C14.58,19.3806 19,13.4741 19,9.1905C19,5.2152 15.87,2 12,2ZM12,11.7586C10.62,11.7586 9.5,10.6081 9.5,9.1905C9.5,7.773 10.62,6.6225 12,6.6225C13.38,6.6225 14.5,7.773 14.5,9.1905C14.5,10.6081 13.38,11.7586 12,11.7586Z"
android:fillColor="#0DBD8B"/>
</vector>

View File

@ -5,6 +5,6 @@
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:pathData="M10.5,2C10.2239,2 10,2.2239 10,2.5V22H14V2.5C14,2.2239 13.7761,2 13.5,2H10.5ZM3,9.5C3,9.2239 3.2239,9 3.5,9H6.5C6.7761,9 7,9.2239 7,9.5V22H3V9.5ZM17,13.5C17,13.2239 17.2239,13 17.5,13H20.5C20.7761,13 21,13.2239 21,13.5V22H17V13.5Z" android:pathData="M10.5,2C10.2239,2 10,2.2239 10,2.5V22H14V2.5C14,2.2239 13.7761,2 13.5,2H10.5ZM3,9.5C3,9.2239 3.2239,9 3.5,9H6.5C6.7761,9 7,9.2239 7,9.5V22H3V9.5ZM17,13.5C17,13.2239 17.2239,13 17.5,13H20.5C20.7761,13 21,13.2239 21,13.5V22H17V13.5Z"
android:fillColor="#FFFFFF" android:fillColor="#0DBD8B"
android:fillType="evenOdd"/> android:fillType="evenOdd"/>
</vector> </vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10.1479,21.321C5.7873,20.4596 2.4987,16.6135 2.4987,12C2.4987,6.7526 6.7526,2.4987 12,2.4987C16.6316,2.4987 20.4897,5.8131 21.331,10.1992C18.2322,9.4198 14.864,10.147 12.4944,12.5383C10.1572,14.8967 9.4261,18.2332 10.1479,21.321ZM20.2524,13.0424L12.9933,20.3015C12.6064,18.222 13.1681,16.1257 14.6151,14.6655C16.0754,13.1918 18.176,12.6299 20.2524,13.0424Z"
android:strokeLineJoin="round"
android:strokeWidth="0.997378"
android:fillColor="#0DBD8B"
android:strokeColor="#0DBD8B"
android:strokeLineCap="round"/>
</vector>

View File

@ -108,9 +108,9 @@
<ImageButton <ImageButton
android:id="@+id/attachmentButton" android:id="@+id/attachmentButton"
android:layout_width="52dp" android:layout_width="@dimen/composer_attachment_size"
android:layout_height="52dp" android:layout_height="@dimen/composer_attachment_size"
android:layout_margin="1dp" android:layout_margin="@dimen/composer_attachment_margin"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/option_send_files" android:contentDescription="@string/option_send_files"
android:src="@drawable/ic_attachment" android:src="@drawable/ic_attachment"
@ -166,7 +166,7 @@
<ImageButton <ImageButton
android:id="@+id/sendButton" android:id="@+id/sendButton"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="@dimen/composer_min_height"
android:layout_marginEnd="2dp" android:layout_marginEnd="2dp"
android:background="@drawable/bg_send" android:background="@drawable/bg_send"
android:contentDescription="@string/send" android:contentDescription="@string/send"

View File

@ -121,9 +121,9 @@
<ImageButton <ImageButton
android:id="@+id/attachmentButton" android:id="@+id/attachmentButton"
android:layout_width="52dp" android:layout_width="@dimen/composer_attachment_size"
android:layout_height="52dp" android:layout_height="@dimen/composer_attachment_size"
android:layout_margin="1dp" android:layout_margin="@dimen/composer_attachment_margin"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/option_send_files" android:contentDescription="@string/option_send_files"
android:src="@drawable/ic_attachment" android:src="@drawable/ic_attachment"
@ -178,7 +178,7 @@
<ImageButton <ImageButton
android:id="@+id/sendButton" android:id="@+id/sendButton"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="@dimen/composer_min_height"
android:layout_marginEnd="2dp" android:layout_marginEnd="2dp"
android:background="@drawable/bg_send" android:background="@drawable/bg_send"
android:contentDescription="@string/send" android:contentDescription="@string/send"

View File

@ -1,199 +1,112 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="@dimen/composer_min_height"
android:paddingStart="8dp" android:background="?android:colorBackground">
android:paddingEnd="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_attachment_type_selector"
android:orientation="vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp"
tools:ignore="UselessParent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:baselineAligned="false"
android:orientation="horizontal"
android:weightSum="3">
<LinearLayout
android:id="@+id/attachmentCameraButtonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton <ImageButton
android:id="@+id/attachmentCameraButton" android:id="@+id/attachmentCloseButton"
style="@style/AttachmentTypeSelectorButton" android:layout_width="@dimen/composer_attachment_size"
android:contentDescription="@string/attachment_type_camera" android:layout_height="@dimen/composer_attachment_size"
android:src="@drawable/ic_attachment_camera_white_24dp" android:layout_margin="@dimen/composer_attachment_margin"
tools:background="?colorPrimary" /> android:background="@null"
android:contentDescription="@string/action_close"
android:src="@drawable/ic_attachment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:rotation="135" />
<TextView <HorizontalScrollView
style="@style/AttachmentTypeSelectorLabel" android:layout_width="0dp"
android:importantForAccessibility="no" android:layout_height="wrap_content"
android:text="@string/attachment_type_camera" /> android:layout_marginStart="4dp"
android:scrollbars="none"
</LinearLayout> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/attachmentCloseButton"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout <LinearLayout
android:id="@+id/attachmentGalleryButtonContainer" android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:orientation="horizontal">
android:gravity="center"
android:orientation="vertical">
<ImageButton <ImageButton
android:id="@+id/attachmentGalleryButton" android:id="@+id/attachmentGalleryButton"
style="@style/AttachmentTypeSelectorButton" android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/attachment_type_gallery" android:contentDescription="@string/attachment_type_gallery"
android:src="@drawable/ic_attachment_gallery_white_24dp" android:src="@drawable/ic_attachment_gallery" />
tools:background="?colorPrimary" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_gallery" />
</LinearLayout>
<LinearLayout
android:id="@+id/attachmentFileButtonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/attachmentFileButton"
style="@style/AttachmentTypeSelectorButton"
android:contentDescription="@string/attachment_type_file"
android:src="@drawable/ic_attachment_file_white_24dp"
tools:background="?colorPrimary" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_file" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:baselineAligned="false"
android:orientation="horizontal"
android:weightSum="3">
<LinearLayout
android:id="@+id/attachmentAudioButtonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/attachmentAudioButton"
style="@style/AttachmentTypeSelectorButton"
android:contentDescription="@string/attachment_type_audio"
android:src="@drawable/ic_attachment_audio_white_24dp"
tools:background="?colorPrimary" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_audio" />
</LinearLayout>
<LinearLayout
android:id="@+id/attachmentContactButtonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/attachmentContactButton"
style="@style/AttachmentTypeSelectorButton"
android:contentDescription="@string/attachment_type_contact"
android:src="@drawable/ic_attachment_contact_white_24dp"
tools:background="?colorPrimary" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_contact" />
</LinearLayout>
<LinearLayout
android:id="@+id/attachmentStickersButtonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton <ImageButton
android:id="@+id/attachmentStickersButton" android:id="@+id/attachmentStickersButton"
style="@style/AttachmentTypeSelectorButton" android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginStart="2dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/attachment_type_sticker" android:contentDescription="@string/attachment_type_sticker"
android:src="@drawable/ic_attachment_stickers_white_24dp" android:src="@drawable/ic_attachment_sticker"
tools:background="?colorPrimary" /> app:tint="?colorPrimary" />
<TextView <ImageButton
style="@style/AttachmentTypeSelectorLabel" android:id="@+id/attachmentFileButton"
android:importantForAccessibility="no" android:layout_width="@dimen/layout_touch_size"
android:text="@string/attachment_type_sticker" /> android:layout_height="@dimen/layout_touch_size"
android:layout_marginStart="2dp"
</LinearLayout> android:background="?android:attr/selectableItemBackground"
</LinearLayout> android:contentDescription="@string/attachment_type_file"
android:src="@drawable/ic_attachment_file"
<LinearLayout app:tint="?colorPrimary" />
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:baselineAligned="false"
android:orientation="horizontal"
android:weightSum="3">
<LinearLayout
android:id="@+id/attachmentPollButtonContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton <ImageButton
android:id="@+id/attachmentPollButton" android:id="@+id/attachmentPollButton"
style="@style/AttachmentTypeSelectorButton" android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginStart="2dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/attachment_type_poll" android:contentDescription="@string/attachment_type_poll"
android:src="@drawable/ic_attachment_poll_white_24dp" android:src="@drawable/ic_attachment_poll"
tools:background="?colorPrimary" /> app:tint="?colorPrimary" />
<ImageButton
android:id="@+id/attachmentCameraButton"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginStart="2dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/attachment_type_camera"
android:src="@drawable/ic_attachment_camera"
app:tint="?colorPrimary" />
<!-- TODO. Request for new icon -->
<ImageButton
android:id="@+id/attachmentAudioButton"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginStart="2dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/attachment_type_audio"
android:src="@drawable/ic_attachment_audio_white_24dp"
app:tint="?colorPrimary" />
<!-- TODO. Request for new icon -->
<ImageButton
android:id="@+id/attachmentContactButton"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginStart="2dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/attachment_type_contact"
android:src="@drawable/ic_attachment_contact_white_24dp"
app:tint="?colorPrimary" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_poll" />
</LinearLayout> </LinearLayout>
</LinearLayout>
</LinearLayout> </HorizontalScrollView>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -34,7 +34,7 @@
<ImageButton <ImageButton
android:id="@+id/voiceMessageSendButton" android:id="@+id/voiceMessageSendButton"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="@dimen/composer_min_height"
android:background="@drawable/bg_send" android:background="@drawable/bg_send"
android:contentDescription="@string/send" android:contentDescription="@string/send"
android:scaleType="center" android:scaleType="center"
@ -232,8 +232,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="84dp" android:layout_marginBottom="84dp"
android:visibility="gone"
android:accessibilityLiveRegion="polite" android:accessibilityLiveRegion="polite"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"