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

@ -2,11 +2,13 @@ name: Move new issues onto Issue triage board
on:
issues:
types: [opened]
types: [ opened ]
jobs:
automate-project-columns:
runs-on: ubuntu-latest
if: |
github.repository == 'vector-im/element-android' # Skip in forks
steps:
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
with:

View file

@ -2,12 +2,14 @@ name: Move labelled issues to correct boards and columns
on:
issues:
types: [labeled]
types: [ labeled ]
jobs:
move_needs_info_issues:
name: X-Needs-Info issues to Need info column on triage board
runs-on: ubuntu-latest
if: |
github.repository == 'vector-im/element-android' # Skip in forks
steps:
- uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338
with:
@ -19,15 +21,16 @@ jobs:
add_priority_design_issues_to_project:
name: P1 X-Needs-Design to Design project board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
(contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
if: |
github.repository == 'vector-im/element-android' && # Skip in forks
contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
(contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent'))
contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent'))
steps:
- uses: octokit/graphql-action@v2.x
id: add_to_project
@ -47,36 +50,38 @@ jobs:
PROJECT_ID: "PN_kwDOAM0swc0sUA"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
# delight_issues_to_board:
# name: Spaces issues to new Delight project board
# runs-on: ubuntu-latest
# if: >
# contains(github.event.issue.labels.*.name, 'A-Spaces') ||
# contains(github.event.issue.labels.*.name, 'A-Space-Settings') ||
# contains(github.event.issue.labels.*.name, 'A-Subspaces')
# steps:
# - uses: octokit/graphql-action@v2.x
# with:
# headers: '{"GraphQL-Features": "projects_next_graphql"}'
# query: |
# mutation add_to_project($projectid:ID!,$contentid:ID!) {
# addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
# projectNextItem {
# id
# }
# }
# }
# projectid: ${{ env.PROJECT_ID }}
# contentid: ${{ github.event.issue.node_id }}
# env:
# PROJECT_ID: "PN_kwDOAM0swc1HvQ"
# GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
# delight_issues_to_board:
# name: Spaces issues to new Delight project board
# runs-on: ubuntu-latest
# if: |
# github.repository == 'vector-im/element-android' && # Skip in forks
# contains(github.event.issue.labels.*.name, 'A-Spaces') ||
# contains(github.event.issue.labels.*.name, 'A-Space-Settings') ||
# contains(github.event.issue.labels.*.name, 'A-Subspaces')
# steps:
# - uses: octokit/graphql-action@v2.x
# with:
# headers: '{"GraphQL-Features": "projects_next_graphql"}'
# query: |
# mutation add_to_project($projectid:ID!,$contentid:ID!) {
# addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
# projectNextItem {
# id
# }
# }
# }
# projectid: ${{ env.PROJECT_ID }}
# contentid: ${{ github.event.issue.node_id }}
# env:
# PROJECT_ID: "PN_kwDOAM0swc1HvQ"
# GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_voice-message_issues:
name: A-Voice Messages to voice message board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'A-Voice Messages')
if: |
github.repository == 'vector-im/element-android' && # Skip in forks
contains(github.event.issue.labels.*.name, 'A-Voice Messages')
steps:
- uses: octokit/graphql-action@v2.x
with:
@ -98,8 +103,9 @@ jobs:
move_threads_issues:
name: A-Threads to Thread board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'A-Threads')
if: |
github.repository == 'vector-im/element-android' && # Skip in forks
contains(github.event.issue.labels.*.name, 'A-Threads')
steps:
- uses: octokit/graphql-action@v2.x
with:
@ -121,8 +127,9 @@ jobs:
move_message_bubbles_issues:
name: A-Message-Bubbles to Message bubbles board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'A-Message-Bubbles')
if: |
github.repository == 'vector-im/element-android' && # Skip in forks
contains(github.event.issue.labels.*.name, 'A-Message-Bubbles')
steps:
- uses: octokit/graphql-action@v2.x
with:

View file

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

View file

