Merge branch 'develop' into feature/aris/threads
# Conflicts: # library/ui-styles/src/main/res/values/dimens.xml
This commit is contained in:
commit
694b8de034
|
@ -2,11 +2,13 @@ name: Move new issues onto Issue triage board
|
||||||
|
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened]
|
types: [ opened ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
automate-project-columns:
|
automate-project-columns:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.repository == 'vector-im/element-android' # Skip in forks
|
||||||
steps:
|
steps:
|
||||||
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -2,12 +2,14 @@ name: Move labelled issues to correct boards and columns
|
||||||
|
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [labeled]
|
types: [ labeled ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
move_needs_info_issues:
|
move_needs_info_issues:
|
||||||
name: X-Needs-Info issues to Need info column on triage board
|
name: X-Needs-Info issues to Need info column on triage board
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.repository == 'vector-im/element-android' # Skip in forks
|
||||||
steps:
|
steps:
|
||||||
- uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338
|
- uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338
|
||||||
with:
|
with:
|
||||||
|
@ -19,15 +21,16 @@ jobs:
|
||||||
add_priority_design_issues_to_project:
|
add_priority_design_issues_to_project:
|
||||||
name: P1 X-Needs-Design to Design project board
|
name: P1 X-Needs-Design to Design project board
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >
|
if: |
|
||||||
contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
|
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||||
(contains(github.event.issue.labels.*.name, 'S-Critical') &&
|
contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
|
||||||
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
(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, 'O-Occasional')) ||
|
||||||
contains(github.event.issue.labels.*.name, 'S-Major') &&
|
contains(github.event.issue.labels.*.name, 'S-Major') &&
|
||||||
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||||
contains(github.event.issue.labels.*.name, 'A11y') &&
|
contains(github.event.issue.labels.*.name, 'A11y') &&
|
||||||
contains(github.event.issue.labels.*.name, 'O-Frequent'))
|
contains(github.event.issue.labels.*.name, 'O-Frequent'))
|
||||||
steps:
|
steps:
|
||||||
- uses: octokit/graphql-action@v2.x
|
- uses: octokit/graphql-action@v2.x
|
||||||
id: add_to_project
|
id: add_to_project
|
||||||
|
@ -47,36 +50,38 @@ jobs:
|
||||||
PROJECT_ID: "PN_kwDOAM0swc0sUA"
|
PROJECT_ID: "PN_kwDOAM0swc0sUA"
|
||||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
# delight_issues_to_board:
|
# delight_issues_to_board:
|
||||||
# name: Spaces issues to new Delight project board
|
# name: Spaces issues to new Delight project board
|
||||||
# runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
# if: >
|
# if: |
|
||||||
# contains(github.event.issue.labels.*.name, 'A-Spaces') ||
|
# github.repository == 'vector-im/element-android' && # Skip in forks
|
||||||
# contains(github.event.issue.labels.*.name, 'A-Space-Settings') ||
|
# contains(github.event.issue.labels.*.name, 'A-Spaces') ||
|
||||||
# contains(github.event.issue.labels.*.name, 'A-Subspaces')
|
# contains(github.event.issue.labels.*.name, 'A-Space-Settings') ||
|
||||||
# steps:
|
# contains(github.event.issue.labels.*.name, 'A-Subspaces')
|
||||||
# - uses: octokit/graphql-action@v2.x
|
# steps:
|
||||||
# with:
|
# - uses: octokit/graphql-action@v2.x
|
||||||
# headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
# with:
|
||||||
# query: |
|
# headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||||
# mutation add_to_project($projectid:ID!,$contentid:ID!) {
|
# query: |
|
||||||
# addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
|
# mutation add_to_project($projectid:ID!,$contentid:ID!) {
|
||||||
# projectNextItem {
|
# addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
|
||||||
# id
|
# projectNextItem {
|
||||||
# }
|
# id
|
||||||
# }
|
# }
|
||||||
# }
|
# }
|
||||||
# projectid: ${{ env.PROJECT_ID }}
|
# }
|
||||||
# contentid: ${{ github.event.issue.node_id }}
|
# projectid: ${{ env.PROJECT_ID }}
|
||||||
# env:
|
# contentid: ${{ github.event.issue.node_id }}
|
||||||
# PROJECT_ID: "PN_kwDOAM0swc1HvQ"
|
# env:
|
||||||
# GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
# PROJECT_ID: "PN_kwDOAM0swc1HvQ"
|
||||||
|
# GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
move_voice-message_issues:
|
move_voice-message_issues:
|
||||||
name: A-Voice Messages to voice message board
|
name: A-Voice Messages to voice message board
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >
|
if: |
|
||||||
contains(github.event.issue.labels.*.name, 'A-Voice Messages')
|
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||||
|
contains(github.event.issue.labels.*.name, 'A-Voice Messages')
|
||||||
steps:
|
steps:
|
||||||
- uses: octokit/graphql-action@v2.x
|
- uses: octokit/graphql-action@v2.x
|
||||||
with:
|
with:
|
||||||
|
@ -98,8 +103,9 @@ jobs:
|
||||||
move_threads_issues:
|
move_threads_issues:
|
||||||
name: A-Threads to Thread board
|
name: A-Threads to Thread board
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >
|
if: |
|
||||||
contains(github.event.issue.labels.*.name, 'A-Threads')
|
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||||
|
contains(github.event.issue.labels.*.name, 'A-Threads')
|
||||||
steps:
|
steps:
|
||||||
- uses: octokit/graphql-action@v2.x
|
- uses: octokit/graphql-action@v2.x
|
||||||
with:
|
with:
|
||||||
|
@ -121,8 +127,9 @@ jobs:
|
||||||
move_message_bubbles_issues:
|
move_message_bubbles_issues:
|
||||||
name: A-Message-Bubbles to Message bubbles board
|
name: A-Message-Bubbles to Message bubbles board
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >
|
if: |
|
||||||
contains(github.event.issue.labels.*.name, 'A-Message-Bubbles')
|
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||||
|
contains(github.event.issue.labels.*.name, 'A-Message-Bubbles')
|
||||||
steps:
|
steps:
|
||||||
- uses: octokit/graphql-action@v2.x
|
- uses: octokit/graphql-action@v2.x
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -2,15 +2,15 @@ name: Move unlabelled from needs info columns to triaged
|
||||||
|
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [unlabeled]
|
types: [ unlabeled ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Move_Unabeled_Issue_On_Project_Board:
|
Move_Unabeled_Issue_On_Project_Board:
|
||||||
name: Move no longer X-Needs-Info issues to Triaged
|
name: Move no longer X-Needs-Info issues to Triaged
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >
|
if: |
|
||||||
${{
|
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||||
!contains(github.event.issue.labels.*.name, 'X-Needs-Info') }}
|
!contains(github.event.issue.labels.*.name, 'X-Needs-Info')
|
||||||
env:
|
env:
|
||||||
BOARD_NAME: "Issue triage"
|
BOARD_NAME: "Issue triage"
|
||||||
OWNER: ${{ github.repository_owner }}
|
OWNER: ${{ github.repository_owner }}
|
||||||
|
|
|
@ -2,28 +2,29 @@ name: Move P1 bugs to boards
|
||||||
|
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [labeled, unlabeled]
|
types: [ labeled, unlabeled ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
p1_issues_to_team_workboard:
|
p1_issues_to_team_workboard:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >
|
if: |
|
||||||
(!contains(github.event.issue.labels.*.name, 'A-E2EE') &&
|
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||||
!contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') &&
|
(!contains(github.event.issue.labels.*.name, 'A-E2EE') &&
|
||||||
!contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') &&
|
!contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') &&
|
||||||
!contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') &&
|
!contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') &&
|
||||||
!contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') &&
|
!contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') &&
|
||||||
!contains(github.event.issue.labels.*.name, 'A-Spaces') &&
|
!contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') &&
|
||||||
!contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') &&
|
!contains(github.event.issue.labels.*.name, 'A-Spaces') &&
|
||||||
!contains(github.event.issue.labels.*.name, 'A-Subspaces')) &&
|
!contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') &&
|
||||||
(contains(github.event.issue.labels.*.name, 'T-Defect') &&
|
!contains(github.event.issue.labels.*.name, 'A-Subspaces')) &&
|
||||||
contains(github.event.issue.labels.*.name, 'S-Critical') &&
|
(contains(github.event.issue.labels.*.name, 'T-Defect') &&
|
||||||
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
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, 'O-Occasional')) ||
|
||||||
contains(github.event.issue.labels.*.name, 'S-Major') &&
|
contains(github.event.issue.labels.*.name, 'S-Major') &&
|
||||||
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||||
contains(github.event.issue.labels.*.name, 'A11y') &&
|
contains(github.event.issue.labels.*.name, 'A11y') &&
|
||||||
contains(github.event.issue.labels.*.name, 'O-Frequent'))
|
contains(github.event.issue.labels.*.name, 'O-Frequent'))
|
||||||
steps:
|
steps:
|
||||||
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
||||||
with:
|
with:
|
||||||
|
@ -33,20 +34,21 @@ jobs:
|
||||||
|
|
||||||
P1_issues_to_crypto_team_workboard:
|
P1_issues_to_crypto_team_workboard:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >
|
if: |
|
||||||
(contains(github.event.issue.labels.*.name, 'A-E2EE') ||
|
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||||
contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') ||
|
(contains(github.event.issue.labels.*.name, 'A-E2EE') ||
|
||||||
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
|
contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') ||
|
||||||
contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') ||
|
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
|
||||||
contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) &&
|
contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') ||
|
||||||
(contains(github.event.issue.labels.*.name, 'T-Defect') &&
|
contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) &&
|
||||||
contains(github.event.issue.labels.*.name, 'S-Critical') &&
|
(contains(github.event.issue.labels.*.name, 'T-Defect') &&
|
||||||
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
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, 'O-Occasional')) ||
|
||||||
contains(github.event.issue.labels.*.name, 'S-Major') &&
|
contains(github.event.issue.labels.*.name, 'S-Major') &&
|
||||||
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||||
contains(github.event.issue.labels.*.name, 'A11y') &&
|
contains(github.event.issue.labels.*.name, 'A11y') &&
|
||||||
contains(github.event.issue.labels.*.name, 'O-Frequent'))
|
contains(github.event.issue.labels.*.name, 'O-Frequent'))
|
||||||
steps:
|
steps:
|
||||||
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -29,7 +29,7 @@ buildscript {
|
||||||
|
|
||||||
// ktlint Plugin
|
// ktlint Plugin
|
||||||
plugins {
|
plugins {
|
||||||
id "org.jlleitschuh.gradle.ktlint" version "10.2.0"
|
id "org.jlleitschuh.gradle.ktlint" version "10.2.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Attachment picker UI improvements
|
|
@ -0,0 +1 @@
|
||||||
|
Workaround to fetch all the pending toDevice events from a Synapse homeserver
|
|
@ -0,0 +1 @@
|
||||||
|
Cleaning rendering of state events in timeline
|
|
@ -0,0 +1 @@
|
||||||
|
Fixes newer emojis rendering strangely when inserting from the system keyboard
|
|
@ -0,0 +1 @@
|
||||||
|
Fixing unable to change change avatar in some scenarios
|
|
@ -0,0 +1 @@
|
||||||
|
Fixing encrypted non message events showing up as notification messages (eg when a participant joins, mutes or leaves a voice call)
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=dd54e87b4d7aa8ff3c6afb0f7805aa121d4b70bca55b8c9b1b896eb103184582
|
distributionSha256Sum=c9490e938b221daf0094982288e4038deed954a3f12fb54cbf270ddf4e37d879
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -46,4 +46,9 @@
|
||||||
<dimen name="menu_item_icon_size">24dp</dimen>
|
<dimen name="menu_item_icon_size">24dp</dimen>
|
||||||
<dimen name="menu_item_size">48dp</dimen>
|
<dimen name="menu_item_size">48dp</dimen>
|
||||||
<dimen name="menu_item_ripple_size">48dp</dimen>
|
<dimen name="menu_item_ripple_size">48dp</dimen>
|
||||||
|
|
||||||
|
<!-- Composer -->
|
||||||
|
<dimen name="composer_min_height">56dp</dimen>
|
||||||
|
<dimen name="composer_attachment_size">52dp</dimen>
|
||||||
|
<dimen name="composer_attachment_margin">1dp</dimen>
|
||||||
</resources>
|
</resources>
|
|
@ -152,6 +152,13 @@ class FlowSession(private val session: Session) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun liveUserAccountData(type: String): Flow<Optional<UserAccountDataEvent>> {
|
||||||
|
return session.accountDataService().getLiveUserAccountDataEvent(type).asFlow()
|
||||||
|
.startWith(session.coroutineDispatchers.io) {
|
||||||
|
session.accountDataService().getUserAccountDataEvent(type).toOptional()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun liveRoomAccountData(types: Set<String>): Flow<List<RoomAccountDataEvent>> {
|
fun liveRoomAccountData(types: Set<String>): Flow<List<RoomAccountDataEvent>> {
|
||||||
return session.accountDataService().getLiveRoomAccountDataEvents(types).asFlow()
|
return session.accountDataService().getLiveRoomAccountDataEvents(types).asFlow()
|
||||||
.startWith(session.coroutineDispatchers.io) {
|
.startWith(session.coroutineDispatchers.io) {
|
||||||
|
|
|
@ -160,7 +160,7 @@ dependencies {
|
||||||
implementation libs.apache.commonsImaging
|
implementation libs.apache.commonsImaging
|
||||||
|
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.39'
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40'
|
||||||
|
|
||||||
testImplementation libs.tests.junit
|
testImplementation libs.tests.junit
|
||||||
testImplementation 'org.robolectric:robolectric:4.7.3'
|
testImplementation 'org.robolectric:robolectric:4.7.3'
|
||||||
|
|
|
@ -429,7 +429,17 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
|
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
|
||||||
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
|
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
|
||||||
}
|
}
|
||||||
if (isStarted()) {
|
// There is a limit of to_device events returned per sync.
|
||||||
|
// If we are in a case of such limited to_device sync we can't try to generate/upload
|
||||||
|
// new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate
|
||||||
|
// the old otk too early. In this case we want to wait for the pending to_device before doing anything
|
||||||
|
// As per spec:
|
||||||
|
// If there is a large queue of send-to-device messages, the server should limit the number sent in each /sync response.
|
||||||
|
// 100 messages is recommended as a reasonable limit.
|
||||||
|
// The limit is not part of the spec, so it's probably safer to handle that when there are no more to_device ( so we are sure
|
||||||
|
// that there are no pending to_device
|
||||||
|
val toDevices = syncResponse.toDevice?.events.orEmpty()
|
||||||
|
if (isStarted() && toDevices.isEmpty()) {
|
||||||
// Make sure we process to-device messages before generating new one-time-keys #2782
|
// Make sure we process to-device messages before generating new one-time-keys #2782
|
||||||
deviceListManager.refreshOutdatedDeviceLists()
|
deviceListManager.refreshOutdatedDeviceLists()
|
||||||
// The presence of device_unused_fallback_key_types indicates that the server supports fallback keys.
|
// The presence of device_unused_fallback_key_types indicates that the server supports fallback keys.
|
||||||
|
|
|
@ -109,18 +109,23 @@ internal class FileUploader @Inject constructor(
|
||||||
filename: String?,
|
filename: String?,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
||||||
val inputStream = withContext(Dispatchers.IO) {
|
val workingFile = context.copyUriToTempFile(uri)
|
||||||
context.contentResolver.openInputStream(uri)
|
|
||||||
} ?: throw FileNotFoundException()
|
|
||||||
val workingFile = temporaryFileCreator.create()
|
|
||||||
workingFile.outputStream().use {
|
|
||||||
inputStream.copyTo(it)
|
|
||||||
}
|
|
||||||
return uploadFile(workingFile, filename, mimeType, progressListener).also {
|
return uploadFile(workingFile, filename, mimeType, progressListener).also {
|
||||||
tryOrNull { workingFile.delete() }
|
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,
|
private suspend fun upload(uploadBody: RequestBody,
|
||||||
filename: String?,
|
filename: String?,
|
||||||
progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
|
progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
|
||||||
|
|
|
@ -68,7 +68,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) {
|
override suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) {
|
||||||
withContext(coroutineDispatchers.main) {
|
withContext(coroutineDispatchers.io) {
|
||||||
val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg)
|
val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg)
|
||||||
setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri))
|
setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri))
|
||||||
userStore.updateAvatar(userId, response.contentUri)
|
userStore.updateAvatar(userId, response.contentUri)
|
||||||
|
|
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.isTokenError
|
import org.matrix.android.sdk.api.failure.isTokenError
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
|
@ -71,6 +72,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
private var isStarted = false
|
private var isStarted = false
|
||||||
private var isTokenValid = true
|
private var isTokenValid = true
|
||||||
private var retryNoNetworkTask: TimerTask? = null
|
private var retryNoNetworkTask: TimerTask? = null
|
||||||
|
private var previousSyncResponseHasToDevice = false
|
||||||
|
|
||||||
private val activeCallListObserver = Observer<MutableList<MxCall>> { activeCalls ->
|
private val activeCallListObserver = Observer<MutableList<MxCall>> { activeCalls ->
|
||||||
if (activeCalls.isEmpty() && backgroundDetectionObserver.isInBackground) {
|
if (activeCalls.isEmpty() && backgroundDetectionObserver.isInBackground) {
|
||||||
|
@ -171,12 +173,15 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
if (state !is SyncState.Running) {
|
if (state !is SyncState.Running) {
|
||||||
updateStateTo(SyncState.Running(afterPause = true))
|
updateStateTo(SyncState.Running(afterPause = true))
|
||||||
}
|
}
|
||||||
// No timeout after a pause
|
val timeout = when {
|
||||||
val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
|
previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */
|
||||||
|
state.let { it is SyncState.Running && it.afterPause } -> 0L /* No timeout after a pause */
|
||||||
|
else -> DEFAULT_LONG_POOL_TIMEOUT
|
||||||
|
}
|
||||||
Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout")
|
Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout")
|
||||||
val params = SyncTask.Params(timeout, SyncPresence.Online)
|
val params = SyncTask.Params(timeout, SyncPresence.Online)
|
||||||
val sync = syncScope.launch {
|
val sync = syncScope.launch {
|
||||||
doSync(params)
|
previousSyncResponseHasToDevice = doSync(params)
|
||||||
}
|
}
|
||||||
runBlocking {
|
runBlocking {
|
||||||
sync.join()
|
sync.join()
|
||||||
|
@ -203,10 +208,14 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doSync(params: SyncTask.Params) {
|
/**
|
||||||
try {
|
* Will return true if the sync response contains some toDevice events.
|
||||||
|
*/
|
||||||
|
private suspend fun doSync(params: SyncTask.Params): Boolean {
|
||||||
|
return try {
|
||||||
val syncResponse = syncTask.execute(params)
|
val syncResponse = syncTask.execute(params)
|
||||||
_syncFlow.emit(syncResponse)
|
_syncFlow.emit(syncResponse)
|
||||||
|
syncResponse.toDevice?.events?.isNotEmpty().orFalse()
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
if (failure is Failure.NetworkConnection) {
|
if (failure is Failure.NetworkConnection) {
|
||||||
canReachServer = false
|
canReachServer = false
|
||||||
|
@ -229,6 +238,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
delay(RETRY_WAIT_TIME_MS)
|
delay(RETRY_WAIT_TIME_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
false
|
||||||
} finally {
|
} finally {
|
||||||
state.let {
|
state.let {
|
||||||
if (it is SyncState.Running && it.afterPause) {
|
if (it is SyncState.Running && it.afterPause) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import androidx.work.BackoffPolicy
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.failure.isTokenError
|
import org.matrix.android.sdk.api.failure.isTokenError
|
||||||
import org.matrix.android.sdk.internal.SessionManager
|
import org.matrix.android.sdk.internal.SessionManager
|
||||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||||
|
@ -34,8 +35,8 @@ import timber.log.Timber
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val DEFAULT_LONG_POOL_TIMEOUT = 6L
|
private const val DEFAULT_LONG_POOL_TIMEOUT_SECONDS = 6L
|
||||||
private const val DEFAULT_DELAY_TIMEOUT = 30_000L
|
private const val DEFAULT_DELAY_MILLIS = 30_000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Possible previous worker: None
|
* Possible previous worker: None
|
||||||
|
@ -47,9 +48,12 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class Params(
|
internal data class Params(
|
||||||
override val sessionId: String,
|
override val sessionId: String,
|
||||||
val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT,
|
// In seconds
|
||||||
val delay: Long = DEFAULT_DELAY_TIMEOUT,
|
val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT_SECONDS,
|
||||||
|
// In milliseconds
|
||||||
|
val delay: Long = DEFAULT_DELAY_MILLIS,
|
||||||
val periodic: Boolean = false,
|
val periodic: Boolean = false,
|
||||||
|
val forceImmediate: Boolean = false,
|
||||||
override val lastFailureMessage: String? = null
|
override val lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
|
@ -65,13 +69,26 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
|
||||||
Timber.i("Sync work starting")
|
Timber.i("Sync work starting")
|
||||||
|
|
||||||
return runCatching {
|
return runCatching {
|
||||||
doSync(params.timeout)
|
doSync(if (params.forceImmediate) 0 else params.timeout)
|
||||||
}.fold(
|
}.fold(
|
||||||
{
|
{ hasToDeviceEvents ->
|
||||||
Result.success().also {
|
Result.success().also {
|
||||||
if (params.periodic) {
|
if (params.periodic) {
|
||||||
// we want to schedule another one after delay
|
// we want to schedule another one after a delay, or immediately if hasToDeviceEvents
|
||||||
automaticallyBackgroundSync(workManagerProvider, params.sessionId, params.timeout, params.delay)
|
automaticallyBackgroundSync(
|
||||||
|
workManagerProvider = workManagerProvider,
|
||||||
|
sessionId = params.sessionId,
|
||||||
|
serverTimeoutInSeconds = params.timeout,
|
||||||
|
delayInSeconds = params.delay,
|
||||||
|
forceImmediate = hasToDeviceEvents
|
||||||
|
)
|
||||||
|
} else if (hasToDeviceEvents) {
|
||||||
|
// Previous response has toDevice events, request an immediate sync request
|
||||||
|
requireBackgroundSync(
|
||||||
|
workManagerProvider = workManagerProvider,
|
||||||
|
sessionId = params.sessionId,
|
||||||
|
serverTimeoutInSeconds = 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -92,16 +109,29 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
|
||||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doSync(timeout: Long) {
|
/**
|
||||||
|
* Will return true if the sync response contains some toDevice events.
|
||||||
|
*/
|
||||||
|
private suspend fun doSync(timeout: Long): Boolean {
|
||||||
val taskParams = SyncTask.Params(timeout * 1000, SyncPresence.Offline)
|
val taskParams = SyncTask.Params(timeout * 1000, SyncPresence.Offline)
|
||||||
syncTask.execute(taskParams)
|
val syncResponse = syncTask.execute(taskParams)
|
||||||
|
return syncResponse.toDevice?.events?.isNotEmpty().orFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val BG_SYNC_WORK_NAME = "BG_SYNCP"
|
private const val BG_SYNC_WORK_NAME = "BG_SYNCP"
|
||||||
|
|
||||||
fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) {
|
fun requireBackgroundSync(workManagerProvider: WorkManagerProvider,
|
||||||
val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, 0L, false))
|
sessionId: String,
|
||||||
|
serverTimeoutInSeconds: Long = 0) {
|
||||||
|
val data = WorkerParamsFactory.toData(
|
||||||
|
Params(
|
||||||
|
sessionId = sessionId,
|
||||||
|
timeout = serverTimeoutInSeconds,
|
||||||
|
delay = 0L,
|
||||||
|
periodic = false
|
||||||
|
)
|
||||||
|
)
|
||||||
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
|
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
|
||||||
.setConstraints(WorkManagerProvider.workConstraints)
|
.setConstraints(WorkManagerProvider.workConstraints)
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
|
||||||
|
@ -111,13 +141,24 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
|
||||||
.enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
.enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delayInSeconds: Long = 30) {
|
fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider,
|
||||||
val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, delayInSeconds, true))
|
sessionId: String,
|
||||||
|
serverTimeoutInSeconds: Long = 0,
|
||||||
|
delayInSeconds: Long = 30,
|
||||||
|
forceImmediate: Boolean = false) {
|
||||||
|
val data = WorkerParamsFactory.toData(
|
||||||
|
Params(
|
||||||
|
sessionId = sessionId,
|
||||||
|
timeout = serverTimeoutInSeconds,
|
||||||
|
delay = delayInSeconds,
|
||||||
|
forceImmediate = forceImmediate
|
||||||
|
)
|
||||||
|
)
|
||||||
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
|
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
|
||||||
.setConstraints(WorkManagerProvider.workConstraints)
|
.setConstraints(WorkManagerProvider.workConstraints)
|
||||||
.setInputData(data)
|
.setInputData(data)
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
|
||||||
.setInitialDelay(delayInSeconds, TimeUnit.SECONDS)
|
.setInitialDelay(if (forceImmediate) 0 else delayInSeconds, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
// Avoid risking multiple chains of syncs by replacing the existing chain
|
// Avoid risking multiple chains of syncs by replacing the existing chain
|
||||||
workManagerProvider.workManager
|
workManagerProvider.workManager
|
||||||
|
|
|
@ -140,7 +140,7 @@ android {
|
||||||
buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\""
|
buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\""
|
||||||
resValue "string", "build_number", "\"${buildNumber}\""
|
resValue "string", "build_number", "\"${buildNumber}\""
|
||||||
|
|
||||||
buildConfigField "im.vector.app.features.VectorFeatures.LoginVersion", "LOGIN_VERSION", "im.vector.app.features.VectorFeatures.LoginVersion.V1"
|
buildConfigField "im.vector.app.features.VectorFeatures.LoginVariant", "LOGIN_VARIANT", "im.vector.app.features.VectorFeatures.LoginVariant.LEGACY"
|
||||||
|
|
||||||
buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping"
|
buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping"
|
||||||
|
|
||||||
|
@ -362,7 +362,7 @@ dependencies {
|
||||||
implementation 'com.facebook.stetho:stetho:1.6.0'
|
implementation 'com.facebook.stetho:stetho:1.6.0'
|
||||||
|
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.39'
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40'
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
implementation libs.github.flowBinding
|
implementation libs.github.flowBinding
|
||||||
|
|
|
@ -28,8 +28,8 @@ class DebugFeaturesStateFactory @Inject constructor(
|
||||||
return FeaturesState(listOf(
|
return FeaturesState(listOf(
|
||||||
createEnumFeature(
|
createEnumFeature(
|
||||||
label = "Login version",
|
label = "Login version",
|
||||||
selection = debugFeatures.loginVersion(),
|
selection = debugFeatures.loginVariant(),
|
||||||
default = defaultFeatures.loginVersion()
|
default = defaultFeatures.loginVariant()
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,8 @@ class DebugVectorFeatures(
|
||||||
|
|
||||||
private val dataStore = context.dataStore
|
private val dataStore = context.dataStore
|
||||||
|
|
||||||
override fun loginVersion(): VectorFeatures.LoginVersion {
|
override fun loginVariant(): VectorFeatures.LoginVariant {
|
||||||
return readPreferences().getEnum<VectorFeatures.LoginVersion>() ?: vectorFeatures.loginVersion()
|
return readPreferences().getEnum<VectorFeatures.LoginVariant>() ?: vectorFeatures.loginVariant()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Enum<T>> hasEnumOverride(type: KClass<T>) = readPreferences().containsEnum(type)
|
fun <T : Enum<T>> hasEnumOverride(type: KClass<T>) = readPreferences().containsEnum(type)
|
||||||
|
|
|
@ -137,7 +137,7 @@
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.login2.LoginActivity2"
|
android:name=".features.ftue.FTUEActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -105,7 +105,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
||||||
protected val viewModelProvider
|
protected val viewModelProvider
|
||||||
get() = ViewModelProvider(this, viewModelFactory)
|
get() = ViewModelProvider(this, viewModelFactory)
|
||||||
|
|
||||||
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
|
fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
|
||||||
viewEvents
|
viewEvents
|
||||||
.stream()
|
.stream()
|
||||||
.onEach {
|
.onEach {
|
||||||
|
|
|
@ -20,11 +20,12 @@ import im.vector.app.BuildConfig
|
||||||
|
|
||||||
interface VectorFeatures {
|
interface VectorFeatures {
|
||||||
|
|
||||||
fun loginVersion(): LoginVersion
|
fun loginVariant(): LoginVariant
|
||||||
|
|
||||||
enum class LoginVersion {
|
enum class LoginVariant {
|
||||||
V1,
|
LEGACY,
|
||||||
V2
|
FTUE,
|
||||||
|
FTUE_WIP
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class NotificationSettingsVersion {
|
enum class NotificationSettingsVersion {
|
||||||
|
@ -34,5 +35,5 @@ interface VectorFeatures {
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultVectorFeatures : VectorFeatures {
|
class DefaultVectorFeatures : VectorFeatures {
|
||||||
override fun loginVersion(): VectorFeatures.LoginVersion = BuildConfig.LOGIN_VERSION
|
override fun loginVariant(): VectorFeatures.LoginVariant = BuildConfig.LOGIN_VARIANT
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,24 +26,18 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewAnimationUtils
|
import android.view.ViewAnimationUtils
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.view.animation.AnimationSet
|
|
||||||
import android.view.animation.OvershootInterpolator
|
|
||||||
import android.view.animation.ScaleAnimation
|
|
||||||
import android.view.animation.TranslateAnimation
|
import android.view.animation.TranslateAnimation
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.PopupWindow
|
import android.widget.PopupWindow
|
||||||
import androidx.core.view.doOnNextLayout
|
import androidx.core.view.doOnNextLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
|
||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.getMeasurements
|
import im.vector.app.core.epoxy.onClick
|
||||||
import im.vector.app.core.utils.PERMISSIONS_EMPTY
|
import im.vector.app.core.utils.PERMISSIONS_EMPTY
|
||||||
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
|
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
|
||||||
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
||||||
import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
|
import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
|
||||||
import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
private const val ANIMATION_DURATION = 250
|
private const val ANIMATION_DURATION = 250
|
||||||
|
@ -52,17 +46,16 @@ private const val ANIMATION_DURATION = 250
|
||||||
* This class is the view presenting choices for picking attachments.
|
* This class is the view presenting choices for picking attachments.
|
||||||
* It will return result through [Callback].
|
* It will return result through [Callback].
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class AttachmentTypeSelectorView(context: Context,
|
class AttachmentTypeSelectorView(context: Context,
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
var callback: Callback?) :
|
var callback: Callback?
|
||||||
PopupWindow(context) {
|
) : PopupWindow(context) {
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onTypeSelected(type: Type)
|
fun onTypeSelected(type: Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val iconColorGenerator = ColorGenerator.MATERIAL
|
|
||||||
|
|
||||||
private val views: ViewAttachmentTypeSelectorBinding
|
private val views: ViewAttachmentTypeSelectorBinding
|
||||||
|
|
||||||
private var anchor: View? = null
|
private var anchor: View? = null
|
||||||
|
@ -85,35 +78,40 @@ class AttachmentTypeSelectorView(context: Context,
|
||||||
inputMethodMode = INPUT_METHOD_NOT_NEEDED
|
inputMethodMode = INPUT_METHOD_NOT_NEEDED
|
||||||
isFocusable = true
|
isFocusable = true
|
||||||
isTouchable = true
|
isTouchable = true
|
||||||
|
|
||||||
|
views.attachmentCloseButton.onClick {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show(anchor: View, isKeyboardOpen: Boolean) {
|
private fun animateOpen() {
|
||||||
|
views.attachmentCloseButton.animate()
|
||||||
|
.setDuration(200)
|
||||||
|
.rotation(135f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateClose() {
|
||||||
|
views.attachmentCloseButton.animate()
|
||||||
|
.setDuration(200)
|
||||||
|
.rotation(0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(anchor: View) {
|
||||||
|
animateOpen()
|
||||||
|
|
||||||
this.anchor = anchor
|
this.anchor = anchor
|
||||||
val anchorCoordinates = IntArray(2)
|
val anchorCoordinates = IntArray(2)
|
||||||
anchor.getLocationOnScreen(anchorCoordinates)
|
anchor.getLocationOnScreen(anchorCoordinates)
|
||||||
if (isKeyboardOpen) {
|
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1])
|
||||||
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height)
|
|
||||||
} else {
|
|
||||||
val contentViewHeight = if (contentView.height == 0) {
|
|
||||||
contentView.getMeasurements().second
|
|
||||||
} else {
|
|
||||||
contentView.height
|
|
||||||
}
|
|
||||||
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight)
|
|
||||||
}
|
|
||||||
contentView.doOnNextLayout {
|
contentView.doOnNextLayout {
|
||||||
animateWindowInCircular(anchor, contentView)
|
animateWindowInCircular(anchor, contentView)
|
||||||
}
|
}
|
||||||
animateButtonIn(views.attachmentGalleryButton, ANIMATION_DURATION / 2)
|
|
||||||
animateButtonIn(views.attachmentCameraButton, ANIMATION_DURATION / 4)
|
|
||||||
animateButtonIn(views.attachmentFileButton, ANIMATION_DURATION / 2)
|
|
||||||
animateButtonIn(views.attachmentAudioButton, 0)
|
|
||||||
animateButtonIn(views.attachmentContactButton, ANIMATION_DURATION / 4)
|
|
||||||
animateButtonIn(views.attachmentStickersButton, ANIMATION_DURATION / 2)
|
|
||||||
animateButtonIn(views.attachmentPollButton, ANIMATION_DURATION / 4)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dismiss() {
|
override fun dismiss() {
|
||||||
|
animateClose()
|
||||||
|
|
||||||
val capturedAnchor = anchor
|
val capturedAnchor = anchor
|
||||||
if (capturedAnchor != null) {
|
if (capturedAnchor != null) {
|
||||||
animateWindowOutCircular(capturedAnchor, contentView)
|
animateWindowOutCircular(capturedAnchor, contentView)
|
||||||
|
@ -124,28 +122,18 @@ class AttachmentTypeSelectorView(context: Context,
|
||||||
|
|
||||||
fun setAttachmentVisibility(type: Type, isVisible: Boolean) {
|
fun setAttachmentVisibility(type: Type, isVisible: Boolean) {
|
||||||
when (type) {
|
when (type) {
|
||||||
Type.CAMERA -> views.attachmentCameraButtonContainer
|
Type.CAMERA -> views.attachmentCameraButton
|
||||||
Type.GALLERY -> views.attachmentGalleryButtonContainer
|
Type.GALLERY -> views.attachmentGalleryButton
|
||||||
Type.FILE -> views.attachmentFileButtonContainer
|
Type.FILE -> views.attachmentFileButton
|
||||||
Type.STICKER -> views.attachmentStickersButtonContainer
|
Type.STICKER -> views.attachmentStickersButton
|
||||||
Type.AUDIO -> views.attachmentAudioButtonContainer
|
Type.AUDIO -> views.attachmentAudioButton
|
||||||
Type.CONTACT -> views.attachmentContactButtonContainer
|
Type.CONTACT -> views.attachmentContactButton
|
||||||
Type.POLL -> views.attachmentPollButtonContainer
|
Type.POLL -> views.attachmentPollButton
|
||||||
}.let {
|
}.let {
|
||||||
it.isVisible = isVisible
|
it.isVisible = isVisible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun animateButtonIn(button: View, delay: Int) {
|
|
||||||
val animation = AnimationSet(true)
|
|
||||||
val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f)
|
|
||||||
animation.addAnimation(scale)
|
|
||||||
animation.interpolator = OvershootInterpolator(1f)
|
|
||||||
animation.duration = ANIMATION_DURATION.toLong()
|
|
||||||
animation.startOffset = delay.toLong()
|
|
||||||
button.startAnimation(animation)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animateWindowInCircular(anchor: View, contentView: View) {
|
private fun animateWindowInCircular(anchor: View, contentView: View) {
|
||||||
val coordinates = getClickCoordinates(anchor, contentView)
|
val coordinates = getClickCoordinates(anchor, contentView)
|
||||||
val animator = ViewAnimationUtils.createCircularReveal(contentView,
|
val animator = ViewAnimationUtils.createCircularReveal(contentView,
|
||||||
|
@ -157,12 +145,6 @@ class AttachmentTypeSelectorView(context: Context,
|
||||||
animator.start()
|
animator.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun animateWindowInTranslate(contentView: View) {
|
|
||||||
val animation = TranslateAnimation(0f, 0f, contentView.height.toFloat(), 0f)
|
|
||||||
animation.duration = ANIMATION_DURATION.toLong()
|
|
||||||
getContentView().startAnimation(animation)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animateWindowOutCircular(anchor: View, contentView: View) {
|
private fun animateWindowOutCircular(anchor: View, contentView: View) {
|
||||||
val coordinates = getClickCoordinates(anchor, contentView)
|
val coordinates = getClickCoordinates(anchor, contentView)
|
||||||
val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
|
val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
|
||||||
|
@ -207,7 +189,6 @@ class AttachmentTypeSelectorView(context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ImageButton.configure(type: Type): ImageButton {
|
private fun ImageButton.configure(type: Type): ImageButton {
|
||||||
this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal))
|
|
||||||
this.setOnClickListener(TypeClickListener(type))
|
this.setOnClickListener(TypeClickListener(type))
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,11 +168,8 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderCreationSuccess(roomId: String?) {
|
private fun renderCreationSuccess(roomId: String) {
|
||||||
// Navigate to freshly created room
|
navigator.openRoom(this, roomId)
|
||||||
if (roomId != null) {
|
|
||||||
navigator.openRoom(this, roomId)
|
|
||||||
}
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* 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
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@ -14,11 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.login2
|
package im.vector.app.features.ftue
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
|
@ -27,17 +25,13 @@ import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.FragmentTransaction
|
import androidx.fragment.app.FragmentTransaction
|
||||||
import com.airbnb.mvrx.viewModel
|
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE
|
import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE
|
||||||
import im.vector.app.core.extensions.addFragment
|
import im.vector.app.core.extensions.addFragment
|
||||||
import im.vector.app.core.extensions.addFragmentToBackstack
|
import im.vector.app.core.extensions.addFragmentToBackstack
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.extensions.resetBackstack
|
import im.vector.app.core.extensions.resetBackstack
|
||||||
import im.vector.app.core.platform.ToolbarConfigurable
|
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.databinding.ActivityLoginBinding
|
import im.vector.app.databinding.ActivityLoginBinding
|
||||||
import im.vector.app.features.home.HomeActivity
|
import im.vector.app.features.home.HomeActivity
|
||||||
|
@ -49,20 +43,41 @@ import im.vector.app.features.login.TextInputFormFragmentMode
|
||||||
import im.vector.app.features.login.isSupported
|
import im.vector.app.features.login.isSupported
|
||||||
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
|
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
|
||||||
import im.vector.app.features.login.terms.toLocalizedLoginTerms
|
import im.vector.app.features.login.terms.toLocalizedLoginTerms
|
||||||
|
import im.vector.app.features.login2.LoginAction2
|
||||||
|
import im.vector.app.features.login2.LoginCaptchaFragment2
|
||||||
|
import im.vector.app.features.login2.LoginFragmentSigninPassword2
|
||||||
|
import im.vector.app.features.login2.LoginFragmentSigninUsername2
|
||||||
|
import im.vector.app.features.login2.LoginFragmentSignupPassword2
|
||||||
|
import im.vector.app.features.login2.LoginFragmentSignupUsername2
|
||||||
|
import im.vector.app.features.login2.LoginFragmentToAny2
|
||||||
|
import im.vector.app.features.login2.LoginGenericTextInputFormFragment2
|
||||||
|
import im.vector.app.features.login2.LoginResetPasswordFragment2
|
||||||
|
import im.vector.app.features.login2.LoginResetPasswordMailConfirmationFragment2
|
||||||
|
import im.vector.app.features.login2.LoginResetPasswordSuccessFragment2
|
||||||
|
import im.vector.app.features.login2.LoginServerSelectionFragment2
|
||||||
|
import im.vector.app.features.login2.LoginServerUrlFormFragment2
|
||||||
|
import im.vector.app.features.login2.LoginSplashSignUpSignInSelectionFragment2
|
||||||
|
import im.vector.app.features.login2.LoginSsoOnlyFragment2
|
||||||
|
import im.vector.app.features.login2.LoginViewEvents2
|
||||||
|
import im.vector.app.features.login2.LoginViewModel2
|
||||||
|
import im.vector.app.features.login2.LoginViewState2
|
||||||
|
import im.vector.app.features.login2.LoginWaitForEmailFragment2
|
||||||
|
import im.vector.app.features.login2.LoginWebFragment2
|
||||||
import im.vector.app.features.login2.created.AccountCreatedFragment
|
import im.vector.app.features.login2.created.AccountCreatedFragment
|
||||||
import im.vector.app.features.login2.terms.LoginTermsFragment2
|
import im.vector.app.features.login2.terms.LoginTermsFragment2
|
||||||
import im.vector.app.features.pin.UnlockedActivity
|
|
||||||
import org.matrix.android.sdk.api.auth.registration.FlowResult
|
import org.matrix.android.sdk.api.auth.registration.FlowResult
|
||||||
import org.matrix.android.sdk.api.auth.registration.Stage
|
import org.matrix.android.sdk.api.auth.registration.Stage
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
|
||||||
/**
|
private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG"
|
||||||
* The LoginActivity manages the fragment navigation and also display the loading View
|
private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG"
|
||||||
*/
|
|
||||||
@AndroidEntryPoint
|
|
||||||
open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarConfigurable, UnlockedActivity {
|
|
||||||
|
|
||||||
private val loginViewModel: LoginViewModel2 by viewModel()
|
class FTUEWipVariant(
|
||||||
|
private val views: ActivityLoginBinding,
|
||||||
|
private val loginViewModel: LoginViewModel2,
|
||||||
|
private val activity: VectorBaseActivity<ActivityLoginBinding>,
|
||||||
|
private val supportFragmentManager: FragmentManager
|
||||||
|
) : FTUEVariant {
|
||||||
|
|
||||||
private val enterAnim = R.anim.enter_fade_in
|
private val enterAnim = R.anim.enter_fade_in
|
||||||
private val exitAnim = R.anim.exit_fade_out
|
private val exitAnim = R.anim.exit_fade_out
|
||||||
|
@ -76,39 +91,36 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
private val commonOption: (FragmentTransaction) -> Unit = { ft ->
|
private val commonOption: (FragmentTransaction) -> Unit = { ft ->
|
||||||
// Find the loginLogo on the current Fragment, this should not return null
|
// Find the loginLogo on the current Fragment, this should not return null
|
||||||
(topFragment?.view as? ViewGroup)
|
(topFragment?.view as? ViewGroup)
|
||||||
// Find findViewById does not work, I do not know why
|
// Find activity.findViewById does not work, I do not know why
|
||||||
// findViewById<View?>(R.id.loginLogo)
|
// activity.findViewById<View?>(views.loginLogo)
|
||||||
?.children
|
?.children
|
||||||
?.firstOrNull { it.id == R.id.loginLogo }
|
?.firstOrNull { it.id == R.id.loginLogo }
|
||||||
?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||||
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
||||||
}
|
}
|
||||||
|
|
||||||
final override fun getBinding() = ActivityLoginBinding.inflate(layoutInflater)
|
override fun initUiAndData(isFirstCreation: Boolean) {
|
||||||
|
if (isFirstCreation) {
|
||||||
override fun getCoordinatorLayout() = views.coordinatorLayout
|
|
||||||
|
|
||||||
override fun initUiAndData() {
|
|
||||||
if (isFirstCreation()) {
|
|
||||||
addFirstFragment()
|
addFirstFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
loginViewModel.onEach {
|
with(activity) {
|
||||||
updateWithState(it)
|
loginViewModel.onEach {
|
||||||
|
updateWithState(it)
|
||||||
|
}
|
||||||
|
loginViewModel.observeViewEvents { handleLoginViewEvents(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
loginViewModel.observeViewEvents { handleLoginViewEvents(it) }
|
|
||||||
|
|
||||||
// Get config extra
|
// Get config extra
|
||||||
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
|
val loginConfig = activity.intent.getParcelableExtra<LoginConfig?>(FTUEActivity.EXTRA_CONFIG)
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation) {
|
||||||
// TODO Check this
|
// TODO Check this
|
||||||
loginViewModel.handle(LoginAction2.InitWith(loginConfig))
|
loginViewModel.handle(LoginAction2.InitWith(loginConfig))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun addFirstFragment() {
|
private fun addFirstFragment() {
|
||||||
addFragment(views.loginFragmentContainer, LoginSplashSignUpSignInSelectionFragment2::class.java)
|
activity.addFragment(views.loginFragmentContainer, LoginSplashSignUpSignInSelectionFragment2::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoginViewEvents(event: LoginViewEvents2) {
|
private fun handleLoginViewEvents(event: LoginViewEvents2) {
|
||||||
|
@ -127,7 +139,7 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
// First ask for login and password
|
// First ask for login and password
|
||||||
// I add a tag to indicate that this fragment is a registration stage.
|
// I add a tag to indicate that this fragment is a registration stage.
|
||||||
// This way it will be automatically popped in when starting the next registration stage
|
// This way it will be automatically popped in when starting the next registration stage
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginFragment2::class.java,
|
LoginFragment2::class.java,
|
||||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||||
option = commonOption
|
option = commonOption
|
||||||
|
@ -138,7 +150,7 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is LoginViewEvents2.OutdatedHomeserver -> {
|
is LoginViewEvents2.OutdatedHomeserver -> {
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(activity)
|
||||||
.setTitle(R.string.login_error_outdated_homeserver_title)
|
.setTitle(R.string.login_error_outdated_homeserver_title)
|
||||||
.setMessage(R.string.login_error_outdated_homeserver_warning_content)
|
.setMessage(R.string.login_error_outdated_homeserver_warning_content)
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
@ -146,54 +158,54 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
is LoginViewEvents2.OpenServerSelection ->
|
is LoginViewEvents2.OpenServerSelection ->
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginServerSelectionFragment2::class.java,
|
LoginServerSelectionFragment2::class.java,
|
||||||
option = { ft ->
|
option = { ft ->
|
||||||
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
activity.findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||||
// Disable transition of text
|
// Disable transition of text
|
||||||
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
// activity.findViewById<View?>(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||||
// No transition here now actually
|
// No transition here now actually
|
||||||
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
// activity.findViewById<View?>(views.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||||
// TODO Disabled because it provokes a flickering
|
// TODO Disabled because it provokes a flickering
|
||||||
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
||||||
})
|
})
|
||||||
is LoginViewEvents2.OpenHomeServerUrlFormScreen -> {
|
is LoginViewEvents2.OpenHomeServerUrlFormScreen -> {
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginServerUrlFormFragment2::class.java,
|
LoginServerUrlFormFragment2::class.java,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
}
|
}
|
||||||
is LoginViewEvents2.OpenSignInEnterIdentifierScreen -> {
|
is LoginViewEvents2.OpenSignInEnterIdentifierScreen -> {
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginFragmentSigninUsername2::class.java,
|
LoginFragmentSigninUsername2::class.java,
|
||||||
option = { ft ->
|
option = { ft ->
|
||||||
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
activity.findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||||
// Disable transition of text
|
// Disable transition of text
|
||||||
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
// activity.findViewById<View?>(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||||
// No transition here now actually
|
// No transition here now actually
|
||||||
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
// activity.findViewById<View?>(views.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||||
// TODO Disabled because it provokes a flickering
|
// TODO Disabled because it provokes a flickering
|
||||||
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
is LoginViewEvents2.OpenSsoOnlyScreen -> {
|
is LoginViewEvents2.OpenSsoOnlyScreen -> {
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginSsoOnlyFragment2::class.java,
|
LoginSsoOnlyFragment2::class.java,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
}
|
}
|
||||||
is LoginViewEvents2.OnWebLoginError -> onWebLoginError(event)
|
is LoginViewEvents2.OnWebLoginError -> onWebLoginError(event)
|
||||||
is LoginViewEvents2.OpenResetPasswordScreen ->
|
is LoginViewEvents2.OpenResetPasswordScreen ->
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginResetPasswordFragment2::class.java,
|
LoginResetPasswordFragment2::class.java,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
is LoginViewEvents2.OnResetPasswordSendThreePidDone -> {
|
is LoginViewEvents2.OnResetPasswordSendThreePidDone -> {
|
||||||
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginResetPasswordMailConfirmationFragment2::class.java,
|
LoginResetPasswordMailConfirmationFragment2::class.java,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
}
|
}
|
||||||
is LoginViewEvents2.OnResetPasswordMailConfirmationSuccess -> {
|
is LoginViewEvents2.OnResetPasswordMailConfirmationSuccess -> {
|
||||||
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginResetPasswordSuccessFragment2::class.java,
|
LoginResetPasswordSuccessFragment2::class.java,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
}
|
}
|
||||||
|
@ -202,37 +214,37 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE)
|
||||||
}
|
}
|
||||||
is LoginViewEvents2.OnSendEmailSuccess ->
|
is LoginViewEvents2.OnSendEmailSuccess ->
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginWaitForEmailFragment2::class.java,
|
LoginWaitForEmailFragment2::class.java,
|
||||||
LoginWaitForEmailFragmentArgument(event.email),
|
LoginWaitForEmailFragmentArgument(event.email),
|
||||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
is LoginViewEvents2.OpenSigninPasswordScreen -> {
|
is LoginViewEvents2.OpenSigninPasswordScreen -> {
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginFragmentSigninPassword2::class.java,
|
LoginFragmentSigninPassword2::class.java,
|
||||||
tag = FRAGMENT_LOGIN_TAG,
|
tag = FRAGMENT_LOGIN_TAG,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
}
|
}
|
||||||
is LoginViewEvents2.OpenSignupPasswordScreen -> {
|
is LoginViewEvents2.OpenSignupPasswordScreen -> {
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginFragmentSignupPassword2::class.java,
|
LoginFragmentSignupPassword2::class.java,
|
||||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
}
|
}
|
||||||
is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> {
|
is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> {
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginFragmentSignupUsername2::class.java,
|
LoginFragmentSignupUsername2::class.java,
|
||||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
}
|
}
|
||||||
is LoginViewEvents2.OpenSignInWithAnythingScreen -> {
|
is LoginViewEvents2.OpenSignInWithAnythingScreen -> {
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginFragmentToAny2::class.java,
|
LoginFragmentToAny2::class.java,
|
||||||
tag = FRAGMENT_LOGIN_TAG,
|
tag = FRAGMENT_LOGIN_TAG,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
}
|
}
|
||||||
is LoginViewEvents2.OnSendMsisdnSuccess ->
|
is LoginViewEvents2.OnSendMsisdnSuccess ->
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginGenericTextInputFormFragment2::class.java,
|
LoginGenericTextInputFormFragment2::class.java,
|
||||||
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, event.msisdn),
|
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, event.msisdn),
|
||||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||||
|
@ -250,14 +262,14 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
|
|
||||||
private fun handleCancelRegistration() {
|
private fun handleCancelRegistration() {
|
||||||
// Cleanup the back stack
|
// Cleanup the back stack
|
||||||
resetBackstack()
|
activity.resetBackstack()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) {
|
private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) {
|
||||||
if (event.newAccount) {
|
if (event.newAccount) {
|
||||||
// Propose to set avatar and display name
|
// Propose to set avatar and display name
|
||||||
// Back on this Fragment will finish the Activity
|
// Back on this Fragment will finish the Activity
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
AccountCreatedFragment::class.java,
|
AccountCreatedFragment::class.java,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
} else {
|
} else {
|
||||||
|
@ -267,11 +279,11 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
|
|
||||||
private fun terminate(newAccount: Boolean) {
|
private fun terminate(newAccount: Boolean) {
|
||||||
val intent = HomeActivity.newIntent(
|
val intent = HomeActivity.newIntent(
|
||||||
this,
|
activity,
|
||||||
accountCreation = newAccount
|
accountCreation = newAccount
|
||||||
)
|
)
|
||||||
startActivity(intent)
|
activity.startActivity(intent)
|
||||||
finish()
|
activity.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateWithState(LoginViewState2: LoginViewState2) {
|
private fun updateWithState(LoginViewState2: LoginViewState2) {
|
||||||
|
@ -280,7 +292,7 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hack for AccountCreatedFragment
|
// Hack for AccountCreatedFragment
|
||||||
fun setIsLoading(isLoading: Boolean) {
|
override fun setIsLoading(isLoading: Boolean) {
|
||||||
views.loginLoading.isVisible = isLoading
|
views.loginLoading.isVisible = isLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,9 +301,9 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
|
|
||||||
// And inform the user
|
// And inform the user
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(activity)
|
||||||
.setTitle(R.string.dialog_title_error)
|
.setTitle(R.string.dialog_title_error)
|
||||||
.setMessage(getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode))
|
.setMessage(activity.getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
@ -300,19 +312,17 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
* Handle the SSO redirection here
|
* Handle the SSO redirection here
|
||||||
*/
|
*/
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
|
||||||
|
|
||||||
intent?.data
|
intent?.data
|
||||||
?.let { tryOrNull { it.getQueryParameter("loginToken") } }
|
?.let { tryOrNull { it.getQueryParameter("loginToken") } }
|
||||||
?.let { loginViewModel.handle(LoginAction2.LoginWithToken(it)) }
|
?.let { loginViewModel.handle(LoginAction2.LoginWithToken(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRegistrationStageNotSupported() {
|
private fun onRegistrationStageNotSupported() {
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(activity)
|
||||||
.setTitle(R.string.app_name)
|
.setTitle(R.string.app_name)
|
||||||
.setMessage(getString(R.string.login_registration_not_supported))
|
.setMessage(activity.getString(R.string.login_registration_not_supported))
|
||||||
.setPositiveButton(R.string.yes) { _, _ ->
|
.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginWebFragment2::class.java,
|
LoginWebFragment2::class.java,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
}
|
}
|
||||||
|
@ -321,11 +331,11 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onLoginModeNotSupported(supportedTypes: List<String>) {
|
private fun onLoginModeNotSupported(supportedTypes: List<String>) {
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(activity)
|
||||||
.setTitle(R.string.app_name)
|
.setTitle(R.string.app_name)
|
||||||
.setMessage(getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
|
.setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" }))
|
||||||
.setPositiveButton(R.string.yes) { _, _ ->
|
.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
addFragmentToBackstack(views.loginFragmentContainer,
|
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginWebFragment2::class.java,
|
LoginWebFragment2::class.java,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
}
|
}
|
||||||
|
@ -355,53 +365,27 @@ open class LoginActivity2 : VectorBaseActivity<ActivityLoginBinding>(), ToolbarC
|
||||||
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
|
|
||||||
when (stage) {
|
when (stage) {
|
||||||
is Stage.ReCaptcha -> addFragmentToBackstack(views.loginFragmentContainer,
|
is Stage.ReCaptcha -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginCaptchaFragment2::class.java,
|
LoginCaptchaFragment2::class.java,
|
||||||
LoginCaptchaFragmentArgument(stage.publicKey),
|
LoginCaptchaFragmentArgument(stage.publicKey),
|
||||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
is Stage.Email -> addFragmentToBackstack(views.loginFragmentContainer,
|
is Stage.Email -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginGenericTextInputFormFragment2::class.java,
|
LoginGenericTextInputFormFragment2::class.java,
|
||||||
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
|
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
|
||||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
is Stage.Msisdn -> addFragmentToBackstack(views.loginFragmentContainer,
|
is Stage.Msisdn -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginGenericTextInputFormFragment2::class.java,
|
LoginGenericTextInputFormFragment2::class.java,
|
||||||
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
|
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
|
||||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
is Stage.Terms -> addFragmentToBackstack(views.loginFragmentContainer,
|
is Stage.Terms -> activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||||
LoginTermsFragment2::class.java,
|
LoginTermsFragment2::class.java,
|
||||||
LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))),
|
LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))),
|
||||||
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
|
||||||
option = commonOption)
|
option = commonOption)
|
||||||
else -> Unit // Should not happen
|
else -> Unit // Should not happen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun configure(toolbar: MaterialToolbar) {
|
|
||||||
configureToolbar(toolbar)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG"
|
|
||||||
private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG"
|
|
||||||
|
|
||||||
private const val EXTRA_CONFIG = "EXTRA_CONFIG"
|
|
||||||
|
|
||||||
// Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string
|
|
||||||
const val VECTOR_REDIRECT_URL = "element://connect"
|
|
||||||
|
|
||||||
fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
|
|
||||||
return Intent(context, LoginActivity2::class.java).apply {
|
|
||||||
putExtra(EXTRA_CONFIG, loginConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun redirectIntent(context: Context, data: Uri?): Intent {
|
|
||||||
return Intent(context, LoginActivity2::class.java).apply {
|
|
||||||
setData(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1450,7 +1450,7 @@ class TimelineFragment @Inject constructor(
|
||||||
AttachmentTypeSelectorView.Type.POLL,
|
AttachmentTypeSelectorView.Type.POLL,
|
||||||
vectorPreferences.labsEnablePolls() && !isThreadTimeLine())
|
vectorPreferences.labsEnablePolls() && !isThreadTimeLine())
|
||||||
}
|
}
|
||||||
attachmentTypeSelector.show(views.composerLayout.views.attachmentButton, keyboardStateUtils.isKeyboardShowing)
|
attachmentTypeSelector.show(views.composerLayout.views.attachmentButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSendMessage(text: CharSequence) {
|
override fun onSendMessage(text: CharSequence) {
|
||||||
|
|
|
@ -24,18 +24,21 @@ import android.text.Editable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputConnection
|
import android.view.inputmethod.InputConnection
|
||||||
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
import androidx.core.view.OnReceiveContentListener
|
import androidx.core.view.OnReceiveContentListener
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||||
import com.vanniktech.emoji.EmojiEditText
|
|
||||||
import im.vector.app.core.extensions.ooi
|
import im.vector.app.core.extensions.ooi
|
||||||
import im.vector.app.core.platform.SimpleTextWatcher
|
import im.vector.app.core.platform.SimpleTextWatcher
|
||||||
import im.vector.app.features.html.PillImageSpan
|
import im.vector.app.features.html.PillImageSpan
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle) :
|
class ComposerEditText @JvmOverloads constructor(
|
||||||
EmojiEditText(context, attrs, defStyleAttr) {
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = android.R.attr.editTextStyle
|
||||||
|
) : AppCompatEditText(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onRichContentSelected(contentUri: Uri): Boolean
|
fun onRichContentSelected(contentUri: Uri): Boolean
|
||||||
|
|
|
@ -44,7 +44,7 @@ class EncryptionItemFactory @Inject constructor(
|
||||||
if (!event.root.isStateEvent()) {
|
if (!event.root.isStateEvent()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm
|
val algorithm = event.root.content.toModel<EncryptionEventContent>()?.algorithm
|
||||||
val informationData = informationDataFactory.create(params)
|
val informationData = informationDataFactory.create(params)
|
||||||
val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
|
val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ class RoomCreateItemFactory @Inject constructor(private val stringProvider: Stri
|
||||||
|
|
||||||
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
||||||
val event = params.event
|
val event = params.event
|
||||||
val createRoomContent = event.root.getClearContent().toModel<RoomCreateContent>() ?: return null
|
val createRoomContent = event.root.content.toModel<RoomCreateContent>() ?: return null
|
||||||
val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(params)
|
val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(params)
|
||||||
val roomLink = session.permalinkService().createRoomPermalink(predecessorId) ?: return null
|
val roomLink = session.permalinkService().createRoomPermalink(predecessorId) ?: return null
|
||||||
val text = span {
|
val text = span {
|
||||||
|
|
|
@ -54,67 +54,83 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||||
params.rootThreadEventId,
|
params.rootThreadEventId,
|
||||||
params.isFromThreadTimeline())
|
params.isFromThreadTimeline())
|
||||||
}
|
}
|
||||||
when (event.root.getClearType()) {
|
|
||||||
// Message itemsX
|
// Manage state event differently, to check validity
|
||||||
EventType.STICKER,
|
if (event.root.isStateEvent()) {
|
||||||
EventType.POLL_START,
|
// state event are not e2e
|
||||||
EventType.MESSAGE -> messageItemFactory.create(params)
|
when (event.root.type) {
|
||||||
EventType.STATE_ROOM_TOMBSTONE,
|
EventType.STATE_ROOM_TOMBSTONE,
|
||||||
EventType.STATE_ROOM_NAME,
|
EventType.STATE_ROOM_NAME,
|
||||||
EventType.STATE_ROOM_TOPIC,
|
EventType.STATE_ROOM_TOPIC,
|
||||||
EventType.STATE_ROOM_AVATAR,
|
EventType.STATE_ROOM_AVATAR,
|
||||||
EventType.STATE_ROOM_MEMBER,
|
EventType.STATE_ROOM_MEMBER,
|
||||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
EventType.STATE_ROOM_CANONICAL_ALIAS,
|
EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||||
EventType.STATE_ROOM_JOIN_RULES,
|
EventType.STATE_ROOM_JOIN_RULES,
|
||||||
EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||||
EventType.STATE_ROOM_SERVER_ACL,
|
EventType.STATE_ROOM_SERVER_ACL,
|
||||||
EventType.STATE_ROOM_GUEST_ACCESS,
|
EventType.STATE_ROOM_GUEST_ACCESS,
|
||||||
EventType.REDACTION,
|
EventType.STATE_ROOM_ALIASES,
|
||||||
EventType.STATE_ROOM_ALIASES,
|
EventType.STATE_SPACE_CHILD,
|
||||||
EventType.KEY_VERIFICATION_ACCEPT,
|
EventType.STATE_SPACE_PARENT,
|
||||||
EventType.KEY_VERIFICATION_START,
|
EventType.STATE_ROOM_POWER_LEVELS -> {
|
||||||
EventType.KEY_VERIFICATION_KEY,
|
noticeItemFactory.create(params)
|
||||||
EventType.KEY_VERIFICATION_READY,
|
}
|
||||||
EventType.KEY_VERIFICATION_MAC,
|
EventType.STATE_ROOM_WIDGET_LEGACY,
|
||||||
EventType.CALL_CANDIDATES,
|
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params)
|
||||||
EventType.CALL_REPLACES,
|
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params)
|
||||||
EventType.CALL_SELECT_ANSWER,
|
// State room create
|
||||||
EventType.CALL_NEGOTIATE,
|
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params)
|
||||||
EventType.REACTION,
|
// Unhandled state event types
|
||||||
EventType.STATE_SPACE_CHILD,
|
else -> {
|
||||||
EventType.STATE_SPACE_PARENT,
|
// Should only happen when shouldShowHiddenEvents() settings is ON
|
||||||
EventType.STATE_ROOM_POWER_LEVELS,
|
Timber.v("State event type ${event.root.type} not handled")
|
||||||
EventType.POLL_RESPONSE,
|
defaultItemFactory.create(params)
|
||||||
EventType.POLL_END -> noticeItemFactory.create(params)
|
|
||||||
EventType.STATE_ROOM_WIDGET_LEGACY,
|
|
||||||
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params)
|
|
||||||
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params)
|
|
||||||
// State room create
|
|
||||||
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params)
|
|
||||||
// Calls
|
|
||||||
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,
|
} else {
|
||||||
EventType.KEY_VERIFICATION_DONE -> {
|
when (event.root.getClearType()) {
|
||||||
verificationConclusionItemFactory.create(params)
|
// Message itemsX
|
||||||
}
|
EventType.STICKER,
|
||||||
// Unhandled event types
|
EventType.POLL_START,
|
||||||
else -> {
|
EventType.MESSAGE -> messageItemFactory.create(params)
|
||||||
// Should only happen when shouldShowHiddenEvents() settings is ON
|
EventType.REDACTION,
|
||||||
Timber.v("Type ${event.root.getClearType()} not handled")
|
EventType.KEY_VERIFICATION_ACCEPT,
|
||||||
defaultItemFactory.create(params)
|
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) {
|
} catch (throwable: Throwable) {
|
||||||
|
|
|
@ -41,7 +41,7 @@ class WidgetItemFactory @Inject constructor(
|
||||||
|
|
||||||
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
||||||
val event = params.event
|
val event = params.event
|
||||||
val widgetContent: WidgetContent = event.root.getClearContent().toModel() ?: return null
|
val widgetContent: WidgetContent = event.root.content.toModel() ?: return null
|
||||||
val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel()
|
val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel()
|
||||||
|
|
||||||
return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) {
|
return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) {
|
||||||
|
|
|
@ -114,7 +114,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? {
|
private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? {
|
||||||
val powerLevelsContent: PowerLevelsContent = event.getClearContent().toModel() ?: return null
|
val powerLevelsContent: PowerLevelsContent = event.content.toModel() ?: return null
|
||||||
val previousPowerLevelsContent: PowerLevelsContent = event.resolvedPrevContent().toModel() ?: return null
|
val previousPowerLevelsContent: PowerLevelsContent = event.resolvedPrevContent().toModel() ?: return null
|
||||||
val userIds = HashSet<String>()
|
val userIds = HashSet<String>()
|
||||||
userIds.addAll(powerLevelsContent.users.orEmpty().keys)
|
userIds.addAll(powerLevelsContent.users.orEmpty().keys)
|
||||||
|
@ -142,7 +142,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatWidgetEvent(event: Event, disambiguatedDisplayName: String): CharSequence? {
|
private fun formatWidgetEvent(event: Event, disambiguatedDisplayName: String): CharSequence? {
|
||||||
val widgetContent: WidgetContent = event.getClearContent().toModel() ?: return null
|
val widgetContent: WidgetContent = event.content.toModel() ?: return null
|
||||||
val previousWidgetContent: WidgetContent? = event.resolvedPrevContent().toModel()
|
val previousWidgetContent: WidgetContent? = event.resolvedPrevContent().toModel()
|
||||||
return if (widgetContent.isActive()) {
|
return if (widgetContent.isActive()) {
|
||||||
val widgetName = widgetContent.getHumanName()
|
val widgetName = widgetContent.getHumanName()
|
||||||
|
@ -198,7 +198,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? {
|
private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? {
|
||||||
return event.getClearContent().toModel<RoomCreateContent>()
|
return event.content.toModel<RoomCreateContent>()
|
||||||
?.takeIf { it.creator.isNullOrBlank().not() }
|
?.takeIf { it.creator.isNullOrBlank().not() }
|
||||||
?.let {
|
?.let {
|
||||||
if (event.isSentByCurrentUser()) {
|
if (event.isSentByCurrentUser()) {
|
||||||
|
@ -210,7 +210,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
|
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
|
||||||
val content = event.getClearContent().toModel<RoomNameContent>() ?: return null
|
val content = event.content.toModel<RoomNameContent>() ?: return null
|
||||||
return if (content.name.isNullOrBlank()) {
|
return if (content.name.isNullOrBlank()) {
|
||||||
if (event.isSentByCurrentUser()) {
|
if (event.isSentByCurrentUser()) {
|
||||||
sp.getString(R.string.notice_room_name_removed_by_you)
|
sp.getString(R.string.notice_room_name_removed_by_you)
|
||||||
|
@ -235,7 +235,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
|
private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
|
||||||
val content = event.getClearContent().toModel<RoomTopicContent>() ?: return null
|
val content = event.content.toModel<RoomTopicContent>() ?: return null
|
||||||
return if (content.topic.isNullOrEmpty()) {
|
return if (content.topic.isNullOrEmpty()) {
|
||||||
if (event.isSentByCurrentUser()) {
|
if (event.isSentByCurrentUser()) {
|
||||||
sp.getString(R.string.notice_room_topic_removed_by_you)
|
sp.getString(R.string.notice_room_topic_removed_by_you)
|
||||||
|
@ -252,7 +252,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomAvatarEvent(event: Event, senderName: String?): CharSequence? {
|
private fun formatRoomAvatarEvent(event: Event, senderName: String?): CharSequence? {
|
||||||
val content = event.getClearContent().toModel<RoomAvatarContent>() ?: return null
|
val content = event.content.toModel<RoomAvatarContent>() ?: return null
|
||||||
return if (content.avatarUrl.isNullOrEmpty()) {
|
return if (content.avatarUrl.isNullOrEmpty()) {
|
||||||
if (event.isSentByCurrentUser()) {
|
if (event.isSentByCurrentUser()) {
|
||||||
sp.getString(R.string.notice_room_avatar_removed_by_you)
|
sp.getString(R.string.notice_room_avatar_removed_by_you)
|
||||||
|
@ -269,7 +269,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
|
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
|
||||||
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
|
val historyVisibility = event.content.toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
|
||||||
|
|
||||||
val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility)
|
val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility)
|
||||||
return if (event.isSentByCurrentUser()) {
|
return if (event.isSentByCurrentUser()) {
|
||||||
|
@ -282,7 +282,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
|
private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
|
||||||
val content = event.getClearContent().toModel<RoomThirdPartyInviteContent>()
|
val content = event.content.toModel<RoomThirdPartyInviteContent>()
|
||||||
val prevContent = event.resolvedPrevContent()?.toModel<RoomThirdPartyInviteContent>()
|
val prevContent = event.resolvedPrevContent()?.toModel<RoomThirdPartyInviteContent>()
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
|
@ -363,7 +363,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomMemberEvent(event: Event, senderName: String?, isDm: Boolean): String? {
|
private fun formatRoomMemberEvent(event: Event, senderName: String?, isDm: Boolean): String? {
|
||||||
val eventContent: RoomMemberContent? = event.getClearContent().toModel()
|
val eventContent: RoomMemberContent? = event.content.toModel()
|
||||||
val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel()
|
val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel()
|
||||||
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership ||
|
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership ||
|
||||||
eventContent?.membership == Membership.LEAVE
|
eventContent?.membership == Membership.LEAVE
|
||||||
|
@ -375,7 +375,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomAliasesEvent(event: Event, senderName: String?): String? {
|
private fun formatRoomAliasesEvent(event: Event, senderName: String?): String? {
|
||||||
val eventContent: RoomAliasesContent? = event.getClearContent().toModel()
|
val eventContent: RoomAliasesContent? = event.content.toModel()
|
||||||
val prevEventContent: RoomAliasesContent? = event.resolvedPrevContent()?.toModel()
|
val prevEventContent: RoomAliasesContent? = event.resolvedPrevContent()?.toModel()
|
||||||
|
|
||||||
val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty()
|
val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty()
|
||||||
|
@ -408,7 +408,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomServerAclEvent(event: Event, senderName: String?): String? {
|
private fun formatRoomServerAclEvent(event: Event, senderName: String?): String? {
|
||||||
val eventContent = event.getClearContent().toModel<RoomServerAclContent>() ?: return null
|
val eventContent = event.content.toModel<RoomServerAclContent>() ?: return null
|
||||||
val prevEventContent = event.resolvedPrevContent()?.toModel<RoomServerAclContent>()
|
val prevEventContent = event.resolvedPrevContent()?.toModel<RoomServerAclContent>()
|
||||||
|
|
||||||
return buildString {
|
return buildString {
|
||||||
|
@ -481,7 +481,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomCanonicalAliasEvent(event: Event, senderName: String?): String? {
|
private fun formatRoomCanonicalAliasEvent(event: Event, senderName: String?): String? {
|
||||||
val eventContent: RoomCanonicalAliasContent? = event.getClearContent().toModel()
|
val eventContent: RoomCanonicalAliasContent? = event.content.toModel()
|
||||||
val prevContent: RoomCanonicalAliasContent? = event.resolvedPrevContent().toModel()
|
val prevContent: RoomCanonicalAliasContent? = event.resolvedPrevContent().toModel()
|
||||||
val canonicalAlias = eventContent?.canonicalAlias?.takeIf { it.isNotEmpty() }
|
val canonicalAlias = eventContent?.canonicalAlias?.takeIf { it.isNotEmpty() }
|
||||||
val prevCanonicalAlias = prevContent?.canonicalAlias?.takeIf { it.isNotEmpty() }
|
val prevCanonicalAlias = prevContent?.canonicalAlias?.takeIf { it.isNotEmpty() }
|
||||||
|
@ -551,7 +551,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, isDm: Boolean): String? {
|
private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, isDm: Boolean): String? {
|
||||||
val eventContent: RoomGuestAccessContent? = event.getClearContent().toModel()
|
val eventContent: RoomGuestAccessContent? = event.content.toModel()
|
||||||
return when (eventContent?.guestAccess) {
|
return when (eventContent?.guestAccess) {
|
||||||
GuestAccess.CanJoin ->
|
GuestAccess.CanJoin ->
|
||||||
if (event.isSentByCurrentUser()) {
|
if (event.isSentByCurrentUser()) {
|
||||||
|
@ -815,7 +815,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatJoinRulesEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
|
private fun formatJoinRulesEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
|
||||||
val content = event.getClearContent().toModel<RoomJoinRulesContent>() ?: return null
|
val content = event.content.toModel<RoomJoinRulesContent>() ?: return null
|
||||||
return when (content.joinRules) {
|
return when (content.joinRules) {
|
||||||
RoomJoinRules.INVITE ->
|
RoomJoinRules.INVITE ->
|
||||||
if (event.isSentByCurrentUser()) {
|
if (event.isSentByCurrentUser()) {
|
||||||
|
|
|
@ -53,10 +53,10 @@ import timber.log.Timber
|
||||||
class RoomListViewModel @AssistedInject constructor(
|
class RoomListViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: RoomListViewState,
|
@Assisted initialState: RoomListViewState,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val stringProvider: StringProvider,
|
stringProvider: StringProvider,
|
||||||
private val appStateHandler: AppStateHandler,
|
appStateHandler: AppStateHandler,
|
||||||
private val vectorPreferences: VectorPreferences,
|
vectorPreferences: VectorPreferences,
|
||||||
private val autoAcceptInvites: AutoAcceptInvites
|
autoAcceptInvites: AutoAcceptInvites
|
||||||
) : VectorViewModel<RoomListViewState, RoomListAction, RoomListViewEvents>(initialState) {
|
) : VectorViewModel<RoomListViewState, RoomListAction, RoomListViewEvents>(initialState) {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
|
@ -88,7 +88,7 @@ abstract class AbstractSSOLoginFragment<VB : ViewBinding> : AbstractLoginFragmen
|
||||||
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
|
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
|
||||||
// in this case we can prefetch (not other cases for privacy concerns)
|
// in this case we can prefetch (not other cases for privacy concerns)
|
||||||
loginViewModel.getSsoUrl(
|
loginViewModel.getSsoUrl(
|
||||||
redirectUrl = LoginActivity.VECTOR_REDIRECT_URL,
|
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||||
deviceId = state.deviceId,
|
deviceId = state.deviceId,
|
||||||
providerId = null
|
providerId = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -356,9 +356,6 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), ToolbarCo
|
||||||
|
|
||||||
private const val EXTRA_CONFIG = "EXTRA_CONFIG"
|
private const val EXTRA_CONFIG = "EXTRA_CONFIG"
|
||||||
|
|
||||||
// Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string
|
|
||||||
const val VECTOR_REDIRECT_URL = "element://connect"
|
|
||||||
|
|
||||||
fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
|
fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
|
||||||
return Intent(context, LoginActivity::class.java).apply {
|
return Intent(context, LoginActivity::class.java).apply {
|
||||||
putExtra(EXTRA_CONFIG, loginConfig)
|
putExtra(EXTRA_CONFIG, loginConfig)
|
||||||
|
|
|
@ -200,7 +200,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
|
||||||
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||||
override fun onProviderSelected(id: String?) {
|
override fun onProviderSelected(id: String?) {
|
||||||
loginViewModel.getSsoUrl(
|
loginViewModel.getSsoUrl(
|
||||||
redirectUrl = LoginActivity.VECTOR_REDIRECT_URL,
|
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||||
deviceId = state.deviceId,
|
deviceId = state.deviceId,
|
||||||
providerId = id
|
providerId = id
|
||||||
)
|
)
|
||||||
|
|
|
@ -76,7 +76,7 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi
|
||||||
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||||
override fun onProviderSelected(id: String?) {
|
override fun onProviderSelected(id: String?) {
|
||||||
loginViewModel.getSsoUrl(
|
loginViewModel.getSsoUrl(
|
||||||
redirectUrl = LoginActivity.VECTOR_REDIRECT_URL,
|
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||||
deviceId = state.deviceId,
|
deviceId = state.deviceId,
|
||||||
providerId = id
|
providerId = id
|
||||||
)
|
)
|
||||||
|
@ -109,7 +109,7 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi
|
||||||
private fun submit() = withState(loginViewModel) { state ->
|
private fun submit() = withState(loginViewModel) { state ->
|
||||||
if (state.loginMode is LoginMode.Sso) {
|
if (state.loginMode is LoginMode.Sso) {
|
||||||
loginViewModel.getSsoUrl(
|
loginViewModel.getSsoUrl(
|
||||||
redirectUrl = LoginActivity.VECTOR_REDIRECT_URL,
|
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||||
deviceId = state.deviceId,
|
deviceId = state.deviceId,
|
||||||
providerId = null
|
providerId = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,4 +32,9 @@ class SSORedirectRouterActivity : AppCompatActivity() {
|
||||||
navigator.loginSSORedirect(this, intent.data)
|
navigator.loginSSORedirect(this, intent.data)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string
|
||||||
|
const val VECTOR_REDIRECT_URL = "element://connect"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import androidx.browser.customtabs.CustomTabsSession
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||||
|
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||||
import im.vector.app.features.login.hasSso
|
import im.vector.app.features.login.hasSso
|
||||||
import im.vector.app.features.login.ssoIdentityProviders
|
import im.vector.app.features.login.ssoIdentityProviders
|
||||||
|
|
||||||
|
@ -90,7 +91,7 @@ abstract class AbstractSSOLoginFragment2<VB : ViewBinding> : AbstractLoginFragme
|
||||||
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
|
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
|
||||||
// in this case we can prefetch (not other cases for privacy concerns)
|
// in this case we can prefetch (not other cases for privacy concerns)
|
||||||
loginViewModel.getSsoUrl(
|
loginViewModel.getSsoUrl(
|
||||||
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL,
|
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||||
deviceId = state.deviceId,
|
deviceId = state.deviceId,
|
||||||
providerId = null
|
providerId = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -30,6 +30,7 @@ import im.vector.app.core.extensions.hideKeyboard
|
||||||
import im.vector.app.core.extensions.toReducedUrl
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.databinding.FragmentLoginSignupUsername2Binding
|
import im.vector.app.databinding.FragmentLoginSignupUsername2Binding
|
||||||
import im.vector.app.features.login.LoginMode
|
import im.vector.app.features.login.LoginMode
|
||||||
|
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||||
import im.vector.app.features.login.SocialLoginButtonsView
|
import im.vector.app.features.login.SocialLoginButtonsView
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
@ -97,7 +98,7 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm
|
||||||
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||||
override fun onProviderSelected(id: String?) {
|
override fun onProviderSelected(id: String?) {
|
||||||
loginViewModel.getSsoUrl(
|
loginViewModel.getSsoUrl(
|
||||||
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL,
|
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||||
deviceId = state.deviceId,
|
deviceId = state.deviceId,
|
||||||
providerId = id
|
providerId = id
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,6 +31,7 @@ import im.vector.app.core.extensions.hidePassword
|
||||||
import im.vector.app.core.extensions.toReducedUrl
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.databinding.FragmentLoginSigninToAny2Binding
|
import im.vector.app.databinding.FragmentLoginSigninToAny2Binding
|
||||||
import im.vector.app.features.login.LoginMode
|
import im.vector.app.features.login.LoginMode
|
||||||
|
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||||
import im.vector.app.features.login.SocialLoginButtonsView
|
import im.vector.app.features.login.SocialLoginButtonsView
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
@ -124,7 +125,7 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2<Frag
|
||||||
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||||
override fun onProviderSelected(id: String?) {
|
override fun onProviderSelected(id: String?) {
|
||||||
loginViewModel.getSsoUrl(
|
loginViewModel.getSsoUrl(
|
||||||
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL,
|
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||||
deviceId = state.deviceId,
|
deviceId = state.deviceId,
|
||||||
providerId = id
|
providerId = id
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,6 +24,7 @@ import com.airbnb.mvrx.withState
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.toReducedUrl
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.databinding.FragmentLoginSsoOnly2Binding
|
import im.vector.app.databinding.FragmentLoginSsoOnly2Binding
|
||||||
|
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,7 +52,7 @@ class LoginSsoOnlyFragment2 @Inject constructor() : AbstractSSOLoginFragment2<Fr
|
||||||
|
|
||||||
private fun submit() = withState(loginViewModel) { state ->
|
private fun submit() = withState(loginViewModel) { state ->
|
||||||
loginViewModel.getSsoUrl(
|
loginViewModel.getSsoUrl(
|
||||||
redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL,
|
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||||
deviceId = state.deviceId,
|
deviceId = state.deviceId,
|
||||||
providerId = null
|
providerId = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -34,11 +34,11 @@ import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.databinding.DialogBaseEditTextBinding
|
import im.vector.app.databinding.DialogBaseEditTextBinding
|
||||||
import im.vector.app.databinding.FragmentLoginAccountCreatedBinding
|
import im.vector.app.databinding.FragmentLoginAccountCreatedBinding
|
||||||
import im.vector.app.features.displayname.getBestName
|
import im.vector.app.features.displayname.getBestName
|
||||||
|
import im.vector.app.features.ftue.FTUEActivity
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||||
import im.vector.app.features.login2.AbstractLoginFragment2
|
import im.vector.app.features.login2.AbstractLoginFragment2
|
||||||
import im.vector.app.features.login2.LoginAction2
|
import im.vector.app.features.login2.LoginAction2
|
||||||
import im.vector.app.features.login2.LoginActivity2
|
|
||||||
import im.vector.app.features.login2.LoginViewState2
|
import im.vector.app.features.login2.LoginViewState2
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
@ -130,7 +130,7 @@ class AccountCreatedFragment @Inject constructor(
|
||||||
|
|
||||||
private fun invalidateState(state: AccountCreatedViewState) {
|
private fun invalidateState(state: AccountCreatedViewState) {
|
||||||
// Ugly hack...
|
// Ugly hack...
|
||||||
(activity as? LoginActivity2)?.setIsLoading(state.isLoading)
|
(activity as? FTUEActivity)?.setIsLoading(state.isLoading)
|
||||||
|
|
||||||
views.loginAccountCreatedSubtitle.text = getString(R.string.login_account_created_subtitle, state.userId)
|
views.loginAccountCreatedSubtitle.text = getString(R.string.login_account_created_subtitle, state.userId)
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ import im.vector.app.features.crypto.verification.SupportedVerificationMethodsPr
|
||||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||||
import im.vector.app.features.debug.DebugMenuActivity
|
import im.vector.app.features.debug.DebugMenuActivity
|
||||||
import im.vector.app.features.devtools.RoomDevToolActivity
|
import im.vector.app.features.devtools.RoomDevToolActivity
|
||||||
|
import im.vector.app.features.ftue.FTUEActivity
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
||||||
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
|
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
|
||||||
import im.vector.app.features.home.room.detail.search.SearchActivity
|
import im.vector.app.features.home.room.detail.search.SearchActivity
|
||||||
|
@ -62,7 +63,6 @@ import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
|
||||||
import im.vector.app.features.invite.InviteUsersToRoomActivity
|
import im.vector.app.features.invite.InviteUsersToRoomActivity
|
||||||
import im.vector.app.features.login.LoginActivity
|
import im.vector.app.features.login.LoginActivity
|
||||||
import im.vector.app.features.login.LoginConfig
|
import im.vector.app.features.login.LoginConfig
|
||||||
import im.vector.app.features.login2.LoginActivity2
|
|
||||||
import im.vector.app.features.matrixto.MatrixToBottomSheet
|
import im.vector.app.features.matrixto.MatrixToBottomSheet
|
||||||
import im.vector.app.features.media.AttachmentData
|
import im.vector.app.features.media.AttachmentData
|
||||||
import im.vector.app.features.media.BigImageViewerActivity
|
import im.vector.app.features.media.BigImageViewerActivity
|
||||||
|
@ -84,7 +84,6 @@ import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.features.settings.VectorSettingsActivity
|
import im.vector.app.features.settings.VectorSettingsActivity
|
||||||
import im.vector.app.features.share.SharedData
|
import im.vector.app.features.share.SharedData
|
||||||
import im.vector.app.features.signout.soft.SoftLogoutActivity
|
import im.vector.app.features.signout.soft.SoftLogoutActivity
|
||||||
import im.vector.app.features.signout.soft.SoftLogoutActivity2
|
|
||||||
import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet
|
import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet
|
||||||
import im.vector.app.features.spaces.SpaceExploreActivity
|
import im.vector.app.features.spaces.SpaceExploreActivity
|
||||||
import im.vector.app.features.spaces.SpacePreviewActivity
|
import im.vector.app.features.spaces.SpacePreviewActivity
|
||||||
|
@ -115,27 +114,26 @@ class DefaultNavigator @Inject constructor(
|
||||||
) : Navigator {
|
) : Navigator {
|
||||||
|
|
||||||
override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) {
|
override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) {
|
||||||
val intent = when (features.loginVersion()) {
|
val intent = when (features.loginVariant()) {
|
||||||
VectorFeatures.LoginVersion.V1 -> LoginActivity.newIntent(context, loginConfig)
|
VectorFeatures.LoginVariant.LEGACY -> LoginActivity.newIntent(context, loginConfig)
|
||||||
VectorFeatures.LoginVersion.V2 -> LoginActivity2.newIntent(context, loginConfig)
|
VectorFeatures.LoginVariant.FTUE,
|
||||||
|
VectorFeatures.LoginVariant.FTUE_WIP -> FTUEActivity.newIntent(context, loginConfig)
|
||||||
}
|
}
|
||||||
intent.addFlags(flags)
|
intent.addFlags(flags)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loginSSORedirect(context: Context, data: Uri?) {
|
override fun loginSSORedirect(context: Context, data: Uri?) {
|
||||||
val intent = when (features.loginVersion()) {
|
val intent = when (features.loginVariant()) {
|
||||||
VectorFeatures.LoginVersion.V1 -> LoginActivity.redirectIntent(context, data)
|
VectorFeatures.LoginVariant.LEGACY -> LoginActivity.redirectIntent(context, data)
|
||||||
VectorFeatures.LoginVersion.V2 -> LoginActivity2.redirectIntent(context, data)
|
VectorFeatures.LoginVariant.FTUE,
|
||||||
|
VectorFeatures.LoginVariant.FTUE_WIP -> FTUEActivity.redirectIntent(context, data)
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun softLogout(context: Context) {
|
override fun softLogout(context: Context) {
|
||||||
val intent = when (features.loginVersion()) {
|
val intent = SoftLogoutActivity.newIntent(context)
|
||||||
VectorFeatures.LoginVersion.V1 -> SoftLogoutActivity.newIntent(context)
|
|
||||||
VectorFeatures.LoginVersion.V2 -> SoftLogoutActivity2.newIntent(context)
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,12 +66,10 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy)
|
return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy)
|
||||||
}
|
}
|
||||||
val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null
|
val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null
|
||||||
when (event.getClearType()) {
|
return when (event.getClearType()) {
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE,
|
||||||
return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
|
|
||||||
}
|
|
||||||
EventType.ENCRYPTED -> {
|
EventType.ENCRYPTED -> {
|
||||||
return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
|
resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// If the event can be displayed, display it as is
|
// If the event can be displayed, display it as is
|
||||||
|
@ -79,7 +77,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
// TODO Better event text display
|
// TODO Better event text display
|
||||||
val bodyPreview = event.type ?: EventType.MISSING_TYPE
|
val bodyPreview = event.type ?: EventType.MISSING_TYPE
|
||||||
|
|
||||||
return SimpleNotifiableEvent(
|
SimpleNotifiableEvent(
|
||||||
session.myUserId,
|
session.myUserId,
|
||||||
eventId = event.eventId!!,
|
eventId = event.eventId!!,
|
||||||
editedEventId = timelineEvent.getEditedEventId(),
|
editedEventId = timelineEvent.getEditedEventId(),
|
||||||
|
@ -126,18 +124,18 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent {
|
private suspend fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? {
|
||||||
// The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
|
// The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
|
||||||
val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
|
val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
|
||||||
|
|
||||||
if (room == null) {
|
return if (room == null) {
|
||||||
Timber.e("## Unable to resolve room for eventId [$event]")
|
Timber.e("## Unable to resolve room for eventId [$event]")
|
||||||
// Ok room is not known in store, but we can still display something
|
// Ok room is not known in store, but we can still display something
|
||||||
val body = displayableEventFormatter.format(event, isDm = false, appendAuthor = false)
|
val body = displayableEventFormatter.format(event, isDm = false, appendAuthor = false)
|
||||||
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
|
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
|
||||||
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
||||||
|
|
||||||
return NotifiableMessageEvent(
|
NotifiableMessageEvent(
|
||||||
eventId = event.root.eventId!!,
|
eventId = event.root.eventId!!,
|
||||||
editedEventId = event.getEditedEventId(),
|
editedEventId = event.getEditedEventId(),
|
||||||
canBeReplaced = canBeReplaced,
|
canBeReplaced = canBeReplaced,
|
||||||
|
@ -152,51 +150,60 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
matrixID = session.myUserId
|
matrixID = session.myUserId
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if (event.root.isEncrypted() && event.root.mxDecryptionResult == null) {
|
event.attemptToDecryptIfNeeded(session)
|
||||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
// only convert encrypted messages to NotifiableMessageEvents
|
||||||
// for now decrypt sync
|
when (event.root.getClearType()) {
|
||||||
try {
|
EventType.MESSAGE -> {
|
||||||
val result = session.cryptoService().decryptEvent(event.root, event.root.roomId + UUID.randomUUID().toString())
|
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
|
||||||
event.root.mxDecryptionResult = OlmDecryptionResult(
|
val roomName = room.roomSummary()?.displayName ?: ""
|
||||||
payload = result.clearEvent,
|
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
||||||
senderKey = result.senderCurve25519Key,
|
|
||||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
NotifiableMessageEvent(
|
||||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
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()
|
private fun TimelineEvent.attemptToDecryptIfNeeded(session: Session) {
|
||||||
val roomName = room.roomSummary()?.displayName ?: ""
|
if (root.isEncrypted() && root.mxDecryptionResult == null) {
|
||||||
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||||
|
// for now decrypt sync
|
||||||
return NotifiableMessageEvent(
|
try {
|
||||||
eventId = event.root.eventId!!,
|
val result = session.cryptoService().decryptEvent(root, root.roomId + UUID.randomUUID().toString())
|
||||||
editedEventId = event.getEditedEventId(),
|
root.mxDecryptionResult = OlmDecryptionResult(
|
||||||
canBeReplaced = canBeReplaced,
|
payload = result.clearEvent,
|
||||||
timestamp = event.root.originServerTs ?: 0,
|
senderKey = result.senderCurve25519Key,
|
||||||
noisy = isNoisy,
|
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||||
senderName = senderDisplayName,
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
senderId = event.root.senderId,
|
)
|
||||||
body = body,
|
} catch (e: MXCryptoError) {
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||||
const val SETTINGS_HOME_SERVER_PREFERENCE_KEY = "SETTINGS_HOME_SERVER_PREFERENCE_KEY"
|
const val SETTINGS_HOME_SERVER_PREFERENCE_KEY = "SETTINGS_HOME_SERVER_PREFERENCE_KEY"
|
||||||
const val SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY"
|
const val SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY"
|
||||||
const val SETTINGS_DISCOVERY_PREFERENCE_KEY = "SETTINGS_DISCOVERY_PREFERENCE_KEY"
|
const val SETTINGS_DISCOVERY_PREFERENCE_KEY = "SETTINGS_DISCOVERY_PREFERENCE_KEY"
|
||||||
|
const val SETTINGS_EMAILS_AND_PHONE_NUMBERS_PREFERENCE_KEY = "SETTINGS_EMAILS_AND_PHONE_NUMBERS_PREFERENCE_KEY"
|
||||||
|
|
||||||
const val SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY"
|
const val SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY"
|
||||||
const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY"
|
const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY"
|
||||||
|
|
|
@ -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 |
|
@ -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>
|
|
@ -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>
|
|
|
@ -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>
|
|
@ -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>
|
|
|
@ -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>
|
|
@ -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>
|
|
|
@ -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>
|
|
@ -5,6 +5,6 @@
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
android:pathData="M10.5,2C10.2239,2 10,2.2239 10,2.5V22H14V2.5C14,2.2239 13.7761,2 13.5,2H10.5ZM3,9.5C3,9.2239 3.2239,9 3.5,9H6.5C6.7761,9 7,9.2239 7,9.5V22H3V9.5ZM17,13.5C17,13.2239 17.2239,13 17.5,13H20.5C20.7761,13 21,13.2239 21,13.5V22H17V13.5Z"
|
android:pathData="M10.5,2C10.2239,2 10,2.2239 10,2.5V22H14V2.5C14,2.2239 13.7761,2 13.5,2H10.5ZM3,9.5C3,9.2239 3.2239,9 3.5,9H6.5C6.7761,9 7,9.2239 7,9.5V22H3V9.5ZM17,13.5C17,13.2239 17.2239,13 17.5,13H20.5C20.7761,13 21,13.2239 21,13.5V22H17V13.5Z"
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#0DBD8B"
|
||||||
android:fillType="evenOdd"/>
|
android:fillType="evenOdd"/>
|
||||||
</vector>
|
</vector>
|
|
@ -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>
|
|
@ -108,9 +108,9 @@
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/attachmentButton"
|
android:id="@+id/attachmentButton"
|
||||||
android:layout_width="52dp"
|
android:layout_width="@dimen/composer_attachment_size"
|
||||||
android:layout_height="52dp"
|
android:layout_height="@dimen/composer_attachment_size"
|
||||||
android:layout_margin="1dp"
|
android:layout_margin="@dimen/composer_attachment_margin"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:contentDescription="@string/option_send_files"
|
android:contentDescription="@string/option_send_files"
|
||||||
android:src="@drawable/ic_attachment"
|
android:src="@drawable/ic_attachment"
|
||||||
|
@ -166,7 +166,7 @@
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/sendButton"
|
android:id="@+id/sendButton"
|
||||||
android:layout_width="56dp"
|
android:layout_width="56dp"
|
||||||
android:layout_height="56dp"
|
android:layout_height="@dimen/composer_min_height"
|
||||||
android:layout_marginEnd="2dp"
|
android:layout_marginEnd="2dp"
|
||||||
android:background="@drawable/bg_send"
|
android:background="@drawable/bg_send"
|
||||||
android:contentDescription="@string/send"
|
android:contentDescription="@string/send"
|
||||||
|
|
|
@ -121,9 +121,9 @@
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/attachmentButton"
|
android:id="@+id/attachmentButton"
|
||||||
android:layout_width="52dp"
|
android:layout_width="@dimen/composer_attachment_size"
|
||||||
android:layout_height="52dp"
|
android:layout_height="@dimen/composer_attachment_size"
|
||||||
android:layout_margin="1dp"
|
android:layout_margin="@dimen/composer_attachment_margin"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:contentDescription="@string/option_send_files"
|
android:contentDescription="@string/option_send_files"
|
||||||
android:src="@drawable/ic_attachment"
|
android:src="@drawable/ic_attachment"
|
||||||
|
@ -178,7 +178,7 @@
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/sendButton"
|
android:id="@+id/sendButton"
|
||||||
android:layout_width="56dp"
|
android:layout_width="56dp"
|
||||||
android:layout_height="56dp"
|
android:layout_height="@dimen/composer_min_height"
|
||||||
android:layout_marginEnd="2dp"
|
android:layout_marginEnd="2dp"
|
||||||
android:background="@drawable/bg_send"
|
android:background="@drawable/bg_send"
|
||||||
android:contentDescription="@string/send"
|
android:contentDescription="@string/send"
|
||||||
|
|
|
@ -1,199 +1,112 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="@dimen/composer_min_height"
|
||||||
android:paddingStart="8dp"
|
android:background="?android:colorBackground">
|
||||||
android:paddingEnd="8dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
<ImageButton
|
||||||
android:layout_width="match_parent"
|
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:layout_height="wrap_content"
|
||||||
android:background="@drawable/bg_attachment_type_selector"
|
android:layout_marginStart="4dp"
|
||||||
android:orientation="vertical"
|
android:scrollbars="none"
|
||||||
android:paddingTop="16dp"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:paddingBottom="16dp"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
tools:ignore="UselessParent">
|
app:layout_constraintStart_toEndOf="@id/attachmentCloseButton"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="16dp"
|
android:orientation="horizontal">
|
||||||
android:baselineAligned="false"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:weightSum="3">
|
|
||||||
|
|
||||||
<LinearLayout
|
<ImageButton
|
||||||
android:id="@+id/attachmentCameraButtonContainer"
|
android:id="@+id/attachmentGalleryButton"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
android:layout_weight="1"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:gravity="center"
|
android:contentDescription="@string/attachment_type_gallery"
|
||||||
android:orientation="vertical">
|
android:src="@drawable/ic_attachment_gallery" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/attachmentCameraButton"
|
android:id="@+id/attachmentStickersButton"
|
||||||
style="@style/AttachmentTypeSelectorButton"
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
android:contentDescription="@string/attachment_type_camera"
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
android:src="@drawable/ic_attachment_camera_white_24dp"
|
android:layout_marginStart="2dp"
|
||||||
tools:background="?colorPrimary" />
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/attachment_type_sticker"
|
||||||
|
android:src="@drawable/ic_attachment_sticker"
|
||||||
|
app:tint="?colorPrimary" />
|
||||||
|
|
||||||
<TextView
|
<ImageButton
|
||||||
style="@style/AttachmentTypeSelectorLabel"
|
android:id="@+id/attachmentFileButton"
|
||||||
android:importantForAccessibility="no"
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
android:text="@string/attachment_type_camera" />
|
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
|
<ImageButton
|
||||||
android:id="@+id/attachmentGalleryButtonContainer"
|
android:id="@+id/attachmentCameraButton"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
android:layout_weight="1"
|
android:layout_marginStart="2dp"
|
||||||
android:gravity="center"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:orientation="vertical">
|
android:contentDescription="@string/attachment_type_camera"
|
||||||
|
android:src="@drawable/ic_attachment_camera"
|
||||||
|
app:tint="?colorPrimary" />
|
||||||
|
|
||||||
<ImageButton
|
<!-- TODO. Request for new icon -->
|
||||||
android:id="@+id/attachmentGalleryButton"
|
<ImageButton
|
||||||
style="@style/AttachmentTypeSelectorButton"
|
android:id="@+id/attachmentAudioButton"
|
||||||
android:contentDescription="@string/attachment_type_gallery"
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
android:src="@drawable/ic_attachment_gallery_white_24dp"
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
tools:background="?colorPrimary" />
|
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
|
<!-- TODO. Request for new icon -->
|
||||||
style="@style/AttachmentTypeSelectorLabel"
|
<ImageButton
|
||||||
android:importantForAccessibility="no"
|
android:id="@+id/attachmentContactButton"
|
||||||
android:text="@string/attachment_type_gallery" />
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
</LinearLayout>
|
android:layout_marginStart="2dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
<LinearLayout
|
android:contentDescription="@string/attachment_type_contact"
|
||||||
android:id="@+id/attachmentFileButtonContainer"
|
android:src="@drawable/ic_attachment_contact_white_24dp"
|
||||||
android:layout_width="match_parent"
|
app:tint="?colorPrimary" />
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/attachmentFileButton"
|
|
||||||
style="@style/AttachmentTypeSelectorButton"
|
|
||||||
android:contentDescription="@string/attachment_type_file"
|
|
||||||
android:src="@drawable/ic_attachment_file_white_24dp"
|
|
||||||
tools:background="?colorPrimary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="@style/AttachmentTypeSelectorLabel"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:text="@string/attachment_type_file" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
</HorizontalScrollView>
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:baselineAligned="false"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:weightSum="3">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/attachmentAudioButtonContainer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ImageButton
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
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>
|
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/voiceMessageSendButton"
|
android:id="@+id/voiceMessageSendButton"
|
||||||
android:layout_width="56dp"
|
android:layout_width="56dp"
|
||||||
android:layout_height="56dp"
|
android:layout_height="@dimen/composer_min_height"
|
||||||
android:background="@drawable/bg_send"
|
android:background="@drawable/bg_send"
|
||||||
android:contentDescription="@string/send"
|
android:contentDescription="@string/send"
|
||||||
android:scaleType="center"
|
android:scaleType="center"
|
||||||
|
@ -232,8 +232,8 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="84dp"
|
android:layout_marginBottom="84dp"
|
||||||
android:visibility="gone"
|
|
||||||
android:accessibilityLiveRegion="polite"
|
android:accessibilityLiveRegion="polite"
|
||||||
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
|
Loading…
Reference in New Issue