Merge remote-tracking branch 'origin/develop' into feature/cjs/view-location-in-timeline
2
.github/workflows/build.yml
vendored
|
|
@ -34,7 +34,7 @@ jobs:
|
|||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.4.2
|
||||
uses: gradle/gradle-build-action@v2.5.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Assemble debug APK
|
||||
|
|
|
|||
2
.github/workflows/nightlyReports.yml
vendored
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.4.2
|
||||
uses: gradle/gradle-build-action@v2.5.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Dependency analysis
|
||||
|
|
|
|||
2
.github/workflows/quality.yml
vendored
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.4.2
|
||||
uses: gradle/gradle-build-action@v2.5.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run code quality check suite
|
||||
|
|
|
|||
2
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
java-version: '17'
|
||||
# Add gradle cache, this should speed up the process
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.4.2
|
||||
uses: gradle/gradle-build-action@v2.5.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Record screenshots
|
||||
|
|
|
|||
2
.github/workflows/tests.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.4.2
|
||||
uses: gradle/gradle-build-action@v2.5.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
|
||||
|
|
|
|||
2
.idea/dictionaries/shared.xml
generated
|
|
@ -4,7 +4,9 @@
|
|||
<w>backstack</w>
|
||||
<w>homeserver</w>
|
||||
<w>kover</w>
|
||||
<w>measurables</w>
|
||||
<w>onboarding</w>
|
||||
<w>placeables</w>
|
||||
<w>showkase</w>
|
||||
<w>textfields</w>
|
||||
</words>
|
||||
|
|
|
|||
|
|
@ -92,13 +92,13 @@ android {
|
|||
|
||||
buildTypes {
|
||||
named("debug") {
|
||||
resValue("string", "app_name", "ElementX dbg")
|
||||
resValue("string", "app_name", "Element X dbg")
|
||||
applicationIdSuffix = ".debug"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
|
||||
named("release") {
|
||||
resValue("string", "app_name", "ElementX")
|
||||
resValue("string", "app_name", "Element X")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
||||
postprocessing {
|
||||
|
|
@ -115,7 +115,7 @@ android {
|
|||
initWith(release)
|
||||
applicationIdSuffix = ".nightly"
|
||||
versionNameSuffix = "-nightly"
|
||||
resValue("string", "app_name", "ElementX nightly")
|
||||
resValue("string", "app_name", "Element X nightly")
|
||||
matchingFallbacks += listOf("release")
|
||||
signingConfig = signingConfigs.getByName("nightly")
|
||||
|
||||
|
|
@ -229,4 +229,6 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 47 KiB |
49
app/src/main/kotlin/io/element/android/x/icon/IconPreview.kt
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.x.icon
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.x.R
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun IconPreview(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box {
|
||||
Image(painter = painterResource(id = R.mipmap.ic_launcher_background), contentDescription = null)
|
||||
Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RoundIconPreview(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.clip(shape = CircleShape)) {
|
||||
Image(painter = painterResource(id = R.mipmap.ic_launcher_background), contentDescription = null)
|
||||
Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<inset
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@mipmap/ic_launcher_round"
|
||||
android:insetTop="80dp"
|
||||
android:insetRight="80dp"
|
||||
android:insetBottom="80dp"
|
||||
android:insetLeft="80dp" />
|
||||
20
app/src/main/res/drawable/transparent.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ 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.
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<solid android:color="#00000000" />
|
||||
|
||||
</shape>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<!-- Waiting for design monochrome android:drawable="@mipmap/ic_launcher_monochrome" /-->
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<!-- Waiting for design monochrome android:drawable="@mipmap/ic_launcher_monochrome" /-->
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 7 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 9.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 13 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 19 KiB |
|
|
@ -17,8 +17,8 @@
|
|||
<resources>
|
||||
|
||||
<style name="Theme.ElementX.Splash" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/black</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_bg_dark</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/transparent</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
||||
</style>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2022 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
|
@ -16,11 +15,8 @@
|
|||
-->
|
||||
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<!-- Must be equal to DarkDesignTokens.colorThemeBg -->
|
||||
<color name="splashscreen_bg_dark">#FF101317</color>
|
||||
<!-- Must be equal to LightDesignTokens.colorThemeBg -->
|
||||
<color name="splashscreen_bg_light">#FFFFFFFF</color>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
-->
|
||||
|
||||
<resources>
|
||||
<style name="Theme.ElementX.Splash" parent="Theme.SplashScreen.IconBackground">
|
||||
<item name="windowSplashScreenBackground">@color/white</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||
<style name="Theme.ElementX.Splash" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_bg_light</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/transparent</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
||||
</style>
|
||||
<style name="Theme.ElementX" parent="Theme.Material3.Light" />
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.appnav
|
|||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
|
|
@ -88,14 +89,12 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
Timber.v("OnCreate")
|
||||
inputs.room.open()
|
||||
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.room) }
|
||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||
fetchRoomMembers()
|
||||
},
|
||||
onDestroy = {
|
||||
Timber.v("OnDestroy")
|
||||
inputs.room.close()
|
||||
plugins<LifecycleCallback>().forEach { it.onFlowReleased(id, inputs.room) }
|
||||
appNavigationStateService.onLeavingRoom(id)
|
||||
}
|
||||
|
|
@ -161,6 +160,15 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
// Rely on the View Lifecycle instead of the Node Lifecycle,
|
||||
// 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.
|
||||
DisposableEffect(Unit) {
|
||||
inputs.room.open()
|
||||
onDispose {
|
||||
inputs.room.close()
|
||||
}
|
||||
}
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ koverMerged {
|
|||
excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*"
|
||||
excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState"
|
||||
excludes += "io.element.android.features.messages.impl.media.local.LocalMediaViewState"
|
||||
excludes += "io.element.android.features.location.api.MapState"
|
||||
excludes += "io.element.android.features.location.impl.map.MapState"
|
||||
}
|
||||
bound {
|
||||
minValue = 90
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ open class AnalyticsPreferencesStateProvider : PreviewParameterProvider<Analytic
|
|||
}
|
||||
|
||||
fun aAnalyticsPreferencesState() = AnalyticsPreferencesState(
|
||||
applicationName = "ElementX",
|
||||
applicationName = "Element X",
|
||||
isEnabled = false,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@
|
|||
package io.element.android.features.analytics.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import javax.inject.Inject
|
||||
|
||||
open class AnalyticsOptInStateProvider @Inject constructor(
|
||||
|
|
@ -30,6 +28,6 @@ open class AnalyticsOptInStateProvider @Inject constructor(
|
|||
}
|
||||
|
||||
fun aAnalyticsOptInState() = AnalyticsOptInState(
|
||||
applicationName = "ElementX",
|
||||
applicationName = "Element X",
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
|
@ -50,14 +49,12 @@ import io.element.android.features.invitelist.impl.model.InviteSender
|
|||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.noFontPadding
|
||||
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
private val minHeight = 72.dp
|
||||
|
|
@ -161,8 +158,7 @@ internal fun DefaultInviteSummaryRow(
|
|||
}
|
||||
}
|
||||
|
||||
val unreadIndicatorColor = if (invite.isNew) MaterialTheme.roomListUnreadIndicator() else Color.Transparent
|
||||
UnreadIndicatorAtom(color = unreadIndicatorColor)
|
||||
UnreadIndicatorAtom(isVisible = invite.isNew)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@
|
|||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
|
|
@ -25,29 +23,14 @@ android {
|
|||
namespace = "io.element.android.features.location.api"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.network)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(libs.maplibre)
|
||||
implementation(libs.network.retrofit)
|
||||
implementation(libs.maplibre.annotation)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.serialization.json)
|
||||
implementation(libs.accompanist.permission)
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||
|
||||
/**
|
||||
* The "Send location" screen.
|
||||
*
|
||||
* Allows a user to share a location message within a room.
|
||||
*/
|
||||
interface SendLocationEntryPoint : SimpleFeatureEntryPoint
|
||||
|
|
@ -27,7 +27,7 @@ private const val STATIC_MAP_SCALE_2X = "@2x"
|
|||
private const val STATIC_MAP_MAX_WIDTH_HEIGHT = 2048
|
||||
private const val STATIC_MAP_MAX_ZOOM = 22.0
|
||||
|
||||
internal fun buildTileServerUrl(
|
||||
fun buildTileServerUrl(
|
||||
darkMode: Boolean
|
||||
): String = if (!darkMode) {
|
||||
"$BASE_URL/maps/$LIGHT_MAP_ID/style.json?key=$API_KEY"
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.location.fake"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.dagger)
|
||||
api(projects.features.location.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.network)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(libs.maplibre)
|
||||
implementation(libs.network.retrofit)
|
||||
implementation(libs.maplibre.annotation)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.serialization.json)
|
||||
implementation(libs.accompanist.permission)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@
|
|||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
|
|
@ -30,18 +29,18 @@ anvil {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.dagger)
|
||||
api(projects.features.location.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.network)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(libs.maplibre)
|
||||
implementation(libs.network.retrofit)
|
||||
implementation(libs.maplibre.annotation)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.serialization.json)
|
||||
implementation(libs.accompanist.permission)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.location.api.SendLocationEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class SendLocationEntryPointImpl @Inject constructor() : SendLocationEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node, buildContext: BuildContext
|
||||
): SendLocationNode = parentNode.createNode(buildContext)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.impl
|
||||
|
||||
sealed interface SendLocationEvents {
|
||||
data class ShareLocation(val lat: Double, val lng: Double) : SendLocationEvents
|
||||
data class SwitchMode(val mode: SendLocationState.Mode) : SendLocationEvents
|
||||
}
|
||||
|
|
@ -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.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class SendLocationNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: SendLocationPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
SendLocationView(
|
||||
state = presenter.present(),
|
||||
modifier = modifier,
|
||||
onBackPressed = ::navigateUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class SendLocationPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
) : Presenter<SendLocationState> {
|
||||
@Composable
|
||||
override fun present(): SendLocationState {
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var mode by remember {
|
||||
mutableStateOf<SendLocationState.Mode>(SendLocationState.Mode.ALocation)
|
||||
}
|
||||
|
||||
fun handleEvents(event: SendLocationEvents) {
|
||||
when (event) {
|
||||
is SendLocationEvents.ShareLocation -> scope.launch {
|
||||
shareLocation(event)
|
||||
}
|
||||
is SendLocationEvents.SwitchMode -> {
|
||||
mode = event.mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SendLocationState(
|
||||
mode = mode,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun shareLocation(
|
||||
event: SendLocationEvents.ShareLocation
|
||||
) {
|
||||
room.sendLocation(
|
||||
body = "Location at latitude: ${event.lat}, longitude: ${event.lng}",
|
||||
geoUri = "geo:${event.lat},${event.lng}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.impl
|
||||
|
||||
data class SendLocationState(
|
||||
val mode: Mode = Mode.ALocation,
|
||||
val eventSink: (SendLocationEvents) -> Unit = {},
|
||||
) {
|
||||
sealed interface Mode {
|
||||
object MyLocation : Mode
|
||||
object ALocation : Mode
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
class SendLocationStateProvider : PreviewParameterProvider<SendLocationState> {
|
||||
override val values: Sequence<SendLocationState>
|
||||
get() = sequenceOf(
|
||||
SendLocationState(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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.impl
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.location.api.R
|
||||
import io.element.android.features.location.impl.map.MapView
|
||||
import io.element.android.features.location.impl.map.rememberMapState
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun SendLocationView(
|
||||
state: SendLocationState,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
) {
|
||||
val mapState = rememberMapState()
|
||||
BottomSheetScaffold(
|
||||
sheetContent = {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(stringResource(CommonStrings.screen_share_this_location_action))
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
state.eventSink(
|
||||
SendLocationEvents.ShareLocation(
|
||||
lat = mapState.position.lat,
|
||||
lng = mapState.position.lon
|
||||
)
|
||||
)
|
||||
onBackPressed()
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(Icons.Default.LocationOn, null)
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
},
|
||||
modifier = modifier,
|
||||
scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded),
|
||||
),
|
||||
sheetDragHandle = {},
|
||||
sheetSwipeEnabled = false,
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.screen_share_location_title),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackPressed)
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.consumeWindowInsets(it),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MapView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
mapState = mapState,
|
||||
)
|
||||
Icon(
|
||||
resourceId = R.drawable.pin,
|
||||
contentDescription = null,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SendLocationViewLightPreview(@PreviewParameter(SendLocationStateProvider::class) state: SendLocationState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SendLocationViewDarkPreview(@PreviewParameter(SendLocationStateProvider::class) state: SendLocationState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: SendLocationState) {
|
||||
SendLocationView(
|
||||
state = state,
|
||||
onBackPressed = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.api
|
||||
package io.element.android.features.location.impl.location
|
||||
|
||||
/**
|
||||
* Represents a location sample emitted by the device's location subsystem.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl
|
||||
package io.element.android.features.location.impl.location
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
|
|
@ -25,7 +25,6 @@ import androidx.core.location.LocationListenerCompat
|
|||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.core.location.LocationRequestCompat
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.features.location.api.Location
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
|
|
@ -14,15 +14,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.api
|
||||
package io.element.android.features.location.impl.map
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.Gravity
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -31,7 +29,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
|
|
@ -47,11 +44,11 @@ import com.mapbox.mapboxsdk.maps.MapboxMap
|
|||
import com.mapbox.mapboxsdk.maps.Style
|
||||
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
|
||||
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
|
||||
import io.element.android.features.location.api.R
|
||||
import io.element.android.features.location.api.internal.buildTileServerUrl
|
||||
import io.element.android.features.location.impl.location.Location
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -68,7 +65,6 @@ fun MapView(
|
|||
modifier: Modifier = Modifier,
|
||||
mapState: MapState = rememberMapState(),
|
||||
darkMode: Boolean = !ElementTheme.isLightTheme,
|
||||
onLocationClick: () -> Unit,
|
||||
) {
|
||||
// When in preview, early return a Box with the received modifier preserving layout
|
||||
if (LocalInspectionMode.current) {
|
||||
|
|
@ -88,7 +84,10 @@ fun MapView(
|
|||
LaunchedEffect(darkMode) {
|
||||
mapView.awaitMap().let { map ->
|
||||
map.uiSettings.apply {
|
||||
attributionGravity = Gravity.TOP
|
||||
logoGravity = Gravity.TOP
|
||||
isCompassEnabled = false
|
||||
isRotateGesturesEnabled = false
|
||||
}
|
||||
map.setStyle(buildTileServerUrl(darkMode = darkMode)) { style ->
|
||||
mapRefs = MapRefs(
|
||||
|
|
@ -180,20 +179,10 @@ fun MapView(
|
|||
}
|
||||
|
||||
@Suppress("ModifierReused")
|
||||
Box(modifier = modifier) {
|
||||
AndroidView(factory = { mapView })
|
||||
FloatingActionButton(
|
||||
onClick = onLocationClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.LocationOn,
|
||||
contentDescription = null, // TODO
|
||||
)
|
||||
}
|
||||
}
|
||||
AndroidView(
|
||||
factory = { mapView },
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -292,6 +281,5 @@ private fun ContentToPreview() {
|
|||
)
|
||||
).toImmutableList()
|
||||
),
|
||||
onLocationClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.impl
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class SendLocationPresenterTest {
|
||||
|
||||
private val room = FakeMatrixRoom()
|
||||
private val presenter = SendLocationPresenter(room)
|
||||
|
||||
@Test
|
||||
fun `emits initial state`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
Truth.assertThat(awaitItem().mode).isEqualTo(SendLocationState.Mode.ALocation)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `share location event shares a location`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(SendLocationEvents.ShareLocation(1.0, 2.0))
|
||||
delay(1)
|
||||
Truth.assertThat(room.sendLocationCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `switches mode`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(SendLocationEvents.SwitchMode(SendLocationState.Mode.MyLocation))
|
||||
Truth.assertThat(awaitItem().mode).isEqualTo(SendLocationState.Mode.MyLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,21 +14,20 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.fake
|
||||
package io.element.android.features.location.impl.location
|
||||
|
||||
import io.element.android.features.location.api.Location
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
fun fakeLocationUpdatesFlow(): Flow<Location> = flow {
|
||||
fun fakeLocationUpdatesFlow(): Flow<io.element.android.features.location.impl.location.Location> = flow {
|
||||
while (true) {
|
||||
delay(1_000)
|
||||
emit(aLocation())
|
||||
}
|
||||
}
|
||||
|
||||
private fun aLocation() = Location(
|
||||
private fun aLocation() = io.element.android.features.location.impl.location.Location(
|
||||
lat = 51.49404,
|
||||
lon = -0.25484,
|
||||
accuracy = 5f
|
||||
|
|
@ -29,6 +29,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.location.api.SendLocationEntryPoint
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode
|
||||
|
|
@ -57,6 +58,7 @@ import kotlinx.parcelize.Parcelize
|
|||
class MessagesFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val sendLocationEntryPoint: SendLocationEntryPoint,
|
||||
) : BackstackNode<MessagesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Messages,
|
||||
|
|
@ -88,6 +90,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data class ReportMessage(val eventId: EventId, val senderId: UserId) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object SendLocation : NavTarget
|
||||
}
|
||||
|
||||
private val callback = plugins<MessagesEntryPoint.Callback>().firstOrNull()
|
||||
|
|
@ -123,6 +128,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
override fun onReportMessage(eventId: EventId, senderId: UserId) {
|
||||
backstack.push(NavTarget.ReportMessage(eventId, senderId))
|
||||
}
|
||||
|
||||
override fun onSendLocationClicked() {
|
||||
backstack.push(NavTarget.SendLocation)
|
||||
}
|
||||
}
|
||||
createNode<MessagesNode>(buildContext, listOf(callback))
|
||||
}
|
||||
|
|
@ -155,6 +164,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
val inputs = ReportMessageNode.Inputs(navTarget.eventId, navTarget.senderId)
|
||||
createNode<ReportMessageNode>(buildContext, listOf(inputs))
|
||||
}
|
||||
NavTarget.SendLocation -> {
|
||||
sendLocationEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ class MessagesNode @AssistedInject constructor(
|
|||
fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo)
|
||||
fun onForwardEventClicked(eventId: EventId)
|
||||
fun onReportMessage(eventId: EventId, senderId: UserId)
|
||||
fun onSendLocationClicked()
|
||||
}
|
||||
|
||||
init {
|
||||
|
|
@ -93,6 +94,10 @@ class MessagesNode @AssistedInject constructor(
|
|||
override fun onReportContentClicked(eventId: EventId, senderId: UserId) {
|
||||
callback?.onReportMessage(eventId, senderId)
|
||||
}
|
||||
|
||||
private fun onSendLocationClicked() {
|
||||
callback?.onSendLocationClicked()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
|
|
@ -104,6 +109,7 @@ class MessagesNode @AssistedInject constructor(
|
|||
onEventClicked = this::onEventClicked,
|
||||
onPreviewAttachments = this::onPreviewAttachments,
|
||||
onUserDataClicked = this::onUserDataClicked,
|
||||
onSendLocationClicked = this::onSendLocationClicked,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialogType
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
|
|
@ -91,6 +92,7 @@ fun MessagesView(
|
|||
onEventClicked: (event: TimelineItem.Event) -> Unit,
|
||||
onUserDataClicked: (UserId) -> Unit,
|
||||
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
||||
|
|
@ -152,7 +154,8 @@ fun MessagesView(
|
|||
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
|
||||
}
|
||||
},
|
||||
onReactionClicked = ::onEmojiReactionClicked
|
||||
onReactionClicked = ::onEmojiReactionClicked,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
|
|
@ -216,7 +219,15 @@ private fun AttachmentStateView(
|
|||
is AttachmentsState.Previewing -> LaunchedEffect(state) {
|
||||
onPreviewAttachments(state.attachments)
|
||||
}
|
||||
is AttachmentsState.Sending -> ProgressDialog(text = stringResource(id = CommonStrings.common_loading))
|
||||
is AttachmentsState.Sending -> {
|
||||
ProgressDialog(
|
||||
type = when (state) {
|
||||
is AttachmentsState.Sending.Uploading -> ProgressDialogType.Determinate(state.progress)
|
||||
is AttachmentsState.Sending.Processing -> ProgressDialogType.Indeterminate
|
||||
},
|
||||
text = stringResource(id = CommonStrings.common_sending)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,6 +239,7 @@ fun MessagesViewContent(
|
|||
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
|
|
@ -251,6 +263,7 @@ fun MessagesViewContent(
|
|||
if (state.userHasPermissionToSendMessage) {
|
||||
MessageComposerView(
|
||||
state = state.composerState,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(Alignment.Bottom)
|
||||
|
|
@ -338,5 +351,6 @@ private fun ContentToPreview(state: MessagesState) {
|
|||
onEventClicked = {},
|
||||
onPreviewAttachments = {},
|
||||
onUserDataClicked = {},
|
||||
onSendLocationClicked = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import androidx.compose.material.icons.outlined.AddReaction
|
|||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -48,6 +47,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -87,8 +87,8 @@ fun ActionListView(
|
|||
onEmojiReactionClicked: (String, TimelineItem.Event) -> Unit,
|
||||
onCustomReactionClicked: (TimelineItem.Event) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sheetState: SheetState = rememberModalBottomSheetState()
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val targetItem = (state.target as? ActionListState.Target.Success)?.event
|
||||
|
||||
|
|
@ -119,22 +119,21 @@ fun ActionListView(
|
|||
}
|
||||
|
||||
fun onDismiss() {
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(ActionListEvents.Clear)
|
||||
}
|
||||
state.eventSink(ActionListEvents.Clear)
|
||||
}
|
||||
|
||||
if (targetItem != null) {
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = ::onDismiss,
|
||||
modifier = modifier,
|
||||
) {
|
||||
SheetContent(
|
||||
state = state,
|
||||
onActionClicked = ::onItemActionClicked,
|
||||
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
||||
onCustomReactionClicked = ::onCustomReactionClicked,
|
||||
modifier = modifier
|
||||
modifier = Modifier
|
||||
.padding(bottom = 32.dp)
|
||||
// .navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
|
||||
// .imePadding()
|
||||
|
|
@ -192,7 +191,7 @@ private fun SheetContent(
|
|||
},
|
||||
text = {
|
||||
Text(
|
||||
text = action.title,
|
||||
text = stringResource(id = action.titleRes),
|
||||
color = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,20 +17,22 @@
|
|||
package io.element.android.features.messages.impl.actionlist.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.designsystem.VectorIcons
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Immutable
|
||||
sealed class TimelineItemAction(
|
||||
val title: String,
|
||||
@StringRes val titleRes: Int,
|
||||
@DrawableRes val icon: Int,
|
||||
val destructive: Boolean = false
|
||||
) {
|
||||
object Forward : TimelineItemAction("Forward", VectorIcons.Forward)
|
||||
object Copy : TimelineItemAction("Copy", VectorIcons.Copy)
|
||||
object Redact : TimelineItemAction("Redact", VectorIcons.Delete, destructive = true)
|
||||
object Reply : TimelineItemAction("Reply", VectorIcons.Reply)
|
||||
object Edit : TimelineItemAction("Edit", VectorIcons.Edit)
|
||||
object Developer : TimelineItemAction("Developer", VectorIcons.DeveloperMode)
|
||||
object ReportContent : TimelineItemAction("Report content", VectorIcons.ReportContent, destructive = true)
|
||||
object Forward : TimelineItemAction(CommonStrings.action_forward, VectorIcons.Forward)
|
||||
object Copy : TimelineItemAction(CommonStrings.action_copy, VectorIcons.Copy)
|
||||
object Redact : TimelineItemAction(CommonStrings.action_remove, VectorIcons.Delete, destructive = true)
|
||||
object Reply : TimelineItemAction(CommonStrings.action_reply, VectorIcons.Reply)
|
||||
object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit)
|
||||
object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode)
|
||||
object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,8 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -48,13 +47,13 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val sendActionState = remember {
|
||||
mutableStateOf<Async<Unit>>(Async.Uninitialized)
|
||||
mutableStateOf<SendActionState>(SendActionState.Idle)
|
||||
}
|
||||
|
||||
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
||||
when (attachmentsPreviewEvents) {
|
||||
AttachmentsPreviewEvents.SendAttachment -> coroutineScope.sendAttachment(attachment, sendActionState)
|
||||
AttachmentsPreviewEvents.ClearSendState -> sendActionState.value = Async.Uninitialized
|
||||
AttachmentsPreviewEvents.ClearSendState -> sendActionState.value = SendActionState.Idle
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +66,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
|
||||
private fun CoroutineScope.sendAttachment(
|
||||
attachment: Attachment,
|
||||
sendActionState: MutableState<Async<Unit>>,
|
||||
sendActionState: MutableState<SendActionState>,
|
||||
) = launch {
|
||||
when (attachment) {
|
||||
is Attachment.Media -> {
|
||||
|
|
@ -81,10 +80,26 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
|
||||
private suspend fun sendMedia(
|
||||
mediaAttachment: Attachment.Media,
|
||||
sendActionState: MutableState<Async<Unit>>,
|
||||
sendActionState: MutableState<SendActionState>,
|
||||
) {
|
||||
sendActionState.runUpdatingState {
|
||||
mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.info.mimeType, mediaAttachment.compressIfPossible)
|
||||
val progressCallback = object : ProgressCallback {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
|
||||
}
|
||||
}
|
||||
sendActionState.value = SendActionState.Sending.Processing
|
||||
mediaSender.sendMedia(
|
||||
uri = mediaAttachment.localMedia.uri,
|
||||
mimeType = mediaAttachment.localMedia.info.mimeType,
|
||||
compressIfPossible = mediaAttachment.compressIfPossible,
|
||||
progressCallback = progressCallback
|
||||
).fold(
|
||||
onSuccess = {
|
||||
sendActionState.value = SendActionState.Done
|
||||
},
|
||||
onFailure = {
|
||||
sendActionState.value = SendActionState.Failure(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,21 @@
|
|||
package io.element.android.features.messages.impl.attachments.preview
|
||||
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
||||
data class AttachmentsPreviewState(
|
||||
val attachment: Attachment,
|
||||
val sendActionState: Async<Unit>,
|
||||
val sendActionState: SendActionState,
|
||||
val eventSink: (AttachmentsPreviewEvents) -> Unit
|
||||
)
|
||||
|
||||
sealed interface SendActionState {
|
||||
object Idle : SendActionState
|
||||
sealed interface Sending : SendActionState {
|
||||
object Processing : Sending
|
||||
data class Uploading(val progress: Float) : Sending
|
||||
}
|
||||
|
||||
data class Failure(val error: Throwable) : SendActionState
|
||||
object Done : SendActionState
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,23 +22,21 @@ import io.element.android.features.messages.impl.attachments.Attachment
|
|||
import io.element.android.features.messages.impl.media.local.LocalMedia
|
||||
import io.element.android.features.messages.impl.media.local.MediaInfo
|
||||
import io.element.android.features.messages.impl.media.local.aFileInfo
|
||||
import io.element.android.features.messages.impl.media.local.aVideoInfo
|
||||
import io.element.android.features.messages.impl.media.local.anImageInfo
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
||||
open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
|
||||
override val values: Sequence<AttachmentsPreviewState>
|
||||
get() = sequenceOf(
|
||||
anAttachmentsPreviewState(),
|
||||
anAttachmentsPreviewState(mediaInfo = aFileInfo()),
|
||||
anAttachmentsPreviewState(sendActionState = Async.Loading()),
|
||||
anAttachmentsPreviewState(sendActionState = Async.Failure(RuntimeException())),
|
||||
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
|
||||
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException())),
|
||||
)
|
||||
}
|
||||
|
||||
fun anAttachmentsPreviewState(
|
||||
mediaInfo: MediaInfo = anImageInfo(),
|
||||
sendActionState: Async<Unit> = Async.Uninitialized) = AttachmentsPreviewState(
|
||||
sendActionState: SendActionState = SendActionState.Idle) = AttachmentsPreviewState(
|
||||
attachment = Attachment.Media(
|
||||
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
||||
compressIfPossible = true
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ import androidx.compose.ui.unit.dp
|
|||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||
import io.element.android.features.messages.impl.media.local.LocalMediaView
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialogType
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
|
|
@ -58,7 +58,7 @@ fun AttachmentsPreviewView(
|
|||
state.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
||||
}
|
||||
|
||||
if (state.sendActionState is Async.Success) {
|
||||
if (state.sendActionState is SendActionState.Done) {
|
||||
LaunchedEffect(state.sendActionState) {
|
||||
onDismiss()
|
||||
}
|
||||
|
|
@ -78,26 +78,32 @@ fun AttachmentsPreviewView(
|
|||
}
|
||||
AttachmentSendStateView(
|
||||
sendActionState = state.sendActionState,
|
||||
onRetryClicked = ::postSendAttachment,
|
||||
onRetryDismissed = ::postClearSendState
|
||||
onDismissClicked = ::postClearSendState,
|
||||
onRetryClicked = ::postSendAttachment
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AttachmentSendStateView(
|
||||
sendActionState: Async<Unit>,
|
||||
onRetryDismissed: () -> Unit,
|
||||
sendActionState: SendActionState,
|
||||
onDismissClicked: () -> Unit,
|
||||
onRetryClicked: () -> Unit
|
||||
) {
|
||||
when (sendActionState) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog(text = stringResource(id = CommonStrings.common_loading))
|
||||
}
|
||||
|
||||
is Async.Failure -> {
|
||||
when (sendActionState) {
|
||||
is SendActionState.Sending -> {
|
||||
ProgressDialog(
|
||||
type = when (sendActionState) {
|
||||
is SendActionState.Sending.Uploading -> ProgressDialogType.Determinate(sendActionState.progress)
|
||||
SendActionState.Sending.Processing -> ProgressDialogType.Indeterminate
|
||||
},
|
||||
text = stringResource(id = CommonStrings.common_sending)
|
||||
)
|
||||
}
|
||||
is SendActionState.Failure -> {
|
||||
RetryDialog(
|
||||
content = stringResource(sendAttachmentError(sendActionState.error)),
|
||||
onDismiss = onRetryDismissed,
|
||||
onDismiss = onDismissClicked,
|
||||
onRetry = onRetryClicked
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.compose.material.ListItem
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AttachFile
|
||||
import androidx.compose.material.icons.filled.Collections
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.PhotoCamera
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
|
@ -48,6 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
@Composable
|
||||
internal fun AttachmentsBottomSheet(
|
||||
state: MessageComposerState,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val localView = LocalView.current
|
||||
|
|
@ -78,7 +80,10 @@ internal fun AttachmentsBottomSheet(
|
|||
modifier = modifier,
|
||||
onDismissRequest = { isVisible = false }
|
||||
) {
|
||||
AttachmentSourcePickerMenu(eventSink = state.eventSink)
|
||||
AttachmentSourcePickerMenu(
|
||||
eventSink = state.eventSink,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +92,7 @@ internal fun AttachmentsBottomSheet(
|
|||
@Composable
|
||||
internal fun AttachmentSourcePickerMenu(
|
||||
eventSink: (MessageComposerEvents) -> Unit,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
|
|
@ -113,5 +119,13 @@ internal fun AttachmentSourcePickerMenu(
|
|||
icon = { Icon(Icons.Default.Videocam, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) },
|
||||
)
|
||||
ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
eventSink(MessageComposerEvents.PickAttachmentSource.Location)
|
||||
onSendLocationClicked()
|
||||
},
|
||||
icon = { Icon(Icons.Default.LocationOn, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_source_location)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,6 @@ sealed interface MessageComposerEvents {
|
|||
object FromFiles : PickAttachmentSource
|
||||
object PhotoFromCamera : PickAttachmentSource
|
||||
object VideoFromCamera : PickAttachmentSource
|
||||
object Location : PickAttachmentSource
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import io.element.android.libraries.di.RoomScope
|
|||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
|
|
@ -110,7 +111,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
|
||||
LaunchedEffect(attachmentsState.value) {
|
||||
when (val attachmentStateValue = attachmentsState.value) {
|
||||
is AttachmentsState.Sending -> localCoroutineScope.sendAttachment(attachmentStateValue.attachments.first(), attachmentsState)
|
||||
is AttachmentsState.Sending.Processing -> localCoroutineScope.sendAttachment(attachmentStateValue.attachments.first(), attachmentsState)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -158,6 +159,10 @@ class MessageComposerPresenter @Inject constructor(
|
|||
showAttachmentSourcePicker = false
|
||||
cameraVideoPicker.launch()
|
||||
}
|
||||
MessageComposerEvents.PickAttachmentSource.Location -> {
|
||||
showAttachmentSourcePicker = false
|
||||
// Navigation to the location picker screen is done at the view layer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +250,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
attachmentsState.value = if (isPreviewable) {
|
||||
AttachmentsState.Previewing(persistentListOf(mediaAttachment))
|
||||
} else {
|
||||
AttachmentsState.Sending(persistentListOf(mediaAttachment))
|
||||
AttachmentsState.Sending.Processing(persistentListOf(mediaAttachment))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +259,12 @@ class MessageComposerPresenter @Inject constructor(
|
|||
mimeType: String,
|
||||
attachmentState: MutableState<AttachmentsState>,
|
||||
) {
|
||||
mediaSender.sendMedia(uri, mimeType, compressIfPossible = false)
|
||||
val progressCallback = object : ProgressCallback {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
attachmentState.value = AttachmentsState.Sending.Uploading(current.toFloat() / total.toFloat())
|
||||
}
|
||||
}
|
||||
mediaSender.sendMedia(uri, mimeType, compressIfPossible = false, progressCallback)
|
||||
.onSuccess {
|
||||
attachmentState.value = AttachmentsState.None
|
||||
}.onFailure {
|
||||
|
|
|
|||
|
|
@ -39,5 +39,8 @@ data class MessageComposerState(
|
|||
sealed interface AttachmentsState {
|
||||
object None : AttachmentsState
|
||||
data class Previewing(val attachments: ImmutableList<Attachment>) : AttachmentsState
|
||||
data class Sending(val attachments: ImmutableList<Attachment>) : AttachmentsState
|
||||
sealed interface Sending : AttachmentsState {
|
||||
data class Processing(val attachments: ImmutableList<Attachment>) : Sending
|
||||
data class Uploading(val progress: Float) : Sending
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.textcomposer.TextComposer
|
|||
@Composable
|
||||
fun MessageComposerView(
|
||||
state: MessageComposerState,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onFullscreenToggle() {
|
||||
|
|
@ -55,7 +56,10 @@ fun MessageComposerView(
|
|||
}
|
||||
|
||||
Box {
|
||||
AttachmentsBottomSheet(state = state)
|
||||
AttachmentsBottomSheet(
|
||||
state = state,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
)
|
||||
|
||||
TextComposer(
|
||||
onSendMessage = ::sendMessage,
|
||||
|
|
@ -83,5 +87,8 @@ internal fun MessageComposerViewDarkPreview(@PreviewParameter(MessageComposerSta
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: MessageComposerState) {
|
||||
MessageComposerView(state)
|
||||
MessageComposerView(
|
||||
state = state,
|
||||
onSendLocationClicked = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,21 +59,16 @@ class TimelinePresenter @Inject constructor(
|
|||
var lastReadMarkerIndex by rememberSaveable { mutableStateOf(Int.MAX_VALUE) }
|
||||
var lastReadMarkerId by rememberSaveable { mutableStateOf<EventId?>(null) }
|
||||
|
||||
val timelineItems = timelineItemsFactory
|
||||
.flow()
|
||||
.collectAsState()
|
||||
|
||||
val paginationState = timeline
|
||||
.paginationState()
|
||||
.collectAsState()
|
||||
val timelineItems by timelineItemsFactory.collectItemsAsState()
|
||||
val paginationState by timeline.paginationState.collectAsState()
|
||||
|
||||
fun handleEvents(event: TimelineEvents) {
|
||||
when (event) {
|
||||
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState.value)
|
||||
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState)
|
||||
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
|
||||
is TimelineEvents.OnScrollFinished -> {
|
||||
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
|
||||
val eventId = getLastEventIdBeforeOrAt(event.firstIndex, timelineItems.value) ?: return
|
||||
val eventId = getLastEventIdBeforeOrAt(event.firstIndex, timelineItems) ?: return
|
||||
if (event.firstIndex <= lastReadMarkerIndex && eventId != lastReadMarkerId) {
|
||||
lastReadMarkerIndex = event.firstIndex
|
||||
lastReadMarkerId = eventId
|
||||
|
|
@ -85,11 +80,11 @@ class TimelinePresenter @Inject constructor(
|
|||
|
||||
LaunchedEffect(Unit) {
|
||||
timeline
|
||||
.timelineItems()
|
||||
.timelineItems
|
||||
.onEach(timelineItemsFactory::replaceWith)
|
||||
.onEach { timelineItems ->
|
||||
if (timelineItems.isEmpty()) {
|
||||
loadMore(paginationState.value)
|
||||
loadMore(paginationState)
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
|
|
@ -97,8 +92,8 @@ class TimelinePresenter @Inject constructor(
|
|||
|
||||
return TimelineState(
|
||||
highlightedEventId = highlightedEventId.value,
|
||||
paginationState = paginationState.value,
|
||||
timelineItems = timelineItems.value,
|
||||
paginationState = paginationState,
|
||||
timelineItems = timelineItems,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
|
|
@ -62,7 +61,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
|
|
@ -123,8 +121,7 @@ fun TimelineView(
|
|||
|
||||
TimelineScrollHelper(
|
||||
lazyListState = lazyListState,
|
||||
timelineItems = state.timelineItems,
|
||||
onLoadMore = ::onReachedLoadMore
|
||||
timelineItems = state.timelineItems
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -222,7 +219,6 @@ fun TimelineItemRow(
|
|||
internal fun BoxScope.TimelineScrollHelper(
|
||||
lazyListState: LazyListState,
|
||||
timelineItems: ImmutableList<TimelineItem>,
|
||||
onLoadMore: () -> Unit = {},
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val firstVisibleItemIndex by remember { derivedStateOf { lazyListState.firstVisibleItemIndex } }
|
||||
|
|
@ -236,24 +232,6 @@ internal fun BoxScope.TimelineScrollHelper(
|
|||
}
|
||||
}
|
||||
|
||||
// Handle load more preloading
|
||||
val loadMore by remember {
|
||||
derivedStateOf {
|
||||
val layoutInfo = lazyListState.layoutInfo
|
||||
val totalItemsNumber = layoutInfo.totalItemsCount
|
||||
val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1
|
||||
lastVisibleItemIndex > (totalItemsNumber - 30)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(loadMore) {
|
||||
snapshotFlow { loadMore }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
onLoadMore()
|
||||
}
|
||||
}
|
||||
|
||||
// Jump to bottom button
|
||||
if (firstVisibleItemIndex > 2) {
|
||||
FloatingActionButton(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.messageFromMeBackground
|
||||
import io.element.android.libraries.designsystem.theme.messageFromOtherBackground
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
||||
private val BUBBLE_RADIUS = 12.dp
|
||||
|
|
@ -97,14 +99,11 @@ fun MessageEventBubble(
|
|||
}
|
||||
}
|
||||
|
||||
val backgroundBubbleColor = if (state.isHighlighted) {
|
||||
ElementTheme.legacyColors.messageHighlightedBackground
|
||||
// Ignore state.isHighlighted for now, we need a design decision on it.
|
||||
val backgroundBubbleColor = if (state.isMine) {
|
||||
ElementTheme.colors.messageFromMeBackground
|
||||
} else {
|
||||
if (state.isMine) {
|
||||
ElementTheme.legacyColors.messageFromMeBackground
|
||||
} else {
|
||||
ElementTheme.legacyColors.messageFromOtherBackground
|
||||
}
|
||||
ElementTheme.colors.messageFromOtherBackground
|
||||
}
|
||||
val bubbleShape = bubbleShape()
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -41,18 +41,15 @@ private val CORNER_RADIUS = 8.dp
|
|||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MessageStateEventContainer(
|
||||
isHighlighted: Boolean,
|
||||
@Suppress("UNUSED_PARAMETER") isHighlighted: Boolean,
|
||||
interactionSource: MutableInteractionSource,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
val backgroundColor = if (isHighlighted) {
|
||||
ElementTheme.legacyColors.messageHighlightedBackground
|
||||
} else {
|
||||
Color.Companion.Transparent
|
||||
}
|
||||
// Ignore isHighlighted for now, we need a design decision on it.
|
||||
val backgroundColor = Color.Transparent
|
||||
val shape = RoundedCornerShape(CORNER_RADIUS)
|
||||
Surface(
|
||||
modifier = modifier
|
||||
|
|
|
|||
|
|
@ -58,15 +58,19 @@ fun TimelineEventTimestampView(
|
|||
val hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed
|
||||
val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse()
|
||||
val tint = if (hasMessageSendingFailed) MaterialTheme.colorScheme.error else null
|
||||
val clickModifier = if (hasMessageSendingFailed) {
|
||||
Modifier.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
indication = rememberRipple(bounded = false),
|
||||
interactionSource = MutableInteractionSource()
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
enabled = true,
|
||||
indication = rememberRipple(bounded = false),
|
||||
interactionSource = MutableInteractionSource()
|
||||
)
|
||||
.then(clickModifier)
|
||||
.padding(start = 16.dp) // Add extra padding for touch target size
|
||||
.then(modifier),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -60,7 +59,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -72,6 +70,8 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||
|
|
@ -271,7 +271,7 @@ private fun MessageEventBubbleContent(
|
|||
}
|
||||
} else {
|
||||
Box(modifier) {
|
||||
ContentView(modifier = contentModifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp))
|
||||
ContentView(modifier = contentModifier)
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
|
|
@ -319,7 +319,11 @@ private fun MessageEventBubbleContent(
|
|||
val contentModifier = if (isMediaItem) {
|
||||
Modifier.clip(RoundedCornerShape(12.dp))
|
||||
} else {
|
||||
Modifier
|
||||
if (inReplyToDetails != null) {
|
||||
Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp)
|
||||
} else {
|
||||
Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
|
||||
}
|
||||
}
|
||||
|
||||
ContentAndTimestampView(
|
||||
|
|
@ -366,20 +370,19 @@ private fun ReplyToContent(
|
|||
}
|
||||
Column(verticalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
senderName,
|
||||
style = ElementTextStyles.Regular.caption2.copy(fontWeight = FontWeight.Medium),
|
||||
text = senderName,
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
textAlign = TextAlign.Start,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
color = ElementTheme.materialColors.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = text.orEmpty(),
|
||||
style = ElementTextStyles.Regular.caption1,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
textAlign = TextAlign.Start,
|
||||
color = ElementTheme.legacyColors.placeholder,
|
||||
maxLines = 1,
|
||||
color = ElementTheme.materialColors.secondary,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
|
@ -458,6 +461,76 @@ private fun ContentToPreview() {
|
|||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemEventRowWithReplyLightPreview() =
|
||||
ElementPreviewLight { ContentToPreviewWithReply() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemEventRowWithReplyDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreviewWithReply() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreviewWithReply() {
|
||||
Column {
|
||||
sequenceOf(false, true).forEach {
|
||||
val replyContent = if(it) {
|
||||
// Short
|
||||
"Message which are being replied."
|
||||
} else {
|
||||
// Long, to test 2 lines and ellipsis)
|
||||
"Message which are being replied, and which was long enough to be displayed on two lines (only!)."
|
||||
}
|
||||
TimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = it,
|
||||
content = aTimelineItemTextContent().copy(
|
||||
body = "A long text which will be displayed on several lines and" +
|
||||
" hopefully can be manually adjusted to test different behaviors."
|
||||
),
|
||||
inReplyTo = aInReplyToReady(replyContent)
|
||||
),
|
||||
isHighlighted = false,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
onUserDataClick = {},
|
||||
inReplyToClick = {},
|
||||
onReactionClick = { _, _ -> },
|
||||
onTimestampClicked = {},
|
||||
)
|
||||
TimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = it,
|
||||
content = aTimelineItemImageContent().copy(
|
||||
aspectRatio = 5f
|
||||
),
|
||||
inReplyTo = aInReplyToReady(replyContent)
|
||||
),
|
||||
isHighlighted = false,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
onUserDataClick = {},
|
||||
inReplyToClick = {},
|
||||
onReactionClick = { _, _ -> },
|
||||
onTimestampClicked = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun aInReplyToReady(
|
||||
replyContent: String
|
||||
): InReplyTo.Ready {
|
||||
return InReplyTo.Ready(
|
||||
eventId = EventId("\$event"),
|
||||
content = MessageContent(replyContent, null, false, TextMessageType(replyContent, null)),
|
||||
senderId = UserId("@Sender:domain"),
|
||||
senderDisplayName = "Sender",
|
||||
senderAvatarUrl = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemEventRowTimestampLightPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) =
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.components
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -50,6 +51,7 @@ fun TimelineItemStateEventRow(
|
|||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.wrapContentHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -38,9 +38,7 @@ fun CustomReactionBottomSheet(
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
fun onDismiss() {
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||
}
|
||||
state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||
}
|
||||
|
||||
fun onEmojiSelectedDismiss(emoji: Emoji) {
|
||||
|
|
|
|||
|
|
@ -51,3 +51,11 @@ fun TimelineItem.Event.toExtraPadding(): ExtraPadding {
|
|||
// A space and a few unbreakable spaces
|
||||
return ExtraPadding(" " + "\u00A0".repeat(strLen))
|
||||
}
|
||||
|
||||
fun ExtraPadding.strBigger(): String {
|
||||
return if (str.isEmpty()) {
|
||||
str
|
||||
} else {
|
||||
str + "\u00A0\u00A0\u00A0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ fun TimelineItemInformativeView(
|
|||
fontStyle = FontStyle.Italic,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 14.sp,
|
||||
text = text + extraPadding.str
|
||||
// Since the font size is smaller, add more space to extra padding, to not overlap with the timestamp
|
||||
text = text + extraPadding.strBigger()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ 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.ElementPreviewLight
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
|
@ -49,15 +48,12 @@ private val CORNER_RADIUS = 8.dp
|
|||
fun GroupHeaderView(
|
||||
text: String,
|
||||
isExpanded: Boolean,
|
||||
isHighlighted: Boolean,
|
||||
@Suppress("UNUSED_PARAMETER") isHighlighted: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val backgroundColor = if (isHighlighted) {
|
||||
ElementTheme.legacyColors.messageHighlightedBackground
|
||||
} else {
|
||||
Color.Companion.Transparent
|
||||
}
|
||||
// Ignore isHighlighted for now, we need a design decision on it.
|
||||
val backgroundColor = Color.Companion.Transparent
|
||||
val shape = RoundedCornerShape(CORNER_RADIUS)
|
||||
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -40,6 +39,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.features.messages.impl.R
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline.factories
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import io.element.android.features.messages.impl.timeline.diff.CacheInvalidator
|
||||
import io.element.android.features.messages.impl.timeline.diff.MatrixTimelineItemsDiffCallback
|
||||
|
|
@ -27,11 +30,8 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
|||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -55,7 +55,10 @@ class TimelineItemsFactory @Inject constructor(
|
|||
private val lock = Mutex()
|
||||
private val cacheInvalidator = CacheInvalidator(timelineItemsCache)
|
||||
|
||||
fun flow(): StateFlow<ImmutableList<TimelineItem>> = timelineItems.asStateFlow()
|
||||
@Composable
|
||||
fun collectItemsAsState(): State<ImmutableList<TimelineItem>> {
|
||||
return timelineItems.collectAsState()
|
||||
}
|
||||
|
||||
suspend fun replaceWith(
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ import io.element.android.features.messages.fixtures.aLocalMedia
|
|||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents
|
||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter
|
||||
import io.element.android.features.messages.impl.attachments.preview.SendActionState
|
||||
import io.element.android.features.messages.impl.media.local.LocalMedia
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
|
|
@ -47,17 +47,26 @@ class AttachmentsPreviewPresenterTest {
|
|||
@Test
|
||||
fun `present - send media success scenario`() = runTest {
|
||||
val room = FakeMatrixRoom()
|
||||
room.givenProgressCallbackValues(
|
||||
listOf(
|
||||
Pair(0, 10),
|
||||
Pair(5, 10),
|
||||
Pair(10, 10)
|
||||
)
|
||||
)
|
||||
val presenter = anAttachmentsPreviewPresenter(room = room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.sendActionState).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.sendActionState).isEqualTo(Async.Loading<Unit>())
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0f))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0.5f))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f))
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.sendActionState).isEqualTo(Async.Success(Unit))
|
||||
assertThat(successState.sendActionState).isEqualTo(SendActionState.Done)
|
||||
assertThat(room.sendMediaCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -72,16 +81,16 @@ class AttachmentsPreviewPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.sendActionState).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.sendActionState).isEqualTo(Async.Loading<Unit>())
|
||||
assertThat(loadingState.sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||
val failureState = awaitItem()
|
||||
assertThat(failureState.sendActionState).isEqualTo(Async.Failure<Unit>(failure))
|
||||
assertThat(failureState.sendActionState).isEqualTo((SendActionState.Failure(failure)))
|
||||
assertThat(room.sendMediaCount).isEqualTo(0)
|
||||
failureState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
||||
val clearedState = awaitItem()
|
||||
assertThat(clearedState.sendActionState).isEqualTo(Async.Uninitialized)
|
||||
assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -368,6 +368,13 @@ class MessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - Pick file from storage`() = runTest {
|
||||
val room = FakeMatrixRoom()
|
||||
room.givenProgressCallbackValues(
|
||||
listOf(
|
||||
Pair(0, 10),
|
||||
Pair(5, 10),
|
||||
Pair(10, 10)
|
||||
)
|
||||
)
|
||||
val presenter = createPresenter(this, room = room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -376,7 +383,10 @@ class MessageComposerPresenterTest {
|
|||
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
|
||||
val sendingState = awaitItem()
|
||||
assertThat(sendingState.showAttachmentSourcePicker).isFalse()
|
||||
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java)
|
||||
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending.Processing::class.java)
|
||||
assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.Sending.Uploading(0f))
|
||||
assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.Sending.Uploading(0.5f))
|
||||
assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.Sending.Uploading(1f))
|
||||
val sentState = awaitItem()
|
||||
assertThat(sentState.attachmentsState).isEqualTo(AttachmentsState.None)
|
||||
assertThat(room.sendMediaCount).isEqualTo(1)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ dependencies {
|
|||
api(projects.services.apperror.api)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(projects.features.leaveroom.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
|
|||
|
|
@ -20,18 +20,21 @@ import android.content.Context
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import timber.log.Timber
|
||||
import io.element.android.libraries.androidutils.R as AndroidUtilsR
|
||||
|
||||
|
|
@ -41,6 +44,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomDetailsPresenter,
|
||||
private val room: MatrixRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
interface Callback : Plugin {
|
||||
|
|
@ -51,6 +55,14 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
|
||||
private val callbacks = plugins<Callback>()
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.RoomDetails))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun openRoomMemberList() {
|
||||
callbacks.forEach { it.openRoomMemberList() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ import androidx.compose.material.icons.outlined.Lock
|
|||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material.icons.outlined.PersonAddAlt
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -70,6 +68,9 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.preview.LargeHeightPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItemText
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
|
|
@ -192,12 +193,11 @@ internal fun RoomDetailsTopBar(
|
|||
Icon(Icons.Default.MoreVert, "")
|
||||
}
|
||||
DropdownMenu(
|
||||
modifier = Modifier.widthIn(200.dp),
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = CommonStrings.action_edit)) },
|
||||
text = { DropdownMenuItemText(stringResource(id = CommonStrings.action_edit)) },
|
||||
onClick = {
|
||||
// Explicitly close the menu before handling the action, as otherwise it stays open during the
|
||||
// transition and renders really badly.
|
||||
|
|
|
|||
|
|
@ -18,21 +18,33 @@ package io.element.android.features.roomdetails.impl.edit
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RoomDetailsEditNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomDetailsEditPresenter,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.RoomSettings))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
|
|
|||
|
|
@ -19,16 +19,19 @@ package io.element.android.features.roomdetails.impl.invite
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.apperror.api.AppErrorStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
|
@ -42,10 +45,19 @@ class RoomInviteMembersNode @AssistedInject constructor(
|
|||
private val room: MatrixRoom,
|
||||
private val presenter: RoomInviteMembersPresenter,
|
||||
private val appErrorStateService: AppErrorStateService,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.io)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.Invites))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
|
|
|||
|
|
@ -18,21 +18,25 @@ package io.element.android.features.roomdetails.impl.members
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RoomMemberListNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomMemberListPresenter,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
interface Callback : Plugin {
|
||||
|
|
@ -42,6 +46,14 @@ class RoomMemberListNode @AssistedInject constructor(
|
|||
|
||||
private val callbacks = plugins<Callback>()
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.RoomMembers))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun openRoomMemberDetails(roomMemberId: UserId) {
|
||||
callbacks.forEach {
|
||||
it.openRoomMemberDetails(roomMemberId)
|
||||
|
|
|
|||
|
|
@ -19,11 +19,13 @@ package io.element.android.features.roomdetails.impl.members.details
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
|
|
@ -32,6 +34,7 @@ import io.element.android.libraries.architecture.inputs
|
|||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import timber.log.Timber
|
||||
import io.element.android.libraries.androidutils.R as AndroidUtilsR
|
||||
|
||||
|
|
@ -39,6 +42,7 @@ import io.element.android.libraries.androidutils.R as AndroidUtilsR
|
|||
class RoomMemberDetailsNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val analyticsService: AnalyticsService,
|
||||
presenterFactory: RoomMemberDetailsPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
|
|
@ -49,6 +53,14 @@ class RoomMemberDetailsNode @AssistedInject constructor(
|
|||
private val inputs = inputs<RoomMemberDetailsInput>()
|
||||
private val presenter = presenterFactory.create(inputs.roomMemberId)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.User))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ dependencies {
|
|||
implementation(projects.features.invitelist.api)
|
||||
implementation(projects.features.networkmonitor.api)
|
||||
implementation(projects.features.leaveroom.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(libs.accompanist.placeholder)
|
||||
api(projects.features.roomlist.api)
|
||||
ksp(libs.showkase.processor)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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.roomlist.impl
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun InvitesEntryPointView(
|
||||
onInvitesClicked: () -> Unit,
|
||||
state: InvitesState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(role = Role.Button, onClick = onInvitesClicked)
|
||||
.padding(start = 24.dp, end = 16.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.heightIn(min = 40.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.action_invites_list),
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
|
||||
if (state == InvitesState.NewInvites) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
UnreadIndicatorAtom()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun InvitesEntryPointViewLightPreview(@PreviewParameter(InvitesStateProvider::class) state: InvitesState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun InvitesEntryPointViewDarkPreview(@PreviewParameter(InvitesStateProvider::class) state: InvitesState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: InvitesState) {
|
||||
InvitesEntryPointView(
|
||||
onInvitesClicked = {},
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.roomlist.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
open class InvitesStateProvider : PreviewParameterProvider<InvitesState> {
|
||||
override val values: Sequence<InvitesState>
|
||||
get() = sequenceOf(
|
||||
InvitesState.SeenInvites,
|
||||
InvitesState.NewInvites,
|
||||
)
|
||||
}
|
||||
|
|
@ -20,18 +20,21 @@ import android.app.Activity
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class RoomListNode @AssistedInject constructor(
|
||||
|
|
@ -39,8 +42,17 @@ class RoomListNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomListPresenter,
|
||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.Home))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onRoomClicked(roomId: RoomId) {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onRoomClicked(roomId) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,18 +17,12 @@
|
|||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
|
@ -42,17 +36,14 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
import io.element.android.features.roomlist.impl.components.RequestVerificationHeader
|
||||
|
|
@ -61,18 +52,15 @@ import io.element.android.features.roomlist.impl.components.RoomListTopBar
|
|||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.designsystem.R as DrawableR
|
||||
|
||||
@Composable
|
||||
|
|
@ -206,7 +194,7 @@ fun RoomListContent(
|
|||
|
||||
if (state.invitesState != InvitesState.NoInvites) {
|
||||
item {
|
||||
InvitesEntryPointView(onInvitesClicked, state)
|
||||
InvitesEntryPointView(onInvitesClicked, state.invitesState)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -251,38 +239,6 @@ fun RoomListContent(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InvitesEntryPointView(
|
||||
onInvitesClicked: () -> Unit,
|
||||
state: RoomListState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(role = Role.Button, onClick = onInvitesClicked)
|
||||
.padding(horizontal = 16.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.heightIn(min = 48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.action_invites_list),
|
||||
fontSize = 14.sp,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
if (state.invitesState == InvitesState.NewInvites) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
UnreadIndicatorAtom()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RoomListRoomSummary.contentType() = isPlaceholder
|
||||
|
||||
@Preview
|
||||
|
|
|
|||
|
|
@ -19,12 +19,10 @@ package io.element.android.features.roomlist.impl.components
|
|||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material.icons.outlined.BugReport
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
|
|
@ -48,6 +46,9 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItemText
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
|
||||
|
|
@ -58,6 +59,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -137,12 +139,20 @@ private fun DefaultRoomListTopBar(
|
|||
IconButton(
|
||||
onClick = onSearchClicked,
|
||||
) {
|
||||
Icon(Icons.Default.Search, contentDescription = stringResource(CommonStrings.action_search))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = stringResource(CommonStrings.action_search),
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { showMenu = !showMenu }
|
||||
) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
|
|
@ -153,16 +163,28 @@ private fun DefaultRoomListTopBar(
|
|||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.InviteFriends)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.action_invite)) },
|
||||
leadingIcon = { Icon(Icons.Default.Share, contentDescription = null) }
|
||||
text = { DropdownMenuItemText(stringResource(id = CommonStrings.action_invite)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.Share,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.ReportBug)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.common_report_a_bug)) },
|
||||
leadingIcon = { Icon(Icons.Default.BugReport, contentDescription = null) }
|
||||
text = { DropdownMenuItemText(stringResource(id = CommonStrings.common_report_a_bug)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.BugReport,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||