@ -2,28 +2,29 @@ name: Move P1 bugs to boards
on:
issues:
types: [labeled, unlabeled]
types: [ labeled, unlabeled ]
jobs:
p1_issues_to_team_workboard:
runs-on: ubuntu-latest
if: >
(!contains(github.event.issue.labels.*.name, 'A-E2EE') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') &&
!contains(github.event.issue.labels.*.name, 'A-Spaces') &&
!contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') &&
!contains(github.event.issue.labels.*.name, 'A-Subspaces')) &&
(contains(github.event.issue.labels.*.name, 'T-Defect') &&
contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
if: |
github.repository == 'vector-im/element-android' && # Skip in forks
(!contains(github.event.issue.labels.*.name, 'A-E2EE') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') &&
!contains(github.event.issue.labels.*.name, 'A-Spaces') &&
!contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') &&
!contains(github.event.issue.labels.*.name, 'A-Subspaces')) &&
(contains(github.event.issue.labels.*.name, 'T-Defect') &&
contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent'))
contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent'))
steps:
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
with:
@ -33,20 +34,21 @@ jobs:
P1_issues_to_crypto_team_workboard:
runs-on: ubuntu-latest
if: >
(contains(github.event.issue.labels.*.name, 'A-E2EE') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) &&
(contains(github.event.issue.labels.*.name, 'T-Defect') &&
contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
if: |
github.repository == 'vector-im/element-android' && # Skip in forks
(contains(github.event.issue.labels.*.name, 'A-E2EE') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) &&
(contains(github.event.issue.labels.*.name, 'T-Defect') &&
contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent'))
contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent'))
steps:
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
with:

View file

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

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
distributionPath=wrapper/dists
distributionSha256Sum=dd54e87b4d7aa8ff3c6afb0f7805aa121d4b70bca55b8c9b1b896eb103184582
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-all.zip
distributionSha256Sum=c9490e938b221daf0094982288e4038deed954a3f12fb54cbf270ddf4e37d879
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View file

@ -46,4 +46,9 @@
<dimen name="menu_item_icon_size">24dp</dimen>
<dimen name="menu_item_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>

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>> {
return session.accountDataService().getLiveRoomAccountDataEvents(types).asFlow()
.startWith(session.coroutineDispatchers.io) {

View file

@ -160,7 +160,7 @@ dependencies {
implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.39'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40'
testImplementation libs.tests.junit
testImplementation 'org.robolectric:robolectric:4.7.3'

View file

@ -429,7 +429,17 @@ internal class DefaultCryptoService @Inject constructor(
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
}
if (isStarted()) {
// There is a limit of to_device events returned per sync.
// If we are in a case of such limited to_device sync we can't try to generate/upload
// new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate
// the old otk too early. In this case we want to wait for the pending to_device before doing anything
// As per spec:
// If there is a large queue of send-to-device messages, the server should limit the number sent in each /sync response.
// 100 messages is recommended as a reasonable limit.
// The limit is not part of the spec, so it's probably safer to handle that when there are no more to_device ( so we are sure
// that there are no pending to_device
val toDevices = syncResponse.toDevice?.events.orEmpty()
if (isStarted() && toDevices.isEmpty()) {
// Make sure we process to-device messages before generating new one-time-keys #2782
deviceListManager.refreshOutdatedDeviceLists()
// The presence of device_unused_fallback_key_types indicates that the server supports fallback keys.

View file

@ -109,18 +109,23 @@ internal class FileUploader @Inject constructor(
filename: String?,
mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val inputStream = withContext(Dispatchers.IO) {
context.contentResolver.openInputStream(uri)
} ?: throw FileNotFoundException()
val workingFile = temporaryFileCreator.create()
workingFile.outputStream().use {
inputStream.copyTo(it)
}
val workingFile = context.copyUriToTempFile(uri)
return uploadFile(workingFile, filename, mimeType, progressListener).also {
tryOrNull { workingFile.delete() }
}
}
private suspend fun Context.copyUriToTempFile(uri: Uri): File {
return withContext(Dispatchers.IO) {
val inputStream = contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
val workingFile = temporaryFileCreator.create()
workingFile.outputStream().use {
inputStream.copyTo(it)
}
workingFile
}
}
private suspend fun upload(uploadBody: RequestBody,
filename: String?,
progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {

View file

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

View file

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

View file

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

View file

@ -140,7 +140,7 @@ android {
buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\""
resValue "string", "build_number", "\"${buildNumber}\""
buildConfigField "im.vector.app.features.VectorFeatures.LoginVersion", "LOGIN_VERSION", "im.vector.app.features.VectorFeatures.LoginVersion.V1"
buildConfigField "im.vector.app.features.VectorFeatures.LoginVariant", "LOGIN_VARIANT", "im.vector.app.features.VectorFeatures.LoginVariant.LEGACY"
buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping"
@ -362,7 +362,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.39'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40'
// FlowBinding
implementation libs.github.flowBinding

View file

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

View file

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

View file

@ -137,7 +137,7 @@
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".features.login2.LoginActivity2"
android:name=".features.ftue.FTUEActivity"
android:launchMode="singleTask"
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
get() = ViewModelProvider(this, viewModelFactory)
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.stream()
.onEach {

View file

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

View file

@ -26,24 +26,18 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewAnimationUtils
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.OvershootInterpolator
import android.view.animation.ScaleAnimation
import android.view.animation.TranslateAnimation
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.core.view.doOnNextLayout
import androidx.core.view.isVisible
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import im.vector.app.R
import im.vector.app.core.extensions.getMeasurements
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.utils.PERMISSIONS_EMPTY
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback
import kotlin.math.max
private const val ANIMATION_DURATION = 250
@ -52,17 +46,16 @@ private const val ANIMATION_DURATION = 250
* This class is the view presenting choices for picking attachments.
* It will return result through [Callback].
*/
class AttachmentTypeSelectorView(context: Context,
inflater: LayoutInflater,
var callback: Callback?) :
PopupWindow(context) {
var callback: Callback?
) : PopupWindow(context) {
interface Callback {
fun onTypeSelected(type: Type)
}
private val iconColorGenerator = ColorGenerator.MATERIAL
private val views: ViewAttachmentTypeSelectorBinding
private var anchor: View? = null
@ -85,35 +78,40 @@ class AttachmentTypeSelectorView(context: Context,
inputMethodMode = INPUT_METHOD_NOT_NEEDED
isFocusable = true
isTouchable = true
views.attachmentCloseButton.onClick {
dismiss()
}
}
fun show(anchor: View, isKeyboardOpen: Boolean) {
private fun animateOpen() {
views.attachmentCloseButton.animate()
.setDuration(200)
.rotation(135f)
}
private fun animateClose() {
views.attachmentCloseButton.animate()
.setDuration(200)
.rotation(0f)
}
fun show(anchor: View) {
animateOpen()
this.anchor = anchor
val anchorCoordinates = IntArray(2)
anchor.getLocationOnScreen(anchorCoordinates)
if (isKeyboardOpen) {
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height)
} else {
val contentViewHeight = if (contentView.height == 0) {
contentView.getMeasurements().second
} else {
contentView.height
}
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight)
}
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1])
contentView.doOnNextLayout {
animateWindowInCircular(anchor, contentView)
}
animateButtonIn(views.attachmentGalleryButton, ANIMATION_DURATION / 2)
animateButtonIn(views.attachmentCameraButton, ANIMATION_DURATION / 4)
animateButtonIn(views.attachmentFileButton, ANIMATION_DURATION / 2)
animateButtonIn(views.attachmentAudioButton, 0)
animateButtonIn(views.attachmentContactButton, ANIMATION_DURATION / 4)
animateButtonIn(views.attachmentStickersButton, ANIMATION_DURATION / 2)
animateButtonIn(views.attachmentPollButton, ANIMATION_DURATION / 4)
}
override fun dismiss() {
animateClose()
val capturedAnchor = anchor
if (capturedAnchor != null) {
animateWindowOutCircular(capturedAnchor, contentView)
@ -124,28 +122,18 @@ class AttachmentTypeSelectorView(context: Context,
fun setAttachmentVisibility(type: Type, isVisible: Boolean) {
when (type) {
Type.CAMERA -> views.attachmentCameraButtonContainer
Type.GALLERY -> views.attachmentGalleryButtonContainer
Type.FILE -> views.attachmentFileButtonContainer
Type.STICKER -> views.attachmentStickersButtonContainer
Type.AUDIO -> views.attachmentAudioButtonContainer
Type.CONTACT -> views.attachmentContactButtonContainer
Type.POLL -> views.attachmentPollButtonContainer
Type.CAMERA -> views.attachmentCameraButton
Type.GALLERY -> views.attachmentGalleryButton
Type.FILE -> views.attachmentFileButton
Type.STICKER -> views.attachmentStickersButton
Type.AUDIO -> views.attachmentAudioButton
Type.CONTACT -> views.attachmentContactButton
Type.POLL -> views.attachmentPollButton
}.let {
it.isVisible = isVisible
}
}
private fun animateButtonIn(button: View, delay: Int) {
val animation = AnimationSet(true)
val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f)
animation.addAnimation(scale)
animation.interpolator = OvershootInterpolator(1f)
animation.duration = ANIMATION_DURATION.toLong()
animation.startOffset = delay.toLong()
button.startAnimation(animation)
}
private fun animateWindowInCircular(anchor: View, contentView: View) {
val coordinates = getClickCoordinates(anchor, contentView)
val animator = ViewAnimationUtils.createCircularReveal(contentView,
@ -157,12 +145,6 @@ class AttachmentTypeSelectorView(context: Context,
animator.start()
}
private fun animateWindowInTranslate(contentView: View) {
val animation = TranslateAnimation(0f, 0f, contentView.height.toFloat(), 0f)
animation.duration = ANIMATION_DURATION.toLong()
getContentView().startAnimation(animation)
}
private fun animateWindowOutCircular(anchor: View, contentView: View) {
val coordinates = getClickCoordinates(anchor, contentView)
val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
@ -207,7 +189,6 @@ class AttachmentTypeSelectorView(context: Context,
}
private fun ImageButton.configure(type: Type): ImageButton {
this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal))
this.setOnClickListener(TypeClickListener(type))
return this
}

View file

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

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

View file

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

View file

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

View file

@ -44,7 +44,7 @@ class EncryptionItemFactory @Inject constructor(
if (!event.root.isStateEvent()) {
return null
}
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm
val algorithm = event.root.content.toModel<EncryptionEventContent>()?.algorithm
val informationData = informationDataFactory.create(params)
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<*>? {
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 roomLink = session.permalinkService().createRoomPermalink(predecessorId) ?: return null
val text = span {

View file

@ -54,67 +54,83 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
params.rootThreadEventId,
params.isFromThreadTimeline())
}
when (event.root.getClearType()) {
// Message itemsX
EventType.STICKER,
EventType.POLL_START,
EventType.MESSAGE -> messageItemFactory.create(params)
EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_AVATAR,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_JOIN_RULES,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.STATE_ROOM_SERVER_ACL,
EventType.STATE_ROOM_GUEST_ACCESS,
EventType.REDACTION,
EventType.STATE_ROOM_ALIASES,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC,
EventType.CALL_CANDIDATES,
EventType.CALL_REPLACES,
EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE,
EventType.REACTION,
EventType.STATE_SPACE_CHILD,
EventType.STATE_SPACE_PARENT,
EventType.STATE_ROOM_POWER_LEVELS,
EventType.POLL_RESPONSE,
EventType.POLL_END -> noticeItemFactory.create(params)
EventType.STATE_ROOM_WIDGET_LEGACY,
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params)
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params)
// State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params)
// Calls
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_REJECT,
EventType.CALL_ANSWER -> callItemFactory.create(params)
// Crypto
EventType.ENCRYPTED -> {
if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(params)
} else {
encryptedItemFactory.create(params)
// Manage state event differently, to check validity
if (event.root.isStateEvent()) {
// state event are not e2e
when (event.root.type) {
EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_AVATAR,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_JOIN_RULES,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.STATE_ROOM_SERVER_ACL,
EventType.STATE_ROOM_GUEST_ACCESS,
EventType.STATE_ROOM_ALIASES,
EventType.STATE_SPACE_CHILD,
EventType.STATE_SPACE_PARENT,
EventType.STATE_ROOM_POWER_LEVELS -> {
noticeItemFactory.create(params)
}
EventType.STATE_ROOM_WIDGET_LEGACY,
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params)
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params)
// State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params)
// Unhandled state event types
else -> {
// Should only happen when shouldShowHiddenEvents() settings is ON
Timber.v("State event type ${event.root.type} not handled")
defaultItemFactory.create(params)
}
}
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE -> {
verificationConclusionItemFactory.create(params)
}
// Unhandled event types
else -> {
// Should only happen when shouldShowHiddenEvents() settings is ON
Timber.v("Type ${event.root.getClearType()} not handled")
defaultItemFactory.create(params)
} else {
when (event.root.getClearType()) {
// Message itemsX
EventType.STICKER,
EventType.POLL_START,
EventType.MESSAGE -> messageItemFactory.create(params)
EventType.REDACTION,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC,
EventType.CALL_CANDIDATES,
EventType.CALL_REPLACES,
EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE,
EventType.REACTION,
EventType.POLL_RESPONSE,
EventType.POLL_END -> noticeItemFactory.create(params)
// Calls
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_REJECT,
EventType.CALL_ANSWER -> callItemFactory.create(params)
// Crypto
EventType.ENCRYPTED -> {
if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(params)
} else {
encryptedItemFactory.create(params)
}
}
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE -> {
verificationConclusionItemFactory.create(params)
}
// Unhandled event types
else -> {
// Should only happen when shouldShowHiddenEvents() settings is ON
Timber.v("Type ${event.root.getClearType()} not handled")
defaultItemFactory.create(params)
}
}
}
} catch (throwable: Throwable) {

View file

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

View file

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

View file

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

View file

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

View file

@ -356,9 +356,6 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), ToolbarCo
private const val EXTRA_CONFIG = "EXTRA_CONFIG"
// Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string
const val VECTOR_REDIRECT_URL = "element://connect"
fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
return Intent(context, LoginActivity::class.java).apply {
putExtra(EXTRA_CONFIG, loginConfig)

View file

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

View file

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

View file

@ -32,4 +32,9 @@ class SSORedirectRouterActivity : AppCompatActivity() {
navigator.loginSSORedirect(this, intent.data)
finish()
}
companion object {
// Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string
const val VECTOR_REDIRECT_URL = "element://connect"
}
}

View file

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

View file

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

View file

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

View file

@ -24,6 +24,7 @@ import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginSsoOnly2Binding
import im.vector.app.features.login.SSORedirectRouterActivity
import javax.inject.Inject
/**
@ -51,7 +52,7 @@ class LoginSsoOnlyFragment2 @Inject constructor() : AbstractSSOLoginFragment2<Fr
private fun submit() = withState(loginViewModel) { state ->
loginViewModel.getSsoUrl(
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL,
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
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.FragmentLoginAccountCreatedBinding
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.ftue.FTUEActivity
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.login2.AbstractLoginFragment2
import im.vector.app.features.login2.LoginAction2
import im.vector.app.features.login2.LoginActivity2
import im.vector.app.features.login2.LoginViewState2
import org.matrix.android.sdk.api.util.MatrixItem
import java.util.UUID
@ -130,7 +130,7 @@ class AccountCreatedFragment @Inject constructor(
private fun invalidateState(state: AccountCreatedViewState) {
// Ugly hack...
(activity as? LoginActivity2)?.setIsLoading(state.isLoading)
(activity as? FTUEActivity)?.setIsLoading(state.isLoading)
views.loginAccountCreatedSubtitle.text = getString(R.string.login_account_created_subtitle, state.userId)

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

View file

@ -66,12 +66,10 @@ class NotifiableEventResolver @Inject constructor(
return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy)
}
val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null
when (event.getClearType()) {
EventType.MESSAGE -> {
return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
}
return when (event.getClearType()) {
EventType.MESSAGE,
EventType.ENCRYPTED -> {
return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
}
else -> {
// If the event can be displayed, display it as is
@ -79,7 +77,7 @@ class NotifiableEventResolver @Inject constructor(
// TODO Better event text display
val bodyPreview = event.type ?: EventType.MISSING_TYPE
return SimpleNotifiableEvent(
SimpleNotifiableEvent(
session.myUserId,
eventId = event.eventId!!,
editedEventId = timelineEvent.getEditedEventId(),
@ -126,18 +124,18 @@ class NotifiableEventResolver @Inject constructor(
}
}
private suspend fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent {
private suspend fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? {
// The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
if (room == null) {
return if (room == null) {
Timber.e("## Unable to resolve room for eventId [$event]")
// Ok room is not known in store, but we can still display something
val body = displayableEventFormatter.format(event, isDm = false, appendAuthor = false)
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
return NotifiableMessageEvent(
NotifiableMessageEvent(
eventId = event.root.eventId!!,
editedEventId = event.getEditedEventId(),
canBeReplaced = canBeReplaced,
@ -152,51 +150,60 @@ class NotifiableEventResolver @Inject constructor(
matrixID = session.myUserId
)
} else {
if (event.root.isEncrypted() && event.root.mxDecryptionResult == null) {
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync
try {
val result = session.cryptoService().decryptEvent(event.root, event.root.roomId + UUID.randomUUID().toString())
event.root.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
event.attemptToDecryptIfNeeded(session)
// only convert encrypted messages to NotifiableMessageEvents
when (event.root.getClearType()) {
EventType.MESSAGE -> {
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
NotifiableMessageEvent(
eventId = event.root.eventId!!,
editedEventId = event.getEditedEventId(),
canBeReplaced = canBeReplaced,
timestamp = event.root.originServerTs ?: 0,
noisy = isNoisy,
senderName = senderDisplayName,
senderId = event.root.senderId,
body = body,
imageUri = event.fetchImageIfPresent(session),
roomId = event.root.roomId!!,
roomName = roomName,
roomIsDirect = room.roomSummary()?.isDirect ?: false,
roomAvatarPath = session.contentUrlResolver()
.resolveThumbnail(room.roomSummary()?.avatarUrl,
250,
250,
ContentUrlResolver.ThumbnailMethod.SCALE),
senderAvatarPath = session.contentUrlResolver()
.resolveThumbnail(event.senderInfo.avatarUrl,
250,
250,
ContentUrlResolver.ThumbnailMethod.SCALE),
matrixID = session.myUserId,
soundName = null
)
} catch (e: MXCryptoError) {
}
else -> null
}
}
}
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
return NotifiableMessageEvent(
eventId = event.root.eventId!!,
editedEventId = event.getEditedEventId(),
canBeReplaced = canBeReplaced,
timestamp = event.root.originServerTs ?: 0,
noisy = isNoisy,
senderName = senderDisplayName,
senderId = event.root.senderId,
body = body,
imageUri = event.fetchImageIfPresent(session),
roomId = event.root.roomId!!,
roomName = roomName,
roomIsDirect = room.roomSummary()?.isDirect ?: false,
roomAvatarPath = session.contentUrlResolver()
.resolveThumbnail(room.roomSummary()?.avatarUrl,
250,
250,
ContentUrlResolver.ThumbnailMethod.SCALE),
senderAvatarPath = session.contentUrlResolver()
.resolveThumbnail(event.senderInfo.avatarUrl,
250,
250,
ContentUrlResolver.ThumbnailMethod.SCALE),
matrixID = session.myUserId,
soundName = null
)
private fun TimelineEvent.attemptToDecryptIfNeeded(session: Session) {
if (root.isEncrypted() && root.mxDecryptionResult == null) {
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync
try {
val result = session.cryptoService().decryptEvent(root, root.roomId + UUID.randomUUID().toString())
root.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
}
}
}

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_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY"
const val SETTINGS_DISCOVERY_PREFERENCE_KEY = "SETTINGS_DISCOVERY_PREFERENCE_KEY"
const val SETTINGS_EMAILS_AND_PHONE_NUMBERS_PREFERENCE_KEY = "SETTINGS_EMAILS_AND_PHONE_NUMBERS_PREFERENCE_KEY"
const val SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY"
const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY"

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">
<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:fillColor="#FFFFFF"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
</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
android:id="@+id/attachmentButton"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_margin="1dp"
android:layout_width="@dimen/composer_attachment_size"
android:layout_height="@dimen/composer_attachment_size"
android:layout_margin="@dimen/composer_attachment_margin"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/option_send_files"
android:src="@drawable/ic_attachment"
@ -166,7 +166,7 @@
<ImageButton
android:id="@+id/sendButton"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_height="@dimen/composer_min_height"
android:layout_marginEnd="2dp"
android:background="@drawable/bg_send"
android:contentDescription="@string/send"

View file

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

View file

@ -1,199 +1,112 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp">
android:layout_height="@dimen/composer_min_height"
android:background="?android:colorBackground">
<LinearLayout
android:layout_width="match_parent"
<ImageButton
android:id="@+id/attachmentCloseButton"
android:layout_width="@dimen/composer_attachment_size"
android:layout_height="@dimen/composer_attachment_size"
android:layout_margin="@dimen/composer_attachment_margin"
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" />
<HorizontalScrollView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/bg_attachment_type_selector"
android:orientation="vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp"
tools:ignore="UselessParent">
android:layout_marginStart="4dp"
android:scrollbars="none"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/attachmentCloseButton"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:baselineAligned="false"
android:orientation="horizontal"
android:weightSum="3">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<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
android:id="@+id/attachmentGalleryButton"
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:src="@drawable/ic_attachment_gallery" />
<ImageButton
android:id="@+id/attachmentCameraButton"
style="@style/AttachmentTypeSelectorButton"
android:contentDescription="@string/attachment_type_camera"
android:src="@drawable/ic_attachment_camera_white_24dp"
tools:background="?colorPrimary" />
<ImageButton
android:id="@+id/attachmentStickersButton"
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:src="@drawable/ic_attachment_sticker"
app:tint="?colorPrimary" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_camera" />
<ImageButton
android:id="@+id/attachmentFileButton"
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_file"
android:src="@drawable/ic_attachment_file"
app:tint="?colorPrimary" />
</LinearLayout>
<ImageButton
android:id="@+id/attachmentPollButton"
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:src="@drawable/ic_attachment_poll"
app:tint="?colorPrimary" />
<LinearLayout
android:id="@+id/attachmentGalleryButtonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<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" />
<ImageButton
android:id="@+id/attachmentGalleryButton"
style="@style/AttachmentTypeSelectorButton"
android:contentDescription="@string/attachment_type_gallery"
android:src="@drawable/ic_attachment_gallery_white_24dp"
tools:background="?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" />
<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>
<!-- 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" />
</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">
</HorizontalScrollView>
<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
android:id="@+id/attachmentStickersButton"
style="@style/AttachmentTypeSelectorButton"
android:contentDescription="@string/attachment_type_sticker"
android:src="@drawable/ic_attachment_stickers_white_24dp"
tools:background="?colorPrimary" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_sticker" />
</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/attachmentPollButtonContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageButton
android:id="@+id/attachmentPollButton"
style="@style/AttachmentTypeSelectorButton"
android:contentDescription="@string/attachment_type_poll"
android:src="@drawable/ic_attachment_poll_white_24dp"
tools:background="?colorPrimary" />
<TextView
style="@style/AttachmentTypeSelectorLabel"
android:importantForAccessibility="no"
android:text="@string/attachment_type_poll" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

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