Merge branch 'develop' into feature/fga/avoid_deadlocks
This commit is contained in:
commit
cfd43af45c
307 changed files with 2806 additions and 1172 deletions
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
|
|
@ -4,7 +4,7 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request: { }
|
pull_request: { }
|
||||||
push:
|
push:
|
||||||
branches: [ main, develop ]
|
branches: [ develop ]
|
||||||
|
|
||||||
# Enrich gradle.properties for CI/CD
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
env:
|
||||||
|
|
@ -34,13 +34,15 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.6.1
|
uses: gradle/gradle-build-action@v2.7.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APK
|
- name: Assemble debug APK
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
run: ./gradlew assembleDebug $CI_GRADLE_ARG_PROPERTIES
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
|
run: ./gradlew assembleDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload debug APKs
|
- name: Upload debug APKs
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|
@ -70,8 +72,8 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Compile release sources
|
- name: Compile release sources
|
||||||
run: ./gradlew compileReleaseSources $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew compileReleaseSources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Compile nightly sources
|
- name: Compile nightly sources
|
||||||
run: ./gradlew compileNightlySources $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew compileNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Compile samples minimal
|
- name: Compile samples minimal
|
||||||
run: ./gradlew :samples:minimal:assemble $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew :samples:minimal:assemble $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|
|
||||||
6
.github/workflows/maestro.yml
vendored
6
.github/workflows/maestro.yml
vendored
|
|
@ -38,6 +38,8 @@ jobs:
|
||||||
run: ./gradlew assembleDebug $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew assembleDebug $CI_GRADLE_ARG_PROPERTIES
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
- name: Upload debug APKs
|
- name: Upload debug APKs
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|
@ -47,7 +49,9 @@ jobs:
|
||||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.4.1
|
- uses: mobile-dev-inc/action-maestro-cloud@v1.4.1
|
||||||
with:
|
with:
|
||||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||||
app-file: app/build/outputs/apk/debug/app-universal-debug.apk
|
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
||||||
|
# app-file should point to an x86 compatible APK file, so upload the x86_64 one (much smaller than the universal APK).
|
||||||
|
app-file: app/build/outputs/apk/debug/app-x86_64-debug.apk
|
||||||
env: |
|
env: |
|
||||||
USERNAME=maestroelement
|
USERNAME=maestroelement
|
||||||
PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
|
PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
|
||||||
|
|
|
||||||
11
.github/workflows/nightly.yml
vendored
11
.github/workflows/nightly.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
name: Build and release nightly APK
|
name: Build and release nightly application
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
@ -12,7 +12,7 @@ env:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
nightly:
|
nightly:
|
||||||
name: Build and publish nightly APK to Firebase
|
name: Build and publish nightly bundle to Firebase
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.repository == 'vector-im/element-x-android' }}
|
if: ${{ github.repository == 'vector-im/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -31,18 +31,21 @@ jobs:
|
||||||
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
|
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
|
||||||
rm towncrier.toml.bak
|
rm towncrier.toml.bak
|
||||||
yes n | towncrier build --version nightly
|
yes n | towncrier build --version nightly
|
||||||
- name: Build and upload Nightly APK
|
- name: Build and upload Nightly application
|
||||||
run: |
|
run: |
|
||||||
./gradlew assembleNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES
|
./gradlew assembleNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
|
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
|
||||||
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
||||||
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
|
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
|
||||||
FIREBASE_TOKEN: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_FIREBASE_TOKEN }}
|
FIREBASE_TOKEN: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_FIREBASE_TOKEN }}
|
||||||
- name: Additionally upload Nightly APK to browserstack for testing
|
- name: Additionally upload Nightly APK to browserstack for testing
|
||||||
continue-on-error: true # don't block anything by this upload failing (for now)
|
continue-on-error: true # don't block anything by this upload failing (for now)
|
||||||
run: curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_PASSWORD" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@app/build/outputs/apk/nightly/app-universal-nightly.apk" -F "custom_id=element-x-android-nightly"
|
run: |
|
||||||
|
curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_PASSWORD" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@app/build/outputs/apk/nightly/app-universal-nightly.apk" -F "custom_id=element-x-android-nightly"
|
||||||
env:
|
env:
|
||||||
BROWSERSTACK_USERNAME: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_USERNAME }}
|
BROWSERSTACK_USERNAME: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_USERNAME }}
|
||||||
BROWSERSTACK_PASSWORD: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_ACCESS_KEY }}
|
BROWSERSTACK_PASSWORD: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_ACCESS_KEY }}
|
||||||
|
|
|
||||||
2
.github/workflows/nightlyReports.yml
vendored
2
.github/workflows/nightlyReports.yml
vendored
|
|
@ -62,7 +62,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.6.1
|
uses: gradle/gradle-build-action@v2.7.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Dependency analysis
|
- name: Dependency analysis
|
||||||
|
|
|
||||||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
|
|
@ -39,7 +39,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.6.1
|
uses: gradle/gradle-build-action@v2.7.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run code quality check suite
|
- name: Run code quality check suite
|
||||||
|
|
|
||||||
2
.github/workflows/recordScreenshots.yml
vendored
2
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
# Add gradle cache, this should speed up the process
|
# Add gradle cache, this should speed up the process
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.6.1
|
uses: gradle/gradle-build-action@v2.7.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Record screenshots
|
- name: Record screenshots
|
||||||
|
|
|
||||||
40
.github/workflows/release.yml
vendored
Normal file
40
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
name: Create release App Bundle
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
# Enrich gradle.properties for CI/CD
|
||||||
|
env:
|
||||||
|
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
||||||
|
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Create App Bundle
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/head/main' && format('build-release-main-{0}', github.sha) }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
|
java-version: '17'
|
||||||
|
- name: Configure gradle
|
||||||
|
uses: gradle/gradle-build-action@v2.7.0
|
||||||
|
- name: Create app bundle
|
||||||
|
env:
|
||||||
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
|
run: ./gradlew bundleRelease $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
- name: Upload bundle as artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: elementx-app-bundle-unsigned
|
||||||
|
path: |
|
||||||
|
app/build/outputs/bundle/release/app-release.aab
|
||||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.6.1
|
uses: gradle/gradle-build-action@v2.7.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -38,6 +38,7 @@ captures/
|
||||||
# IntelliJ
|
# IntelliJ
|
||||||
*.iml
|
*.iml
|
||||||
.idea/.name
|
.idea/.name
|
||||||
|
.idea/androidTestResultsUserPreferences.xml
|
||||||
.idea/assetWizardSettings.xml
|
.idea/assetWizardSettings.xml
|
||||||
.idea/compiler.xml
|
.idea/compiler.xml
|
||||||
.idea/deploymentTargetDropDown.xml
|
.idea/deploymentTargetDropDown.xml
|
||||||
|
|
|
||||||
1
.idea/dictionaries/shared.xml
generated
1
.idea/dictionaries/shared.xml
generated
|
|
@ -8,6 +8,7 @@
|
||||||
<w>measurables</w>
|
<w>measurables</w>
|
||||||
<w>onboarding</w>
|
<w>onboarding</w>
|
||||||
<w>placeables</w>
|
<w>placeables</w>
|
||||||
|
<w>posthog</w>
|
||||||
<w>showkase</w>
|
<w>showkase</w>
|
||||||
<w>snackbar</w>
|
<w>snackbar</w>
|
||||||
<w>swipeable</w>
|
<w>swipeable</w>
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,8 @@ android {
|
||||||
// "App Distribution found more than 1 output file for this variant.
|
// "App Distribution found more than 1 output file for this variant.
|
||||||
// Please contact firebase-support@google.com for help using APK splits with App Distribution."
|
// Please contact firebase-support@google.com for help using APK splits with App Distribution."
|
||||||
artifactPath = "$rootDir/app/build/outputs/apk/nightly/app-universal-nightly.apk"
|
artifactPath = "$rootDir/app/build/outputs/apk/nightly/app-universal-nightly.apk"
|
||||||
|
// artifactType = "AAB"
|
||||||
|
// artifactPath = "$rootDir/app/build/outputs/bundle/nightly/app-nightly.aab"
|
||||||
// This file will be generated by the GitHub action
|
// This file will be generated by the GitHub action
|
||||||
releaseNotesFile = "CHANGES_NIGHTLY.md"
|
releaseNotesFile = "CHANGES_NIGHTLY.md"
|
||||||
groups = "external-testers"
|
groups = "external-testers"
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ import io.element.android.features.login.api.oidc.OidcAction
|
||||||
import io.element.android.features.login.api.oidc.OidcIntentResolver
|
import io.element.android.features.login.api.oidc.OidcIntentResolver
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
import io.element.android.libraries.deeplink.DeeplinkParser
|
import io.element.android.libraries.deeplink.DeeplinkParser
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.composable.Children
|
import com.bumble.appyx.core.composable.Children
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
|
|
@ -161,13 +162,16 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
// Rely on the View Lifecycle instead of the Node Lifecycle,
|
// Rely on the View Lifecycle in addition to the Node Lifecycle,
|
||||||
// because this node enters 'onDestroy' before his children, so it can leads to
|
// because this node enters 'onDestroy' before his children, so it can leads to
|
||||||
// using the room in a child node where it's already closed.
|
// using the room in a child node where it's already closed.
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
inputs.room.open()
|
inputs.room.subscribeToSync()
|
||||||
onDispose {
|
onDispose {
|
||||||
inputs.room.close()
|
inputs.room.unsubscribeFromSync()
|
||||||
|
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
||||||
|
inputs.room.destroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Children(
|
Children(
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.appnav
|
package io.element.android.appnav
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -38,7 +38,7 @@ class RootPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createPresenter()
|
val presenter = createPresenter()
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -54,7 +54,7 @@ class RootPresenterTest {
|
||||||
showError("Bad news", "Something bad happened")
|
showError("Bad news", "Something bad happened")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -34,7 +34,7 @@ class LoggedInPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createPresenter()
|
val presenter = createPresenter()
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,14 @@ allprojects {
|
||||||
apply {
|
apply {
|
||||||
plugin("org.owasp.dependencycheck")
|
plugin("org.owasp.dependencycheck")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||||
|
// Warnings are potential errors, so stop ignoring them
|
||||||
|
// This is disabled by default, but the CI will enforce this.
|
||||||
|
// You can override by passing `-PallWarningsAsErrors=true` in the command line
|
||||||
|
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
|
||||||
|
kotlinOptions.allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// To run a sonar analysis:
|
// To run a sonar analysis:
|
||||||
|
|
|
||||||
17
docs/maps.md
17
docs/maps.md
|
|
@ -27,16 +27,21 @@ Place your API key in `local.properties` with the key
|
||||||
services.maptiler.apikey=abCd3fGhijK1mN0pQr5t
|
services.maptiler.apikey=abCd3fGhijK1mN0pQr5t
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optionally you can also place your custom MapTyler style ids for light and dark maps
|
||||||
|
in the `local.properties` with the keys `services.maptiler.lightMapId` and
|
||||||
|
`services.maptiler.darkMapId`. If you don't specify these, the default MapTiler "basic-v2"
|
||||||
|
styles will be used.
|
||||||
|
|
||||||
## Making releasable builds with MapTiler
|
## Making releasable builds with MapTiler
|
||||||
|
|
||||||
To insert the MapTiler API key when building an APK, set the
|
To insert the MapTiler API key when building an APK, set the
|
||||||
`ELEMENT_ANDROID_MAPTILER_API_KEY` environment variable in your build
|
`ELEMENT_ANDROID_MAPTILER_API_KEY` environment variable in your build
|
||||||
environment.
|
environment.
|
||||||
|
If you've added custom styles also set the `ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID`
|
||||||
|
and `ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID` environment variables accordingly.
|
||||||
|
|
||||||
## Using other map sources or MapTiler styles
|
## Using other map sources or MapTiler styles
|
||||||
|
|
||||||
If you wish to use an alternative map provider, or custom MapTiler styles,
|
If you wish to use an alternative map provider, you can provide your own implementations of
|
||||||
you can customise the functions in
|
`TileServerStyleUriBuilder` and `StaticMapUrlBuilder` in
|
||||||
`features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt`.
|
`features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/`.
|
||||||
We've kept this file small and self contained to minimise the chances of merge
|
|
||||||
collisions in forks.
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,4 @@ dependencies {
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.analytics.test)
|
testImplementation(projects.features.analytics.test)
|
||||||
testImplementation(projects.features.analytics.impl)
|
testImplementation(projects.features.analytics.impl)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.analytics.impl
|
package io.element.android.features.analytics.impl
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -35,7 +35,7 @@ class AnalyticsOptInPresenterTest {
|
||||||
aBuildMeta(),
|
aBuildMeta(),
|
||||||
analyticsService
|
analyticsService
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -53,7 +53,7 @@ class AnalyticsOptInPresenterTest {
|
||||||
aBuildMeta(),
|
aBuildMeta(),
|
||||||
analyticsService
|
analyticsService
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.analytics.impl.preferences
|
package io.element.android.features.analytics.impl.preferences
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -33,7 +33,7 @@ class AnalyticsPreferencesPresenterTest {
|
||||||
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
|
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
|
||||||
aBuildMeta()
|
aBuildMeta()
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -48,7 +48,7 @@ class AnalyticsPreferencesPresenterTest {
|
||||||
FakeAnalyticsService(isEnabled = false, didAskUserConsent = false),
|
FakeAnalyticsService(isEnabled = false, didAskUserConsent = false),
|
||||||
aBuildMeta()
|
aBuildMeta()
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -62,7 +62,7 @@ class AnalyticsPreferencesPresenterTest {
|
||||||
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
|
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
|
||||||
aBuildMeta()
|
aBuildMeta()
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class FakeAnalyticsService(
|
||||||
private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
|
private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
|
||||||
val capturedEvents = mutableListOf<VectorAnalyticsEvent>()
|
val capturedEvents = mutableListOf<VectorAnalyticsEvent>()
|
||||||
|
|
||||||
override fun getAvailableAnalyticsProviders(): List<AnalyticsProvider> = emptyList()
|
override fun getAvailableAnalyticsProviders(): Set<AnalyticsProvider> = emptySet()
|
||||||
|
|
||||||
override fun getUserConsent(): Flow<Boolean> = isEnabledFlow
|
override fun getUserConsent(): Flow<Boolean> = isEnabledFlow
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,5 @@ dependencies {
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
testImplementation(projects.libraries.usersearch.test)
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem
|
import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem
|
||||||
import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems
|
import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -43,7 +43,7 @@ class AddPeoplePresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// TODO This doesn't actually test anything...
|
// TODO This doesn't actually test anything...
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -93,7 +93,7 @@ class ConfigureRoomPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -108,7 +108,7 @@ class ConfigureRoomPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - create room button is enabled only if the required fields are completed`() = runTest {
|
fun `present - create room button is enabled only if the required fields are completed`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -133,7 +133,7 @@ class ConfigureRoomPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - state is updated when fields are changed`() = runTest {
|
fun `present - state is updated when fields are changed`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -203,7 +203,7 @@ class ConfigureRoomPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - trigger create room action`() = runTest {
|
fun `present - trigger create room action`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -221,7 +221,7 @@ class ConfigureRoomPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - record analytics when creating room`() = runTest {
|
fun `present - record analytics when creating room`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -240,7 +240,7 @@ class ConfigureRoomPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - trigger create room with upload error and retry`() = runTest {
|
fun `present - trigger create room with upload error and retry`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -265,7 +265,7 @@ class ConfigureRoomPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - trigger retry and cancel actions`() = runTest {
|
fun `present - trigger retry and cancel actions`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.root
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -27,8 +27,6 @@ import io.element.android.features.createroom.impl.userlist.FakeUserListPresente
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.features.createroom.impl.userlist.aUserListState
|
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
@ -68,7 +66,7 @@ class CreateRoomRootPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -82,7 +80,7 @@ class CreateRoomRootPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - trigger create DM action`() = runTest {
|
fun `present - trigger create DM action`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -102,7 +100,7 @@ class CreateRoomRootPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - creating a DM records analytics event`() = runTest {
|
fun `present - creating a DM records analytics event`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -123,7 +121,7 @@ class CreateRoomRootPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - trigger retrieve DM action`() = runTest {
|
fun `present - trigger retrieve DM action`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -142,7 +140,7 @@ class CreateRoomRootPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - trigger retry create DM action`() = runTest {
|
fun `present - trigger retry create DM action`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.userlist
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -41,7 +41,7 @@ class DefaultUserListPresenterTests {
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -62,7 +62,7 @@ class DefaultUserListPresenterTests {
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -83,7 +83,7 @@ class DefaultUserListPresenterTests {
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -119,7 +119,7 @@ class DefaultUserListPresenterTests {
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -158,7 +158,7 @@ class DefaultUserListPresenterTests {
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -183,7 +183,7 @@ class DefaultUserListPresenterTests {
|
||||||
userRepository,
|
userRepository,
|
||||||
UserListDataStore(),
|
UserListDataStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_welcome_bullet_1">"Anrufe, Standortfreigabe, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst."</string>
|
||||||
|
<string name="screen_welcome_button">"Los geht\'s!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"Folgendes musst du wissen:"</string>
|
||||||
|
<string name="screen_welcome_title">"Willkommen bei %1$s!"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_welcome_bullet_2">"L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"Nous serions ravis d’avoir votre avis, n’hésitez pas à nous le partager via la page des paramètres."</string>
|
||||||
|
<string name="screen_welcome_button">"C’est parti !"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"Voici ce qu’il faut savoir :"</string>
|
||||||
|
<string name="screen_welcome_title">"Bienvenue sur %1$s !"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_welcome_bullet_1">"Hovory, zdieľanie polohy, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."</string>
|
||||||
|
<string name="screen_welcome_button">"Poďme na to!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"Tu je to, čo potrebujete vedieť:"</string>
|
||||||
|
<string name="screen_welcome_title">"Vitajte v %1$s!"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_welcome_bullet_1">"Calls, location sharing, search and more will be added later this year."</string>
|
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms won’t be available in this update."</string>
|
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms won’t be available in this update."</string>
|
||||||
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
||||||
<string name="screen_welcome_button">"Let\'s go!"</string>
|
<string name="screen_welcome_button">"Let\'s go!"</string>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.test.TestScope
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.invitelist.impl
|
package io.element.android.features.invitelist.impl
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
|
|
@ -55,7 +55,7 @@ class InviteListPresenterTests {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
|
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -76,7 +76,7 @@ class InviteListPresenterTests {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
|
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val withInviteState = awaitItem()
|
val withInviteState = awaitItem()
|
||||||
|
|
@ -102,7 +102,7 @@ class InviteListPresenterTests {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
|
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val withInviteState = awaitItem()
|
val withInviteState = awaitItem()
|
||||||
|
|
@ -131,7 +131,7 @@ class InviteListPresenterTests {
|
||||||
FakeAnalyticsService(),
|
FakeAnalyticsService(),
|
||||||
FakeNotificationDrawerManager()
|
FakeNotificationDrawerManager()
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitItem()
|
||||||
|
|
@ -152,7 +152,7 @@ class InviteListPresenterTests {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
|
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitItem()
|
||||||
|
|
@ -173,7 +173,7 @@ class InviteListPresenterTests {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
|
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitItem()
|
||||||
|
|
@ -199,7 +199,7 @@ class InviteListPresenterTests {
|
||||||
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
|
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitItem()
|
||||||
|
|
@ -227,7 +227,7 @@ class InviteListPresenterTests {
|
||||||
room.givenLeaveRoomError(ex)
|
room.givenLeaveRoomError(ex)
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitItem()
|
||||||
|
|
@ -257,7 +257,7 @@ class InviteListPresenterTests {
|
||||||
room.givenLeaveRoomError(ex)
|
room.givenLeaveRoomError(ex)
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitItem()
|
||||||
|
|
@ -288,7 +288,7 @@ class InviteListPresenterTests {
|
||||||
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
|
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitItem()
|
||||||
|
|
@ -313,7 +313,7 @@ class InviteListPresenterTests {
|
||||||
room.givenJoinRoomResult(Result.failure(ex))
|
room.givenJoinRoomResult(Result.failure(ex))
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitItem()
|
||||||
|
|
@ -335,7 +335,7 @@ class InviteListPresenterTests {
|
||||||
room.givenJoinRoomResult(Result.failure(ex))
|
room.givenJoinRoomResult(Result.failure(ex))
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitItem()
|
||||||
|
|
@ -362,7 +362,7 @@ class InviteListPresenterTests {
|
||||||
FakeAnalyticsService(),
|
FakeAnalyticsService(),
|
||||||
FakeNotificationDrawerManager()
|
FakeNotificationDrawerManager()
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
awaitItem()
|
awaitItem()
|
||||||
|
|
@ -400,7 +400,7 @@ class InviteListPresenterTests {
|
||||||
FakeAnalyticsService(),
|
FakeAnalyticsService(),
|
||||||
FakeNotificationDrawerManager()
|
FakeNotificationDrawerManager()
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
awaitItem()
|
awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.leaveroom.impl
|
package io.element.android.features.leaveroom.impl
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -40,7 +40,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state hides all dialogs`() = runTest {
|
fun `present - initial state hides all dialogs`() = runTest {
|
||||||
val presenter = createPresenter()
|
val presenter = createPresenter()
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -60,7 +60,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -80,7 +80,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -100,7 +100,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -122,7 +122,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
},
|
},
|
||||||
roomMembershipObserver = roomMembershipObserver
|
roomMembershipObserver = roomMembershipObserver
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -145,7 +145,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -167,7 +167,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -191,7 +191,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,12 @@ plugins {
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readLocalProperty(name: String) = Properties().apply {
|
fun readLocalProperty(name: String): String? = Properties().apply {
|
||||||
try {
|
try {
|
||||||
load(rootProject.file("local.properties").reader())
|
load(rootProject.file("local.properties").reader())
|
||||||
} catch (ignored: java.io.IOException) {
|
} catch (ignored: java.io.IOException) {
|
||||||
}
|
}
|
||||||
}[name]
|
}.getProperty(name)
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.location.api"
|
namespace = "io.element.android.features.location.api"
|
||||||
|
|
@ -37,9 +37,23 @@ android {
|
||||||
type = "string",
|
type = "string",
|
||||||
name = "maptiler_api_key",
|
name = "maptiler_api_key",
|
||||||
value = System.getenv("ELEMENT_ANDROID_MAPTILER_API_KEY")
|
value = System.getenv("ELEMENT_ANDROID_MAPTILER_API_KEY")
|
||||||
?: readLocalProperty("services.maptiler.apikey") as? String
|
?: readLocalProperty("services.maptiler.apikey")
|
||||||
?: ""
|
?: ""
|
||||||
)
|
)
|
||||||
|
resValue(
|
||||||
|
type = "string",
|
||||||
|
name = "maptiler_light_map_id",
|
||||||
|
value = System.getenv("ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID")
|
||||||
|
?: readLocalProperty("services.maptiler.lightMapId")
|
||||||
|
?: "basic-v2" // fall back to maptiler's default light map.
|
||||||
|
)
|
||||||
|
resValue(
|
||||||
|
type = "string",
|
||||||
|
name = "maptiler_dark_map_id",
|
||||||
|
value = System.getenv("ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID")
|
||||||
|
?: readLocalProperty("services.maptiler.darkMapId")
|
||||||
|
?: "basic-v2-dark" // fall back to maptiler's default dark map.
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,16 +29,16 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImagePainter
|
import coil.compose.AsyncImagePainter
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
||||||
|
import io.element.android.features.location.api.internal.StaticMapUrlBuilder
|
||||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||||
import io.element.android.features.location.api.internal.staticMapUrl
|
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.text.toDp
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -65,23 +65,22 @@ fun StaticMapView(
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var retryHash by remember { mutableStateOf(0) }
|
var retryHash by remember { mutableStateOf(0) }
|
||||||
|
val builder = remember { StaticMapUrlBuilder(context) }
|
||||||
val painter = rememberAsyncImagePainter(
|
val painter = rememberAsyncImagePainter(
|
||||||
model = if (constraints.isZero) {
|
model = if (constraints.isZero) {
|
||||||
// Avoid building a URL if any of the size constraints is zero (else it will thrown an exception).
|
// Avoid building a URL if any of the size constraints is zero (else it will thrown an exception).
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
ImageRequest.Builder(LocalContext.current)
|
ImageRequest.Builder(context)
|
||||||
.data(
|
.data(
|
||||||
staticMapUrl(
|
builder.build(
|
||||||
context = context,
|
|
||||||
lat = lat,
|
lat = lat,
|
||||||
lon = lon,
|
lon = lon,
|
||||||
zoom = zoom,
|
zoom = zoom,
|
||||||
darkMode = darkMode,
|
darkMode = darkMode,
|
||||||
// Size the map based on DP rather than pixels, as otherwise the features and attribution
|
width = constraints.maxWidth,
|
||||||
// end up being illegibly tiny on high density displays.
|
height = constraints.maxHeight,
|
||||||
width = constraints.maxWidth.toDp().value.toInt(),
|
density = LocalDensity.current.density,
|
||||||
height = constraints.maxHeight.toDp().value.toInt(),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.size(width = constraints.maxWidth, height = constraints.maxHeight)
|
.size(width = constraints.maxWidth, height = constraints.maxHeight)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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 io.element.android.features.location.api.internal
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.element.android.features.location.api.R
|
||||||
|
|
||||||
|
internal const val MAPTILER_BASE_URL = "https://api.maptiler.com/maps"
|
||||||
|
|
||||||
|
internal fun Context.mapId(darkMode: Boolean) = when (darkMode) {
|
||||||
|
true -> getString(R.string.maptiler_dark_map_id)
|
||||||
|
false -> getString(R.string.maptiler_light_map_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val Context.apiKey: String
|
||||||
|
get() = getString(R.string.maptiler_api_key)
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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 io.element.android.features.location.api.internal
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an URL for MapTiler's Static Maps API.
|
||||||
|
*
|
||||||
|
* https://docs.maptiler.com/cloud/api/static-maps/
|
||||||
|
*/
|
||||||
|
internal class MapTilerStaticMapUrlBuilder(
|
||||||
|
private val apiKey: String,
|
||||||
|
private val lightMapId: String,
|
||||||
|
private val darkMapId: String,
|
||||||
|
) : StaticMapUrlBuilder {
|
||||||
|
|
||||||
|
constructor(context: Context) : this(
|
||||||
|
apiKey = context.apiKey,
|
||||||
|
lightMapId = context.mapId(darkMode = false),
|
||||||
|
darkMapId = context.mapId(darkMode = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun build(
|
||||||
|
lat: Double,
|
||||||
|
lon: Double,
|
||||||
|
zoom: Double,
|
||||||
|
darkMode: Boolean,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
density: Float
|
||||||
|
): String {
|
||||||
|
val mapId = if (darkMode) darkMapId else lightMapId
|
||||||
|
val finalZoom = zoom.coerceIn(zoomRange)
|
||||||
|
|
||||||
|
// Request @2x density for xhdpi and above (xhdpi == 320dpi == 2x density).
|
||||||
|
val is2x = density >= 2
|
||||||
|
|
||||||
|
// Scale requested width/height according to the reported display density.
|
||||||
|
val (finalWidth, finalHeight) = coerceWidthAndHeight(
|
||||||
|
width = (width / density).roundToInt(),
|
||||||
|
height = (height / density).roundToInt(),
|
||||||
|
is2x = is2x,
|
||||||
|
)
|
||||||
|
|
||||||
|
val scale = if (is2x) "@2x" else ""
|
||||||
|
|
||||||
|
// Since Maptiler doesn't support arbitrary dpi scaling, we stick to 2x sized
|
||||||
|
// images even on displays with density higher than 2x, thereby yielding an
|
||||||
|
// image smaller than the available space in pixels.
|
||||||
|
// The resulting image will have to be scaled to fit the available space in order
|
||||||
|
// to keep the perceived content size constant at the expense of sharpness.
|
||||||
|
return "$MAPTILER_BASE_URL/${mapId}/static/${lon},${lat},${finalZoom}/${finalWidth}x${finalHeight}${scale}.webp?key=${apiKey}&attribution=bottomleft"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun coerceWidthAndHeight(width: Int, height: Int, is2x: Boolean): Pair<Int, Int> {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
// This effectively yields an URL which asks for a 0x0 image which will result in an HTTP error,
|
||||||
|
// but it's better than e.g. asking for a 1x1 image which would be unreadable and increase usage costs.
|
||||||
|
return 0 to 0
|
||||||
|
}
|
||||||
|
val aspectRatio = width.toDouble() / height.toDouble()
|
||||||
|
val range = if (is2x) widthHeightRange2x else widthHeightRange
|
||||||
|
return if (width >= height) {
|
||||||
|
width.coerceIn(range).let { coercedWidth ->
|
||||||
|
coercedWidth to (coercedWidth / aspectRatio).roundToInt()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
height.coerceIn(range).let { coercedHeight ->
|
||||||
|
(coercedHeight * aspectRatio).roundToInt() to coercedHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val widthHeightRange = 1..2048 // API will error if outside 1-2048 range @1x.
|
||||||
|
private val widthHeightRange2x = 1..1024 // API will error if outside 1-1024 range @2x.
|
||||||
|
private val zoomRange = 0.0..22.0 // API will error if outside 0-22 range.
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:JvmName("TileServerStyleUriBuilderKt")
|
||||||
|
|
||||||
|
package io.element.android.features.location.api.internal
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
internal class MapTilerTileServerStyleUriBuilder(
|
||||||
|
private val apiKey: String,
|
||||||
|
private val lightMapId: String,
|
||||||
|
private val darkMapId: String,
|
||||||
|
) : TileServerStyleUriBuilder {
|
||||||
|
|
||||||
|
constructor(context: Context) : this(
|
||||||
|
apiKey = context.apiKey,
|
||||||
|
lightMapId = context.mapId(darkMode = false),
|
||||||
|
darkMapId = context.mapId(darkMode = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun build(darkMode: Boolean): String {
|
||||||
|
val mapId = if (darkMode) darkMapId else lightMapId
|
||||||
|
return "${MAPTILER_BASE_URL}/${mapId}/style.json?key=${apiKey}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 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 io.element.android.features.location.api.internal
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import io.element.android.features.location.api.R
|
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the URL to an image that contains a statically-generated map of the given location.
|
|
||||||
*/
|
|
||||||
fun staticMapUrl(
|
|
||||||
context: Context,
|
|
||||||
lat: Double,
|
|
||||||
lon: Double,
|
|
||||||
zoom: Double,
|
|
||||||
width: Int,
|
|
||||||
height: Int,
|
|
||||||
darkMode: Boolean,
|
|
||||||
): String {
|
|
||||||
return "${baseUrl(darkMode)}/static/${lon},${lat},${zoom}/${width}x${height}@2x.webp?key=${context.apiKey}&attribution=bottomleft"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to remember the tile server URL based on the current theme.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun rememberTileStyleUrl(): String {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val darkMode = !ElementTheme.isLightTheme
|
|
||||||
return remember(darkMode) {
|
|
||||||
tileStyleUrl(
|
|
||||||
context = context,
|
|
||||||
darkMode = darkMode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the URL to a MapLibre style document, used for rendering dynamic maps.
|
|
||||||
*/
|
|
||||||
private fun tileStyleUrl(
|
|
||||||
context: Context,
|
|
||||||
darkMode: Boolean,
|
|
||||||
): String {
|
|
||||||
return "${baseUrl(darkMode)}/style.json?key=${context.apiKey}"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun baseUrl(darkMode: Boolean) =
|
|
||||||
"https://api.maptiler.com/maps/" +
|
|
||||||
if (darkMode)
|
|
||||||
"dea61faf-292b-4774-9660-58fcef89a7f3"
|
|
||||||
else
|
|
||||||
"9bc819c8-e627-474a-a348-ec144fe3d810"
|
|
||||||
|
|
||||||
private val Context.apiKey: String
|
|
||||||
get() = getString(R.string.maptiler_api_key)
|
|
||||||
|
|
@ -29,7 +29,7 @@ fun Modifier.centerBottomEdge(scope: BoxScope): Modifier = with(scope) {
|
||||||
Modifier.align { size, space, _ ->
|
Modifier.align { size, space, _ ->
|
||||||
IntOffset(
|
IntOffset(
|
||||||
x = (space.width - size.width) / 2,
|
x = (space.width - size.width) / 2,
|
||||||
y = (space.height / 2) - size.height,
|
y = space.height / 2 - size.height,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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 io.element.android.features.location.api.internal
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an URL for a 3rd party service provider static maps API.
|
||||||
|
*/
|
||||||
|
interface StaticMapUrlBuilder {
|
||||||
|
fun build(
|
||||||
|
lat: Double,
|
||||||
|
lon: Double,
|
||||||
|
zoom: Double,
|
||||||
|
darkMode: Boolean,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
density: Float,
|
||||||
|
): String
|
||||||
|
}
|
||||||
|
|
||||||
|
fun StaticMapUrlBuilder(context: Context): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder(context = context)
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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 io.element.android.features.location.api.internal
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a style URI for a MapLibre compatible tile server.
|
||||||
|
*
|
||||||
|
* Used for rendering dynamic maps.
|
||||||
|
*/
|
||||||
|
interface TileServerStyleUriBuilder {
|
||||||
|
fun build(
|
||||||
|
darkMode: Boolean,
|
||||||
|
): String
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TileServerStyleUriBuilder(context: Context): TileServerStyleUriBuilder = MapTilerTileServerStyleUriBuilder(context = context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides and remembers a style URI for a MapLibre compatible tile server.
|
||||||
|
*
|
||||||
|
* Used for rendering dynamic maps.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberTileStyleUrl(): String {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val darkMode = !ElementTheme.isLightTheme
|
||||||
|
return remember(darkMode) {
|
||||||
|
TileServerStyleUriBuilder(context).build(darkMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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 io.element.android.features.location.api.internal
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MapTilerStaticMapUrlBuilderTest {
|
||||||
|
|
||||||
|
private val builder = MapTilerStaticMapUrlBuilder(
|
||||||
|
apiKey = "anApiKey",
|
||||||
|
lightMapId = "aLightMapId",
|
||||||
|
darkMapId = "aDarkMapId",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `static map 1x density`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 800,
|
||||||
|
height = 600,
|
||||||
|
density = 1f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `static map 1,5x density`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 1200,
|
||||||
|
height = 900,
|
||||||
|
density = 1.5f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `static map 2x density`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 1600,
|
||||||
|
height = 1200,
|
||||||
|
density = 2f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `static map 3x density`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 2400,
|
||||||
|
height = 1800,
|
||||||
|
density = 3f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `too big image is coerced keeping aspect ratio`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 4096,
|
||||||
|
height = 2048,
|
||||||
|
density = 1f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/2048x1024.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 2048,
|
||||||
|
height = 4096,
|
||||||
|
density = 1f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x2048.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 4096,
|
||||||
|
height = 2048,
|
||||||
|
density = 2f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x512@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 2048,
|
||||||
|
height = 4096,
|
||||||
|
density = 2f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/512x1024@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = Int.MAX_VALUE,
|
||||||
|
height = Int.MAX_VALUE,
|
||||||
|
density = 2f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x1024@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `too small image is coerced to 0x0`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 0,
|
||||||
|
height = 0,
|
||||||
|
density = 1f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = 0,
|
||||||
|
height = 0,
|
||||||
|
density = 2f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
builder.build(
|
||||||
|
lat = 1.23,
|
||||||
|
lon = -4.56,
|
||||||
|
zoom = 7.8,
|
||||||
|
darkMode = false,
|
||||||
|
width = Int.MIN_VALUE,
|
||||||
|
height = Int.MIN_VALUE,
|
||||||
|
density = 1f,
|
||||||
|
)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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 io.element.android.features.location.api.internal
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MapTilerTileServerStyleUriBuilderTest {
|
||||||
|
|
||||||
|
private val builder = MapTilerTileServerStyleUriBuilder(
|
||||||
|
apiKey = "anApiKey",
|
||||||
|
lightMapId = "aLightMapId",
|
||||||
|
darkMapId = "aDarkMapId",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `light map uri`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(darkMode = false)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/style.json?key=anApiKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dark map uri`() {
|
||||||
|
assertThat(
|
||||||
|
builder.build(darkMode = true)
|
||||||
|
).isEqualTo("https://api.maptiler.com/maps/aDarkMapId/style.json?key=anApiKey")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,7 +45,6 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
implementation(projects.services.toolbox.api)
|
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import android.Manifest
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition
|
import com.mapbox.mapboxsdk.camera.CameraPosition
|
||||||
import com.mapbox.mapboxsdk.geometry.LatLng
|
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||||
import io.element.android.libraries.maplibre.compose.MapLocationSettings
|
import io.element.android.libraries.maplibre.compose.MapLocationSettings
|
||||||
|
|
@ -53,6 +54,13 @@ object MapDefaults {
|
||||||
val locationSettings: MapLocationSettings
|
val locationSettings: MapLocationSettings
|
||||||
get() = MapLocationSettings(
|
get() = MapLocationSettings(
|
||||||
locationEnabled = false,
|
locationEnabled = false,
|
||||||
|
backgroundTintColor = Color.White,
|
||||||
|
foregroundTintColor = Color.Black,
|
||||||
|
backgroundStaleTintColor = Color.White,
|
||||||
|
foregroundStaleTintColor = Color.Black,
|
||||||
|
accuracyColor = Color.Black,
|
||||||
|
pulseEnabled = true,
|
||||||
|
pulseColor = Color.Black,
|
||||||
)
|
)
|
||||||
|
|
||||||
val centerCameraPosition = CameraPosition.Builder()
|
val centerCameraPosition = CameraPosition.Builder()
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,7 @@ import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
import java.time.ZonedDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SendLocationPresenter @Inject constructor(
|
class SendLocationPresenter @Inject constructor(
|
||||||
|
|
@ -50,7 +45,6 @@ class SendLocationPresenter @Inject constructor(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val messageComposerContext: MessageComposerContext,
|
private val messageComposerContext: MessageComposerContext,
|
||||||
private val locationActions: LocationActions,
|
private val locationActions: LocationActions,
|
||||||
private val systemClock: SystemClock,
|
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
) : Presenter<SendLocationState> {
|
) : Presenter<SendLocationState> {
|
||||||
|
|
||||||
|
|
@ -115,7 +109,7 @@ class SendLocationPresenter @Inject constructor(
|
||||||
SendLocationState.Mode.PinLocation -> {
|
SendLocationState.Mode.PinLocation -> {
|
||||||
val geoUri = event.cameraPosition.toGeoUri()
|
val geoUri = event.cameraPosition.toGeoUri()
|
||||||
room.sendLocation(
|
room.sendLocation(
|
||||||
body = generateBody(geoUri, systemClock.epochMillis()),
|
body = generateBody(geoUri),
|
||||||
geoUri = geoUri,
|
geoUri = geoUri,
|
||||||
description = null,
|
description = null,
|
||||||
zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
|
zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
|
||||||
|
|
@ -134,7 +128,7 @@ class SendLocationPresenter @Inject constructor(
|
||||||
SendLocationState.Mode.SenderLocation -> {
|
SendLocationState.Mode.SenderLocation -> {
|
||||||
val geoUri = event.toGeoUri()
|
val geoUri = event.toGeoUri()
|
||||||
room.sendLocation(
|
room.sendLocation(
|
||||||
body = generateBody(geoUri, systemClock.epochMillis()),
|
body = generateBody(geoUri),
|
||||||
geoUri = geoUri,
|
geoUri = geoUri,
|
||||||
description = null,
|
description = null,
|
||||||
zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
|
zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
|
||||||
|
|
@ -158,7 +152,4 @@ private fun SendLocationEvents.SendLocation.toGeoUri(): String = location?.toGeo
|
||||||
|
|
||||||
private fun SendLocationEvents.SendLocation.CameraPosition.toGeoUri(): String = "geo:$lat,$lon"
|
private fun SendLocationEvents.SendLocation.CameraPosition.toGeoUri(): String = "geo:$lat,$lon"
|
||||||
|
|
||||||
private fun generateBody(uri: String, epochMillis: Long): String {
|
private fun generateBody(uri: String): String = "Location was shared at $uri"
|
||||||
val timestamp = ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT)
|
|
||||||
return "Location was shared at $uri as of $timestamp"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.LocationOn
|
|
||||||
import androidx.compose.material.icons.filled.LocationSearching
|
import androidx.compose.material.icons.filled.LocationSearching
|
||||||
import androidx.compose.material.icons.filled.MyLocation
|
import androidx.compose.material.icons.filled.MyLocation
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
|
@ -49,6 +48,7 @@ import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
||||||
import io.element.android.features.location.impl.MapDefaults
|
import io.element.android.features.location.impl.MapDefaults
|
||||||
|
import io.element.android.features.location.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
|
|
@ -156,7 +156,11 @@ fun SendLocationView(
|
||||||
navigateUp()
|
navigateUp()
|
||||||
},
|
},
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(Icons.Default.LocationOn, null)
|
Icon(
|
||||||
|
resourceId = R.drawable.pin_small,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.Unspecified,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp + navBarPadding))
|
Spacer(modifier = Modifier.height(16.dp + navBarPadding))
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,13 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
||||||
isTrackMyLocation = false,
|
isTrackMyLocation = false,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
|
ShowLocationState(
|
||||||
|
Location(1.23, 2.34, 4f),
|
||||||
|
description = "For some reason I decided to to write a small essay that wraps at just two lines!",
|
||||||
|
hasLocationPermission = false,
|
||||||
|
isTrackMyLocation = false,
|
||||||
|
eventSink = {},
|
||||||
|
),
|
||||||
ShowLocationState(
|
ShowLocationState(
|
||||||
Location(1.23, 2.34, 4f),
|
Location(1.23, 2.34, 4f),
|
||||||
description = "For some reason I decided to write a small essay in the location description. " +
|
description = "For some reason I decided to write a small essay in the location description. " +
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ import com.mapbox.mapboxsdk.camera.CameraPosition
|
||||||
import com.mapbox.mapboxsdk.geometry.LatLng
|
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
||||||
import io.element.android.features.location.impl.MapDefaults
|
import io.element.android.features.location.impl.MapDefaults
|
||||||
import io.element.android.features.location.impl.send.SendLocationState
|
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="26dp"
|
||||||
|
android:height="28dp"
|
||||||
|
android:viewportWidth="26"
|
||||||
|
android:viewportHeight="28">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.962,28L9.819,24.889L16.105,24.889L12.962,28Z"
|
||||||
|
android:fillColor="#EBEEF2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.963,12.963m-12.963,0a12.963,12.963 0,1 1,25.926 0a12.963,12.963 0,1 1,-25.926 0"
|
||||||
|
android:fillColor="#EBEEF2"/>
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M6.74,6.74h12.444v12.444h-12.444z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.962,6.74C10.554,6.74 8.606,8.741 8.606,11.215C8.606,13.88 11.357,17.555 12.489,18.955C12.738,19.262 13.192,19.262 13.441,18.955C14.567,17.555 17.318,13.88 17.318,11.215C17.318,8.741 15.37,6.74 12.962,6.74ZM12.962,12.813C12.103,12.813 11.406,12.097 11.406,11.215C11.406,10.333 12.103,9.617 12.962,9.617C13.821,9.617 14.518,10.333 14.518,11.215C14.518,12.097 13.821,12.813 12.962,12.813Z"
|
||||||
|
android:fillColor="#101317"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
19
features/location/impl/src/main/res/drawable/pin_small.xml
Normal file
19
features/location/impl/src/main/res/drawable/pin_small.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="26dp"
|
||||||
|
android:height="28dp"
|
||||||
|
android:viewportWidth="26"
|
||||||
|
android:viewportHeight="28">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.962,28L9.819,24.889L16.105,24.889L12.962,28Z"
|
||||||
|
android:fillColor="#1B1D22"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.963,12.963m-12.963,0a12.963,12.963 0,1 1,25.926 0a12.963,12.963 0,1 1,-25.926 0"
|
||||||
|
android:fillColor="#1B1D22"/>
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M6.74,6.741h12.444v12.444h-12.444z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.962,6.741C10.554,6.741 8.606,8.741 8.606,11.215C8.606,13.88 11.357,17.555 12.489,18.955C12.738,19.262 13.192,19.262 13.441,18.955C14.567,17.555 17.318,13.88 17.318,11.215C17.318,8.741 15.37,6.741 12.962,6.741ZM12.962,12.813C12.103,12.813 11.406,12.097 11.406,11.215C11.406,10.333 12.103,9.617 12.962,9.617C13.821,9.617 14.518,10.333 14.518,11.215C14.518,12.097 13.821,12.813 12.962,12.813Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.location.impl.send
|
package io.element.android.features.location.impl.send
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
|
|
@ -34,7 +34,6 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.SendLocationInvocation
|
import io.element.android.libraries.matrix.test.room.SendLocationInvocation
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -46,7 +45,6 @@ class SendLocationPresenterTest {
|
||||||
private val fakeAnalyticsService = FakeAnalyticsService()
|
private val fakeAnalyticsService = FakeAnalyticsService()
|
||||||
private val messageComposerContextFake = MessageComposerContextFake()
|
private val messageComposerContextFake = MessageComposerContextFake()
|
||||||
private val fakeLocationActions = FakeLocationActions()
|
private val fakeLocationActions = FakeLocationActions()
|
||||||
private val fakeSystemClock = SystemClock { 0L }
|
|
||||||
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
||||||
private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter(
|
private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter(
|
||||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||||
|
|
@ -56,7 +54,6 @@ class SendLocationPresenterTest {
|
||||||
analyticsService = fakeAnalyticsService,
|
analyticsService = fakeAnalyticsService,
|
||||||
messageComposerContext = messageComposerContextFake,
|
messageComposerContext = messageComposerContextFake,
|
||||||
locationActions = fakeLocationActions,
|
locationActions = fakeLocationActions,
|
||||||
systemClock = fakeSystemClock,
|
|
||||||
buildMeta = fakeBuildMeta,
|
buildMeta = fakeBuildMeta,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -69,7 +66,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
||||||
|
|
@ -96,7 +93,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
||||||
|
|
@ -123,7 +120,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -149,7 +146,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -175,7 +172,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// Skip initial state
|
// Skip initial state
|
||||||
|
|
@ -206,7 +203,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// Skip initial state
|
// Skip initial state
|
||||||
|
|
@ -234,7 +231,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// Skip initial state
|
// Skip initial state
|
||||||
|
|
@ -265,7 +262,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// Skip initial state
|
// Skip initial state
|
||||||
|
|
@ -292,7 +289,7 @@ class SendLocationPresenterTest {
|
||||||
Truth.assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1)
|
Truth.assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1)
|
||||||
Truth.assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo(
|
Truth.assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo(
|
||||||
SendLocationInvocation(
|
SendLocationInvocation(
|
||||||
body = "Location was shared at geo:3.0,4.0;u=5.0 as of 1970-01-01T00:00:00Z",
|
body = "Location was shared at geo:3.0,4.0;u=5.0",
|
||||||
geoUri = "geo:3.0,4.0;u=5.0",
|
geoUri = "geo:3.0,4.0;u=5.0",
|
||||||
description = null,
|
description = null,
|
||||||
zoomLevel = 15,
|
zoomLevel = 15,
|
||||||
|
|
@ -322,7 +319,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// Skip initial state
|
// Skip initial state
|
||||||
|
|
@ -349,7 +346,7 @@ class SendLocationPresenterTest {
|
||||||
Truth.assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1)
|
Truth.assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1)
|
||||||
Truth.assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo(
|
Truth.assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo(
|
||||||
SendLocationInvocation(
|
SendLocationInvocation(
|
||||||
body = "Location was shared at geo:0.0,1.0 as of 1970-01-01T00:00:00Z",
|
body = "Location was shared at geo:0.0,1.0",
|
||||||
geoUri = "geo:0.0,1.0",
|
geoUri = "geo:0.0,1.0",
|
||||||
description = null,
|
description = null,
|
||||||
zoomLevel = 15,
|
zoomLevel = 15,
|
||||||
|
|
@ -384,7 +381,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// Skip initial state
|
// Skip initial state
|
||||||
|
|
@ -431,7 +428,7 @@ class SendLocationPresenterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
// Skip initial state
|
// Skip initial state
|
||||||
|
|
@ -451,7 +448,7 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `application name is in state`() = runTest {
|
fun `application name is in state`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.location.impl.show
|
package io.element.android.features.location.impl.show
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
|
|
@ -44,7 +44,7 @@ class ShowLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `emits initial state with no location permission`() = runTest {
|
fun `emits initial state with no location permission`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -59,7 +59,7 @@ class ShowLocationPresenterTest {
|
||||||
fun `emits initial state with location permission`() = runTest {
|
fun `emits initial state with location permission`() = runTest {
|
||||||
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -74,7 +74,7 @@ class ShowLocationPresenterTest {
|
||||||
fun `emits initial state with partial location permission`() = runTest {
|
fun `emits initial state with partial location permission`() = runTest {
|
||||||
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -87,7 +87,7 @@ class ShowLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `uses action to share location`() = runTest {
|
fun `uses action to share location`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -102,7 +102,7 @@ class ShowLocationPresenterTest {
|
||||||
fun `centers on user location`() = runTest {
|
fun `centers on user location`() = runTest {
|
||||||
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,4 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ data class LoginPasswordState(
|
||||||
) {
|
) {
|
||||||
val submitEnabled: Boolean
|
val submitEnabled: Boolean
|
||||||
get() = loginAction !is Async.Failure &&
|
get() = loginAction !is Async.Failure &&
|
||||||
((formState.login.isNotEmpty() && formState.password.isNotEmpty()))
|
formState.login.isNotEmpty() &&
|
||||||
|
formState.password.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
<string name="screen_account_provider_form_notice">"Zadajte hľadaný výraz alebo adresu domény."</string>
|
<string name="screen_account_provider_form_notice">"Zadajte hľadaný výraz alebo adresu domény."</string>
|
||||||
<string name="screen_account_provider_form_subtitle">"Vyhľadať spoločnosť, komunitu alebo súkromný server."</string>
|
<string name="screen_account_provider_form_subtitle">"Vyhľadať spoločnosť, komunitu alebo súkromný server."</string>
|
||||||
<string name="screen_account_provider_form_title">"Nájsť poskytovateľa účtu"</string>
|
<string name="screen_account_provider_form_title">"Nájsť poskytovateľa účtu"</string>
|
||||||
<string name="screen_account_provider_signin_subtitle">"Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."</string>
|
<string name="screen_account_provider_signin_subtitle">"Tu budú žiť vaše konverzácie — podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."</string>
|
||||||
<string name="screen_account_provider_signin_title">"Chystáte sa prihlásiť do %s"</string>
|
<string name="screen_account_provider_signin_title">"Chystáte sa prihlásiť do %s"</string>
|
||||||
<string name="screen_account_provider_signup_subtitle">"Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."</string>
|
<string name="screen_account_provider_signup_subtitle">"Tu budú žiť vaše konverzácie — podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."</string>
|
||||||
<string name="screen_account_provider_signup_title">"Chystáte sa vytvoriť účet na %s"</string>
|
<string name="screen_account_provider_signup_title">"Chystáte sa vytvoriť účet na %s"</string>
|
||||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."</string>
|
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."</string>
|
||||||
<string name="screen_change_account_provider_other">"Iný"</string>
|
<string name="screen_change_account_provider_other">"Iný"</string>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.changeserver
|
package io.element.android.features.login.impl.changeserver
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -36,7 +36,7 @@ class ChangeServerPresenterTest {
|
||||||
FakeAuthenticationService(),
|
FakeAuthenticationService(),
|
||||||
AccountProviderDataSource()
|
AccountProviderDataSource()
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -51,7 +51,7 @@ class ChangeServerPresenterTest {
|
||||||
authenticationService,
|
authenticationService,
|
||||||
AccountProviderDataSource()
|
AccountProviderDataSource()
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -72,7 +72,7 @@ class ChangeServerPresenterTest {
|
||||||
authenticationService,
|
authenticationService,
|
||||||
AccountProviderDataSource()
|
AccountProviderDataSource()
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.oidc.webview
|
package io.element.android.features.login.impl.oidc.webview
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -38,7 +38,7 @@ class OidcPresenterTest {
|
||||||
A_OIDC_DATA,
|
A_OIDC_DATA,
|
||||||
FakeAuthenticationService(),
|
FakeAuthenticationService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -53,7 +53,7 @@ class OidcPresenterTest {
|
||||||
A_OIDC_DATA,
|
A_OIDC_DATA,
|
||||||
FakeAuthenticationService(),
|
FakeAuthenticationService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -73,7 +73,7 @@ class OidcPresenterTest {
|
||||||
authenticationService,
|
authenticationService,
|
||||||
)
|
)
|
||||||
authenticationService.givenOidcCancelError(A_THROWABLE)
|
authenticationService.givenOidcCancelError(A_THROWABLE)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -92,7 +92,7 @@ class OidcPresenterTest {
|
||||||
A_OIDC_DATA,
|
A_OIDC_DATA,
|
||||||
FakeAuthenticationService(),
|
FakeAuthenticationService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -110,7 +110,7 @@ class OidcPresenterTest {
|
||||||
A_OIDC_DATA,
|
A_OIDC_DATA,
|
||||||
FakeAuthenticationService(),
|
FakeAuthenticationService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -129,7 +129,7 @@ class OidcPresenterTest {
|
||||||
authenticationService,
|
authenticationService,
|
||||||
)
|
)
|
||||||
authenticationService.givenLoginError(A_THROWABLE)
|
authenticationService.givenLoginError(A_THROWABLE)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.changeaccountprovider
|
package io.element.android.features.login.impl.screens.changeaccountprovider
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -37,7 +37,7 @@ class ChangeAccountProviderPresenterTest {
|
||||||
val presenter = ChangeAccountProviderPresenter(
|
val presenter = ChangeAccountProviderPresenter(
|
||||||
changeServerPresenter
|
changeServerPresenter
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -38,7 +38,7 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
AccountProviderDataSource(),
|
AccountProviderDataSource(),
|
||||||
FakeAuthenticationService(),
|
FakeAuthenticationService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -58,7 +58,7 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
authServer,
|
authServer,
|
||||||
)
|
)
|
||||||
authServer.givenHomeserver(A_HOMESERVER)
|
authServer.givenHomeserver(A_HOMESERVER)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -82,7 +82,7 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
authServer,
|
authServer,
|
||||||
)
|
)
|
||||||
authServer.givenHomeserver(A_HOMESERVER_OIDC)
|
authServer.givenHomeserver(A_HOMESERVER_OIDC)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -105,7 +105,7 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
AccountProviderDataSource(),
|
AccountProviderDataSource(),
|
||||||
authServer,
|
authServer,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -126,7 +126,7 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
AccountProviderDataSource(),
|
AccountProviderDataSource(),
|
||||||
authenticationService,
|
authenticationService,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.loginpassword
|
package io.element.android.features.login.impl.screens.loginpassword
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -45,7 +45,7 @@ class LoginPasswordPresenterTest {
|
||||||
accountProviderDataSource,
|
accountProviderDataSource,
|
||||||
loginUserStory,
|
loginUserStory,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -67,7 +67,7 @@ class LoginPasswordPresenterTest {
|
||||||
loginUserStory,
|
loginUserStory,
|
||||||
)
|
)
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -93,7 +93,7 @@ class LoginPasswordPresenterTest {
|
||||||
loginUserStory,
|
loginUserStory,
|
||||||
)
|
)
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
|
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
|
||||||
|
|
@ -122,7 +122,7 @@ class LoginPasswordPresenterTest {
|
||||||
loginUserStory,
|
loginUserStory,
|
||||||
)
|
)
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -150,7 +150,7 @@ class LoginPasswordPresenterTest {
|
||||||
loginUserStory,
|
loginUserStory,
|
||||||
)
|
)
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.searchaccountprovider
|
package io.element.android.features.login.impl.screens.searchaccountprovider
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -46,7 +46,7 @@ class SearchAccountProviderPresenterTest {
|
||||||
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
||||||
changeServerPresenter
|
changeServerPresenter
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -66,7 +66,7 @@ class SearchAccountProviderPresenterTest {
|
||||||
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
||||||
changeServerPresenter
|
changeServerPresenter
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -90,7 +90,7 @@ class SearchAccountProviderPresenterTest {
|
||||||
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
||||||
changeServerPresenter
|
changeServerPresenter
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -125,7 +125,7 @@ class SearchAccountProviderPresenterTest {
|
||||||
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
||||||
changeServerPresenter
|
changeServerPresenter
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -160,7 +160,7 @@ class SearchAccountProviderPresenterTest {
|
||||||
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
||||||
changeServerPresenter
|
changeServerPresenter
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.screens.waitlistscreen
|
package io.element.android.features.login.impl.screens.waitlistscreen
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -46,7 +46,7 @@ class WaitListPresenterTest {
|
||||||
authenticationService,
|
authenticationService,
|
||||||
loginUserStory,
|
loginUserStory,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -68,7 +68,7 @@ class WaitListPresenterTest {
|
||||||
authenticationService,
|
authenticationService,
|
||||||
loginUserStory,
|
loginUserStory,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -97,7 +97,7 @@ class WaitListPresenterTest {
|
||||||
authenticationService,
|
authenticationService,
|
||||||
loginUserStory,
|
loginUserStory,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
|
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,4 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.logout.impl
|
package io.element.android.features.logout.impl
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
@ -34,7 +34,7 @@ class LogoutPreferencePresenterTest {
|
||||||
val presenter = DefaultLogoutPreferencePresenter(
|
val presenter = DefaultLogoutPreferencePresenter(
|
||||||
FakeMatrixClient(),
|
FakeMatrixClient(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -47,7 +47,7 @@ class LogoutPreferencePresenterTest {
|
||||||
val presenter = DefaultLogoutPreferencePresenter(
|
val presenter = DefaultLogoutPreferencePresenter(
|
||||||
FakeMatrixClient(),
|
FakeMatrixClient(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -65,7 +65,7 @@ class LogoutPreferencePresenterTest {
|
||||||
val presenter = DefaultLogoutPreferencePresenter(
|
val presenter = DefaultLogoutPreferencePresenter(
|
||||||
matrixClient,
|
matrixClient,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,5 @@ dependencies {
|
||||||
testImplementation(projects.libraries.mediapickers.test)
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
testImplementation(libs.test.mockk)
|
testImplementation(libs.test.mockk)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
|
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ fun ForwardMessagesView(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun onRoomRemoved(roomSummaryDetails: RoomSummaryDetails) {
|
fun onRoomRemoved(roomSummaryDetails: RoomSummaryDetails) {
|
||||||
// TODO toggle selection when multi-selection is enabled
|
// TODO toggle selection when multi-selection is enabled
|
||||||
state.eventSink(ForwardMessagesEvents.RemoveSelectedRoom)
|
state.eventSink(ForwardMessagesEvents.RemoveSelectedRoom)
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ import io.element.android.features.messages.impl.media.local.LocalMediaView
|
||||||
import io.element.android.features.messages.impl.media.local.MediaInfo
|
import io.element.android.features.messages.impl.media.local.MediaInfo
|
||||||
import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState
|
import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ internal fun AttachmentsBottomSheet(
|
||||||
onDismissRequest = { isVisible = false }
|
onDismissRequest = { isVisible = false }
|
||||||
) {
|
) {
|
||||||
AttachmentSourcePickerMenu(
|
AttachmentSourcePickerMenu(
|
||||||
eventSink = state.eventSink,
|
state = state,
|
||||||
onSendLocationClicked = onSendLocationClicked,
|
onSendLocationClicked = onSendLocationClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +93,7 @@ internal fun AttachmentsBottomSheet(
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AttachmentSourcePickerMenu(
|
internal fun AttachmentSourcePickerMenu(
|
||||||
eventSink: (MessageComposerEvents) -> Unit,
|
state: MessageComposerState,
|
||||||
onSendLocationClicked: () -> Unit,
|
onSendLocationClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -102,33 +102,35 @@ internal fun AttachmentSourcePickerMenu(
|
||||||
// .navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
|
// .navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) },
|
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) },
|
||||||
icon = { Icon(Icons.Default.Collections, null) },
|
icon = { Icon(Icons.Default.Collections, null) },
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) },
|
text = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) },
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) },
|
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) },
|
||||||
icon = { Icon(Icons.Default.AttachFile, null) },
|
icon = { Icon(Icons.Default.AttachFile, null) },
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_files)) },
|
text = { Text(stringResource(R.string.screen_room_attachment_source_files)) },
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) },
|
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) },
|
||||||
icon = { Icon(Icons.Default.PhotoCamera, null) },
|
icon = { Icon(Icons.Default.PhotoCamera, null) },
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) },
|
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) },
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) },
|
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) },
|
||||||
icon = { Icon(Icons.Default.Videocam, null) },
|
icon = { Icon(Icons.Default.Videocam, null) },
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) },
|
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) },
|
||||||
)
|
)
|
||||||
ListItem(
|
if (state.canShareLocation) {
|
||||||
modifier = Modifier.clickable {
|
ListItem(
|
||||||
eventSink(MessageComposerEvents.PickAttachmentSource.Location)
|
modifier = Modifier.clickable {
|
||||||
onSendLocationClicked()
|
state.eventSink(MessageComposerEvents.PickAttachmentSource.Location)
|
||||||
},
|
onSendLocationClicked()
|
||||||
icon = { Icon(Icons.Default.LocationOn, null) },
|
},
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_location)) },
|
icon = { Icon(Icons.Default.LocationOn, null) },
|
||||||
)
|
text = { Text(stringResource(R.string.screen_room_attachment_source_location)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +138,9 @@ internal fun AttachmentSourcePickerMenu(
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AttachmentSourcePickerMenuPreview() = ElementPreview {
|
internal fun AttachmentSourcePickerMenuPreview() = ElementPreview {
|
||||||
AttachmentSourcePickerMenu(
|
AttachmentSourcePickerMenu(
|
||||||
eventSink = {},
|
state = aMessageComposerState(
|
||||||
|
canShareLocation = true,
|
||||||
|
),
|
||||||
onSendLocationClicked = {},
|
onSendLocationClicked = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,11 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
mutableStateOf<AttachmentsState>(AttachmentsState.None)
|
mutableStateOf<AttachmentsState>(AttachmentsState.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val canShareLocation = remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
canShareLocation.value = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)
|
||||||
|
}
|
||||||
|
|
||||||
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType ->
|
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType ->
|
||||||
handlePickedMedia(attachmentsState, uri, mimeType)
|
handlePickedMedia(attachmentsState, uri, mimeType)
|
||||||
}
|
}
|
||||||
|
|
@ -140,23 +145,23 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
MessageComposerEvents.AddAttachment -> localCoroutineScope.launchIfMediaPickerEnabled {
|
MessageComposerEvents.AddAttachment -> localCoroutineScope.launch {
|
||||||
showAttachmentSourcePicker = true
|
showAttachmentSourcePicker = true
|
||||||
}
|
}
|
||||||
MessageComposerEvents.DismissAttachmentMenu -> showAttachmentSourcePicker = false
|
MessageComposerEvents.DismissAttachmentMenu -> showAttachmentSourcePicker = false
|
||||||
MessageComposerEvents.PickAttachmentSource.FromGallery -> localCoroutineScope.launchIfMediaPickerEnabled {
|
MessageComposerEvents.PickAttachmentSource.FromGallery -> localCoroutineScope.launch {
|
||||||
showAttachmentSourcePicker = false
|
showAttachmentSourcePicker = false
|
||||||
galleryMediaPicker.launch()
|
galleryMediaPicker.launch()
|
||||||
}
|
}
|
||||||
MessageComposerEvents.PickAttachmentSource.FromFiles -> localCoroutineScope.launchIfMediaPickerEnabled {
|
MessageComposerEvents.PickAttachmentSource.FromFiles -> localCoroutineScope.launch {
|
||||||
showAttachmentSourcePicker = false
|
showAttachmentSourcePicker = false
|
||||||
filesPicker.launch()
|
filesPicker.launch()
|
||||||
}
|
}
|
||||||
MessageComposerEvents.PickAttachmentSource.PhotoFromCamera -> localCoroutineScope.launchIfMediaPickerEnabled {
|
MessageComposerEvents.PickAttachmentSource.PhotoFromCamera -> localCoroutineScope.launch {
|
||||||
showAttachmentSourcePicker = false
|
showAttachmentSourcePicker = false
|
||||||
cameraPhotoPicker.launch()
|
cameraPhotoPicker.launch()
|
||||||
}
|
}
|
||||||
MessageComposerEvents.PickAttachmentSource.VideoFromCamera -> localCoroutineScope.launchIfMediaPickerEnabled {
|
MessageComposerEvents.PickAttachmentSource.VideoFromCamera -> localCoroutineScope.launch {
|
||||||
showAttachmentSourcePicker = false
|
showAttachmentSourcePicker = false
|
||||||
cameraVideoPicker.launch()
|
cameraVideoPicker.launch()
|
||||||
}
|
}
|
||||||
|
|
@ -173,17 +178,12 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
hasFocus = hasFocus.value,
|
hasFocus = hasFocus.value,
|
||||||
mode = messageComposerContext.composerMode,
|
mode = messageComposerContext.composerMode,
|
||||||
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
||||||
|
canShareLocation = canShareLocation.value,
|
||||||
attachmentsState = attachmentsState.value,
|
attachmentsState = attachmentsState.value,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.launchIfMediaPickerEnabled(action: suspend () -> Unit) = launch {
|
|
||||||
if (featureFlagService.isFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow)) {
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CoroutineScope.sendMessage(
|
private fun CoroutineScope.sendMessage(
|
||||||
text: String,
|
text: String,
|
||||||
updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit,
|
updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ data class MessageComposerState(
|
||||||
val hasFocus: Boolean,
|
val hasFocus: Boolean,
|
||||||
val mode: MessageComposerMode,
|
val mode: MessageComposerMode,
|
||||||
val showAttachmentSourcePicker: Boolean,
|
val showAttachmentSourcePicker: Boolean,
|
||||||
|
val canShareLocation: Boolean,
|
||||||
val attachmentsState: AttachmentsState,
|
val attachmentsState: AttachmentsState,
|
||||||
val eventSink: (MessageComposerEvents) -> Unit
|
val eventSink: (MessageComposerEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,21 @@ open class MessageComposerStateProvider : PreviewParameterProvider<MessageCompos
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aMessageComposerState() = MessageComposerState(
|
fun aMessageComposerState(
|
||||||
text = "",
|
text: String = "",
|
||||||
isFullScreen = false,
|
isFullScreen: Boolean = false,
|
||||||
hasFocus = false,
|
hasFocus: Boolean = false,
|
||||||
mode = MessageComposerMode.Normal(content = ""),
|
mode: MessageComposerMode = MessageComposerMode.Normal(content = ""),
|
||||||
showAttachmentSourcePicker = false,
|
showAttachmentSourcePicker: Boolean = false,
|
||||||
attachmentsState = AttachmentsState.None,
|
canShareLocation: Boolean = true,
|
||||||
eventSink = {}
|
attachmentsState: AttachmentsState = AttachmentsState.None,
|
||||||
|
) = MessageComposerState(
|
||||||
|
text = text,
|
||||||
|
isFullScreen = isFullScreen,
|
||||||
|
hasFocus = hasFocus,
|
||||||
|
mode = mode,
|
||||||
|
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
||||||
|
canShareLocation = canShareLocation,
|
||||||
|
attachmentsState = attachmentsState,
|
||||||
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val backPaginationEventLimit = 20
|
private const val BACK_PAGINATION_EVENT_LIMIT = 20
|
||||||
private const val backPaginationPageSize = 50
|
private const val BACK_PAGINATION_PAGE_SIZE = 50
|
||||||
|
|
||||||
class TimelinePresenter @Inject constructor(
|
class TimelinePresenter @Inject constructor(
|
||||||
private val timelineItemsFactory: TimelineItemsFactory,
|
private val timelineItemsFactory: TimelineItemsFactory,
|
||||||
|
|
@ -164,6 +164,6 @@ class TimelinePresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.paginateBackwards() = launch {
|
private fun CoroutineScope.paginateBackwards() = launch {
|
||||||
timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize)
|
timeline.paginateBackwards(BACK_PAGINATION_EVENT_LIMIT, BACK_PAGINATION_PAGE_SIZE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ fun TimelineView(
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun inReplyToClicked(eventId: EventId) {
|
fun inReplyToClicked(eventId: EventId) {
|
||||||
// TODO implement this logic once we have support to 'jump to event X' in sliding sync
|
// TODO implement this logic once we have support to 'jump to event X' in sliding sync
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.vanniktech.emoji.Emoji
|
import com.vanniktech.emoji.Emoji
|
||||||
import com.vanniktech.emoji.google.GoogleEmojiProvider
|
import com.vanniktech.emoji.google.GoogleEmojiProvider
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
|
@ -62,7 +61,7 @@ fun EmojiPicker(
|
||||||
val emojiProvider = remember { GoogleEmojiProvider() }
|
val emojiProvider = remember { GoogleEmojiProvider() }
|
||||||
val categories = remember { emojiProvider.categories }
|
val categories = remember { emojiProvider.categories }
|
||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState()
|
||||||
Column (modifier) {
|
Column(modifier) {
|
||||||
TabRow(
|
TabRow(
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
) {
|
) {
|
||||||
|
|
@ -109,7 +108,8 @@ fun EmojiPicker(
|
||||||
Text(
|
Text(
|
||||||
text = item.unicode,
|
text = item.unicode,
|
||||||
style = ElementTheme.typography.fontHeadingSmRegular,
|
style = ElementTheme.typography.fontHeadingSmRegular,
|
||||||
) }
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
||||||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleStateProvider
|
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleStateProvider
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
|
|
||||||
private val CORNER_RADIUS = 8.dp
|
private val CORNER_RADIUS = 8.dp
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ private fun TextContent(
|
||||||
.height(reactionEmojiLineHeight.toDp()),
|
.height(reactionEmojiLineHeight.toDp()),
|
||||||
text = text,
|
text = text,
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = ElementTheme.materialColors.primary
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -126,7 +127,7 @@ private fun IconContent(
|
||||||
) = Icon(
|
) = Icon(
|
||||||
imageVector = imageVector,
|
imageVector = imageVector,
|
||||||
contentDescription = stringResource(id = R.string.screen_room_timeline_add_reaction),
|
contentDescription = stringResource(id = R.string.screen_room_timeline_add_reaction),
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
tint = ElementTheme.materialColors.secondary,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(reactionEmojiLineHeight.toDp())
|
.size(reactionEmojiLineHeight.toDp())
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.constraintlayout.compose.ConstrainScope
|
import androidx.constraintlayout.compose.ConstrainScope
|
||||||
import androidx.constraintlayout.compose.ConstraintLayout
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
import com.google.accompanist.flowlayout.FlowMainAxisAlignment
|
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||||
|
|
@ -291,7 +290,7 @@ private fun TimelineItemEventRowContent(
|
||||||
if (event.reactionsState.reactions.isNotEmpty()) {
|
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||||
TimelineItemReactions(
|
TimelineItemReactions(
|
||||||
reactionsState = event.reactionsState,
|
reactionsState = event.reactionsState,
|
||||||
mainAxisAlignment = if (event.isMine) FlowMainAxisAlignment.End else FlowMainAxisAlignment.Start,
|
isOutgoing = event.isMine,
|
||||||
onReactionClicked = onReactionClicked,
|
onReactionClicked = onReactionClicked,
|
||||||
onMoreReactionsClicked = { onMoreReactionsClicked(event) },
|
onMoreReactionsClicked = { onMoreReactionsClicked(event) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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 io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.AddReaction
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.MeasureResult
|
||||||
|
import androidx.compose.ui.layout.Placeable
|
||||||
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.features.messages.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flow layout for reactions that will show a collapse/expand button when the layout wraps over a defined number of rows.
|
||||||
|
* It displays an add more button when there are greater than 0 reactions and always displays the reaction and add more button
|
||||||
|
* on the same row (moving them both to a new row if necessary).
|
||||||
|
* @param expandButton The expand button
|
||||||
|
* @param addMoreButton The add more button
|
||||||
|
* @param modifier The modifier to apply to this layout
|
||||||
|
* @param itemSpacing The horizontal spacing between items
|
||||||
|
* @param rowSpacing The vertical spacing between rows
|
||||||
|
* @param expanded Whether the layout should display in expanded or collapsed state
|
||||||
|
* @param rowsBeforeCollapsible The number of rows before the collapse/expand button is shown
|
||||||
|
* @param reactions The reaction buttons
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TimelineItemReactionsLayout(
|
||||||
|
expandButton: @Composable () -> Unit,
|
||||||
|
addMoreButton: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
itemSpacing: Dp = 0.dp,
|
||||||
|
rowSpacing: Dp = 0.dp,
|
||||||
|
expanded: Boolean = false,
|
||||||
|
rowsBeforeCollapsible: Int? = 2,
|
||||||
|
reactions: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
SubcomposeLayout(modifier) { constraints ->
|
||||||
|
// Given the placeables and returns a structure representing
|
||||||
|
// how they should wrap on to multiple rows given the constraints max width.
|
||||||
|
fun calculateRows(measurables: List<Placeable>): List<List<Placeable>> {
|
||||||
|
val rows = mutableListOf<List<Placeable>>()
|
||||||
|
var currentRow = mutableListOf<Placeable>()
|
||||||
|
var rowX = 0
|
||||||
|
|
||||||
|
measurables.forEach { placeable ->
|
||||||
|
val horizontalSpacing = if (currentRow.isEmpty()) 0 else itemSpacing.toPx().toInt()
|
||||||
|
// If the current view does not fit on this row bump to the next
|
||||||
|
if (rowX + placeable.width > constraints.maxWidth) {
|
||||||
|
rows.add(currentRow)
|
||||||
|
currentRow = mutableListOf()
|
||||||
|
rowX = 0
|
||||||
|
}
|
||||||
|
rowX += horizontalSpacing + placeable.width
|
||||||
|
currentRow.add(placeable)
|
||||||
|
}
|
||||||
|
// If there are items in the current row remember to append it to the returned value
|
||||||
|
if (currentRow.size > 0) {
|
||||||
|
rows.add(currentRow)
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to render the collapsed state, this takes the rows inputted and adds the extra button to the last row,
|
||||||
|
// removing only as many trailing reactions as needed to make space for it.
|
||||||
|
fun replaceTrailingItemsWithButtons(rowsIn: List<List<Placeable>>, expandButton: Placeable, addMoreButton: Placeable): List<List<Placeable>> {
|
||||||
|
val rows = rowsIn.toMutableList()
|
||||||
|
val lastRow = rows.last()
|
||||||
|
val buttonsWidth = expandButton.width + itemSpacing.toPx().toInt() + addMoreButton.width
|
||||||
|
var rowX = 0
|
||||||
|
lastRow.forEachIndexed { i, placeable ->
|
||||||
|
val horizontalSpacing = if (i == 0) 0 else itemSpacing.toPx().toInt()
|
||||||
|
rowX += placeable.width + horizontalSpacing
|
||||||
|
if (rowX > constraints.maxWidth - (buttonsWidth + horizontalSpacing)) {
|
||||||
|
val lastRowWithButton = lastRow.take(i) + listOf(expandButton, addMoreButton)
|
||||||
|
rows[rows.size - 1] = lastRowWithButton
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val lastRowWithButton = lastRow + listOf(expandButton, addMoreButton)
|
||||||
|
rows[rows.size - 1] = lastRowWithButton
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// To prevent the add more and expand buttons from wrapping on to separate lines.
|
||||||
|
// If there is one item on the last line, it moves the expand button down.
|
||||||
|
fun ensureCollapseAndAddMoreButtonsAreOnTheSameRow(rowsIn: List<List<Placeable>>): List<List<Placeable>> {
|
||||||
|
val lastRow = rowsIn.last().toMutableList()
|
||||||
|
if (lastRow.size != 1) {
|
||||||
|
return rowsIn
|
||||||
|
}
|
||||||
|
val rows = rowsIn.toMutableList()
|
||||||
|
val secondLastRow = rows[rows.size - 2].toMutableList()
|
||||||
|
val expandButtonPlaceable = secondLastRow.removeLast()
|
||||||
|
lastRow.add(0, expandButtonPlaceable)
|
||||||
|
rows[rows.size - 2] = secondLastRow
|
||||||
|
rows[rows.size - 1] = lastRow
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a list of rows place them in the layout.
|
||||||
|
fun layoutRows(rows: List<List<Placeable>>): MeasureResult {
|
||||||
|
var width = 0
|
||||||
|
var height = 0
|
||||||
|
val placeables = rows.mapIndexed { i, row ->
|
||||||
|
var rowX = 0
|
||||||
|
var rowHeight = 0
|
||||||
|
val verticalSpacing = if (i == 0) 0 else rowSpacing.toPx().toInt()
|
||||||
|
val rowWithPoints = row.mapIndexed { j, placeable ->
|
||||||
|
val horizontalSpacing = if (j == 0) 0 else itemSpacing.toPx().toInt()
|
||||||
|
val point = IntOffset(rowX + horizontalSpacing, height + verticalSpacing)
|
||||||
|
rowX += placeable.width + horizontalSpacing
|
||||||
|
rowHeight = maxOf(rowHeight, placeable.height)
|
||||||
|
Pair(placeable, point)
|
||||||
|
}
|
||||||
|
height += rowHeight + verticalSpacing
|
||||||
|
width = maxOf(width, rowX)
|
||||||
|
rowWithPoints
|
||||||
|
}.flatten()
|
||||||
|
|
||||||
|
return layout(width = width, height = height) {
|
||||||
|
placeables.forEach {
|
||||||
|
val (placeable, origin) = it
|
||||||
|
placeable.placeRelative(origin.x, origin.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val reactionsPlaceables = subcompose(0, reactions).map { it.measure(constraints) }
|
||||||
|
if (reactionsPlaceables.isEmpty()) {
|
||||||
|
return@SubcomposeLayout layoutRows(listOf())
|
||||||
|
}
|
||||||
|
val addMorePlaceable = subcompose(1, addMoreButton).first().measure(constraints)
|
||||||
|
val expandPlaceable = subcompose(2, expandButton).first().measure(constraints)
|
||||||
|
|
||||||
|
// Calculate the layout of the rows with the reactions button and add more button
|
||||||
|
val reactionsAndAddMore = calculateRows(reactionsPlaceables + listOf(addMorePlaceable))
|
||||||
|
// If we have extended beyond the defined number of rows we are showing the expand/collapse ui
|
||||||
|
if (rowsBeforeCollapsible?.let { reactionsAndAddMore.size > it } == true) {
|
||||||
|
if (expanded) {
|
||||||
|
// Show all subviews with the add more button at the end
|
||||||
|
var reactionsAndButtons = calculateRows(reactionsPlaceables + listOf(expandPlaceable, addMorePlaceable))
|
||||||
|
reactionsAndButtons = ensureCollapseAndAddMoreButtonsAreOnTheSameRow(reactionsAndButtons)
|
||||||
|
layoutRows(reactionsAndButtons)
|
||||||
|
} else {
|
||||||
|
// Truncate to `rowsBeforeCollapsible` number of rows and replace the reactions at the end of the last row with the buttons
|
||||||
|
val collapsedRows = reactionsAndAddMore.take(rowsBeforeCollapsible)
|
||||||
|
val collapsedRowsWithButtons = replaceTrailingItemsWithButtons(collapsedRows, expandPlaceable, addMorePlaceable)
|
||||||
|
layoutRows(collapsedRowsWithButtons)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise we are just showing all items without the expand button
|
||||||
|
layoutRows(reactionsAndAddMore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DayNightPreviews
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemReactionsLayoutPreview() = ElementPreview {
|
||||||
|
TimelineItemReactionsLayout(
|
||||||
|
expanded = false,
|
||||||
|
expandButton = {
|
||||||
|
MessagesReactionButton(
|
||||||
|
content = MessagesReactionsButtonContent.Text(
|
||||||
|
text = stringResource(id = R.string.screen_room_timeline_less_reactions)
|
||||||
|
),
|
||||||
|
onClick = { },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
addMoreButton = {
|
||||||
|
MessagesReactionButton(
|
||||||
|
content = MessagesReactionsButtonContent.Icon(Icons.Outlined.AddReaction),
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
reactions = {
|
||||||
|
io.element.android.features.messages.impl.timeline.aTimelineItemReactions(count = 18).reactions.forEach {
|
||||||
|
MessagesReactionButton(
|
||||||
|
content = MessagesReactionsButtonContent.Reaction(
|
||||||
|
it
|
||||||
|
),
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -19,18 +19,16 @@ package io.element.android.features.messages.impl.timeline.components
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.AddReaction
|
import androidx.compose.material.icons.outlined.AddReaction
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.accompanist.flowlayout.FlowMainAxisAlignment
|
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
|
||||||
import io.element.android.features.messages.impl.R
|
import io.element.android.features.messages.impl.R
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
|
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
|
||||||
|
|
@ -38,162 +36,119 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemReac
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The maximum number of items that can be displayed before some items will be hidden
|
|
||||||
*
|
|
||||||
* TODO The threshold should be based on the number of rows, rather than items.
|
|
||||||
* Once items would spill onto a third row, they should be hidden.
|
|
||||||
* Note this could be particularly worthwhile to handle reactions that are
|
|
||||||
* longer than a single character (as annotation keys are free text).
|
|
||||||
*/
|
|
||||||
private const val COLLAPSE_ITEMS_THRESHOLD = 8
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemReactions(
|
fun TimelineItemReactions(
|
||||||
reactionsState: TimelineItemReactions,
|
reactionsState: TimelineItemReactions,
|
||||||
mainAxisAlignment: FlowMainAxisAlignment,
|
isOutgoing: Boolean,
|
||||||
onReactionClicked: (emoji: String) -> Unit,
|
onReactionClicked: (emoji: String) -> Unit,
|
||||||
onMoreReactionsClicked: () -> Unit,
|
onMoreReactionsClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var expanded: Boolean by rememberSaveable { mutableStateOf(false) }
|
var expanded: Boolean by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val reactions by remember(reactionsState, expanded) {
|
// In LTR languages we want an incoming message's reactions to be LRT and outgoing to be RTL.
|
||||||
derivedStateOf {
|
// For RTL languages it should be the opposite.
|
||||||
val numToDisplay = if (expanded) {
|
val reactionsLayoutDirection = if (!isOutgoing) LocalLayoutDirection.current
|
||||||
reactionsState.reactions.count()
|
else if (LocalLayoutDirection.current == LayoutDirection.Ltr)
|
||||||
} else {
|
LayoutDirection.Rtl
|
||||||
COLLAPSE_ITEMS_THRESHOLD
|
else
|
||||||
}
|
LayoutDirection.Ltr
|
||||||
reactionsState.reactions.take(numToDisplay).toPersistentList()
|
|
||||||
}
|
CompositionLocalProvider(LocalLayoutDirection provides reactionsLayoutDirection) {
|
||||||
|
TimelineItemReactionsView(
|
||||||
|
modifier = modifier,
|
||||||
|
reactions = reactionsState.reactions,
|
||||||
|
expanded = expanded,
|
||||||
|
onReactionClick = onReactionClicked,
|
||||||
|
onMoreReactionsClick = onMoreReactionsClicked,
|
||||||
|
onToggleExpandClick = { expanded = !expanded },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val expandableState by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
if (expanded) {
|
|
||||||
ExpandableState.Expanded
|
|
||||||
} else {
|
|
||||||
val hiddenItems = reactionsState.reactions.count() - reactions.count()
|
|
||||||
if (hiddenItems > 0) {
|
|
||||||
ExpandableState.Collapsed(hidden = hiddenItems)
|
|
||||||
} else {
|
|
||||||
ExpandableState.None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TimelineItemReactionsView(
|
|
||||||
modifier = modifier,
|
|
||||||
reactions = reactions,
|
|
||||||
expandableState = expandableState,
|
|
||||||
mainAxisAlignment = mainAxisAlignment,
|
|
||||||
onReactionClick = onReactionClicked,
|
|
||||||
onMoreReactionsClick = onMoreReactionsClicked,
|
|
||||||
onExpandClick = { expanded = true },
|
|
||||||
onCollapseClick = { expanded = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ExpandableState {
|
|
||||||
object None: ExpandableState()
|
|
||||||
data class Collapsed(val hidden: Int): ExpandableState()
|
|
||||||
object Expanded : ExpandableState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TimelineItemReactionsView(
|
private fun TimelineItemReactionsView(
|
||||||
reactions: ImmutableList<AggregatedReaction>,
|
reactions: ImmutableList<AggregatedReaction>,
|
||||||
expandableState: ExpandableState,
|
expanded: Boolean,
|
||||||
mainAxisAlignment: FlowMainAxisAlignment,
|
|
||||||
onReactionClick: (emoji: String) -> Unit,
|
onReactionClick: (emoji: String) -> Unit,
|
||||||
onMoreReactionsClick: () -> Unit,
|
onMoreReactionsClick: () -> Unit,
|
||||||
onExpandClick: () -> Unit,
|
onToggleExpandClick: () -> Unit,
|
||||||
onCollapseClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) =
|
) = TimelineItemReactionsLayout(
|
||||||
FlowRow(
|
modifier = modifier,
|
||||||
modifier = modifier,
|
itemSpacing = 4.dp,
|
||||||
mainAxisSpacing = 4.dp,
|
rowSpacing = 4.dp,
|
||||||
crossAxisSpacing = 4.dp,
|
expanded = expanded,
|
||||||
mainAxisAlignment = mainAxisAlignment,
|
expandButton = {
|
||||||
) {
|
MessagesReactionButton(
|
||||||
|
content = MessagesReactionsButtonContent.Text(
|
||||||
|
text = stringResource(id = if (expanded) R.string.screen_room_reactions_show_less else R.string.screen_room_reactions_show_more)
|
||||||
|
),
|
||||||
|
onClick = onToggleExpandClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
addMoreButton = {
|
||||||
|
MessagesReactionButton(
|
||||||
|
content = MessagesReactionsButtonContent.Icon(Icons.Outlined.AddReaction),
|
||||||
|
onClick = onMoreReactionsClick
|
||||||
|
)
|
||||||
|
},
|
||||||
|
reactions = {
|
||||||
reactions.forEach { reaction ->
|
reactions.forEach { reaction ->
|
||||||
MessagesReactionButton(
|
MessagesReactionButton(
|
||||||
content = MessagesReactionsButtonContent.Reaction(reaction = reaction),
|
content = MessagesReactionsButtonContent.Reaction(reaction = reaction),
|
||||||
onClick = { onReactionClick(reaction.key) }
|
onClick = { onReactionClick(reaction.key) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (expandableState) {
|
|
||||||
ExpandableState.Expanded ->
|
|
||||||
MessagesReactionButton(
|
|
||||||
content = MessagesReactionsButtonContent.Text(
|
|
||||||
text = stringResource(id = R.string.screen_room_timeline_less_reactions)
|
|
||||||
),
|
|
||||||
onClick = onCollapseClick,
|
|
||||||
)
|
|
||||||
is ExpandableState.Collapsed -> {
|
|
||||||
val hidden = expandableState.hidden
|
|
||||||
MessagesReactionButton(
|
|
||||||
content = MessagesReactionsButtonContent.Text(
|
|
||||||
text = pluralStringResource(id = R.plurals.screen_room_timeline_more_reactions, hidden, hidden)
|
|
||||||
),
|
|
||||||
onClick = onExpandClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ExpandableState.None -> {
|
|
||||||
// No expand or collapse action available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MessagesReactionButton(
|
|
||||||
content = MessagesReactionsButtonContent.Icon(Icons.Outlined.AddReaction),
|
|
||||||
onClick = onMoreReactionsClick
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@DayNightPreviews
|
@DayNightPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemReactionsViewPreview() = ElementPreview {
|
fun TimelineItemReactionsViewPreview() = ElementPreview {
|
||||||
ContentToPreview(
|
ContentToPreview(
|
||||||
reactions = aTimelineItemReactions(count = 1).reactions,
|
reactions = aTimelineItemReactions(count = 1).reactions
|
||||||
expandableState = ExpandableState.None,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@DayNightPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemReactionsViewCollapsedPreview() = ElementPreview {
|
fun TimelineItemReactionsViewFewPreview() = ElementPreview {
|
||||||
ContentToPreview(
|
ContentToPreview(
|
||||||
reactions = aTimelineItemReactions(count = 3).reactions,
|
reactions = aTimelineItemReactions(count = 3).reactions
|
||||||
expandableState = ExpandableState.Collapsed(hidden = 7),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@DayNightPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemReactionsViewExpandedPreview() = ElementPreview {
|
fun TimelineItemReactionsViewIncomingPreview() = ElementPreview {
|
||||||
ContentToPreview(
|
ContentToPreview(
|
||||||
reactions = aTimelineItemReactions(count = 10).reactions,
|
reactions = aTimelineItemReactions(count = 18).reactions
|
||||||
expandableState = ExpandableState.Expanded,
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@DayNightPreviews
|
||||||
|
@Composable
|
||||||
|
fun TimelineItemReactionsViewOutgoingPreview() = ElementPreview {
|
||||||
|
ContentToPreview(
|
||||||
|
reactions = aTimelineItemReactions(count = 18).reactions,
|
||||||
|
isOutgoing = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(
|
private fun ContentToPreview(
|
||||||
reactions: ImmutableList<AggregatedReaction>,
|
reactions: ImmutableList<AggregatedReaction>,
|
||||||
expandableState: ExpandableState
|
isOutgoing: Boolean = false
|
||||||
) {
|
) {
|
||||||
TimelineItemReactionsView(
|
TimelineItemReactions(
|
||||||
reactions = reactions,
|
reactionsState = TimelineItemReactions(
|
||||||
expandableState = expandableState,
|
reactions
|
||||||
mainAxisAlignment = FlowMainAxisAlignment.Center,
|
),
|
||||||
onReactionClick = {},
|
isOutgoing = isOutgoing,
|
||||||
onMoreReactionsClick = {},
|
onReactionClicked = {},
|
||||||
onExpandClick = {},
|
onMoreReactionsClicked = {},
|
||||||
onCollapseClick = {}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ fun TimelineItem.Event.toExtraPadding(): ExtraPadding {
|
||||||
fun ExtraPadding.getStr(fontSize: TextUnit): String {
|
fun ExtraPadding.getStr(fontSize: TextUnit): String {
|
||||||
if (nbChars == 0) return ""
|
if (nbChars == 0) return ""
|
||||||
val timestampFontSize = ElementTheme.typography.fontBodyXsRegular.fontSize // 11.sp
|
val timestampFontSize = ElementTheme.typography.fontBodyXsRegular.fontSize // 11.sp
|
||||||
val nbOfSpaces = ((timestampFontSize.value / fontSize.value) * nbChars).toInt() + 1
|
val nbOfSpaces = (timestampFontSize.value / fontSize.value * nbChars).toInt() + 1
|
||||||
// A space and some unbreakable spaces
|
// A space and some unbreakable spaces
|
||||||
return " " + "\u00A0".repeat(nbOfSpaces)
|
return " " + "\u00A0".repeat(nbOfSpaces)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemEncryptedView(
|
fun TimelineItemEncryptedView(
|
||||||
content: TimelineItemEncryptedContent,
|
@Suppress("UNUSED_PARAMETER") content: TimelineItemEncryptedContent,
|
||||||
extraPadding: ExtraPadding,
|
extraPadding: ExtraPadding,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemRedactedView(
|
fun TimelineItemRedactedView(
|
||||||
content: TimelineItemRedactedContent,
|
@Suppress("UNUSED_PARAMETER") content: TimelineItemRedactedContent,
|
||||||
extraPadding: ExtraPadding,
|
extraPadding: ExtraPadding,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemUnknownView(
|
fun TimelineItemUnknownView(
|
||||||
content: TimelineItemUnknownContent,
|
@Suppress("UNUSED_PARAMETER") content: TimelineItemUnknownContent,
|
||||||
extraPadding: ExtraPadding,
|
extraPadding: ExtraPadding,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.messages.impl.timeline.components.event
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ import org.jsoup.nodes.Element
|
||||||
import org.jsoup.nodes.Node
|
import org.jsoup.nodes.Node
|
||||||
import org.jsoup.nodes.TextNode
|
import org.jsoup.nodes.TextNode
|
||||||
|
|
||||||
private const val chipId = "chip"
|
private const val CHIP_ID = "chip"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HtmlDocument(
|
fun HtmlDocument(
|
||||||
|
|
@ -351,7 +351,7 @@ private fun HtmlMxReply(
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(bottom = 4.dp)
|
.padding(bottom = 4.dp)
|
||||||
.offset(x = -(8.dp)),
|
.offset(x = (-8).dp),
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
shape = shape,
|
shape = shape,
|
||||||
) {
|
) {
|
||||||
|
|
@ -544,17 +544,27 @@ private fun AnnotatedString.Builder.appendLink(link: Element) {
|
||||||
pop()
|
pop()
|
||||||
}
|
}
|
||||||
is PermalinkData.RoomEmailInviteLink -> {
|
is PermalinkData.RoomEmailInviteLink -> {
|
||||||
appendInlineContent(chipId, link.ownText())
|
safeAppendInlineContent(CHIP_ID, link.ownText())
|
||||||
}
|
}
|
||||||
is PermalinkData.RoomLink -> {
|
is PermalinkData.RoomLink -> {
|
||||||
appendInlineContent(chipId, link.ownText())
|
safeAppendInlineContent(CHIP_ID, link.ownText())
|
||||||
}
|
}
|
||||||
is PermalinkData.UserLink -> {
|
is PermalinkData.UserLink -> {
|
||||||
appendInlineContent(chipId, link.ownText())
|
safeAppendInlineContent(CHIP_ID, link.ownText())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun AnnotatedString.Builder.safeAppendInlineContent(chipId: String, ownText: String) {
|
||||||
|
if (ownText.isEmpty()) {
|
||||||
|
// alternateText cannot be empty and default parameter value is private,
|
||||||
|
// so just omit the second param here.
|
||||||
|
appendInlineContent(chipId)
|
||||||
|
} else {
|
||||||
|
appendInlineContent(chipId, ownText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HtmlText(
|
private fun HtmlText(
|
||||||
text: AnnotatedString,
|
text: AnnotatedString,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -165,6 +164,7 @@ internal fun RetrySendMessageMenuPreviewDark(@PreviewParameter(RetrySendMenuStat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(state: RetrySendMenuState) {
|
private fun ContentToPreview(state: RetrySendMenuState) {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.messages.impl.R
|
import io.element.android.features.messages.impl.R
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineItemContentFailedToParseMessageFactory @Inject constructor() {
|
class TimelineItemContentFailedToParseMessageFactory @Inject constructor() {
|
||||||
|
|
||||||
fun create(failedToParseMessageLike: FailedToParseMessageLikeContent): TimelineItemEventContent {
|
fun create(@Suppress("UNUSED_PARAMETER") failedToParseMessageLike: FailedToParseMessageLikeContent): TimelineItemEventContent {
|
||||||
return TimelineItemUnknownContent
|
return TimelineItemUnknownContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineItemContentFailedToParseStateFactory @Inject constructor() {
|
class TimelineItemContentFailedToParseStateFactory @Inject constructor() {
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun create(failedToParseState: FailedToParseStateContent): TimelineItemEventContent {
|
fun create(failedToParseState: FailedToParseStateContent): TimelineItemEventContent {
|
||||||
return TimelineItemUnknownContent
|
return TimelineItemUnknownContent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineItemContentRedactedFactory @Inject constructor() {
|
class TimelineItemContentRedactedFactory @Inject constructor() {
|
||||||
|
|
||||||
fun create(content: RedactedContent): TimelineItemEventContent {
|
fun create(@Suppress("UNUSED_PARAMETER") content: RedactedContent): TimelineItemEventContent {
|
||||||
return TimelineItemRedactedContent
|
return TimelineItemRedactedContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineItemContentStickerFactory @Inject constructor() {
|
class TimelineItemContentStickerFactory @Inject constructor() {
|
||||||
|
|
||||||
fun create(content: StickerContent): TimelineItemEventContent {
|
fun create(@Suppress("UNUSED_PARAMETER") content: StickerContent): TimelineItemEventContent {
|
||||||
return TimelineItemUnknownContent
|
return TimelineItemUnknownContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<item quantity="other">"%1$d changements dans la conversation"</item>
|
<item quantity="other">"%1$d changements dans la conversation"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="screen_room_timeline_more_reactions">
|
<plurals name="screen_room_timeline_more_reactions">
|
||||||
<item quantity="one"></item>
|
<item quantity="one">"1 de plus"</item>
|
||||||
<item quantity="other">"%1$d de plus"</item>
|
<item quantity="other">"%1$d de plus"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_room_attachment_source_camera">"Appareil photo"</string>
|
<string name="screen_room_attachment_source_camera">"Appareil photo"</string>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue