Merge branch 'develop' into feature/fga/room_list_api
This commit is contained in:
commit
e3744636b6
935 changed files with 6059 additions and 3293 deletions
6
.github/renovate.json
vendored
6
.github/renovate.json
vendored
|
|
@ -23,6 +23,12 @@
|
||||||
"org.jetbrains.kotlinx.kover"
|
"org.jetbrains.kotlinx.kover"
|
||||||
],
|
],
|
||||||
"enabled" : false
|
"enabled" : false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackagePatterns" : [
|
||||||
|
"^org.maplibre"
|
||||||
|
],
|
||||||
|
"versioning" : "semver"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -47,6 +47,8 @@ jobs:
|
||||||
app/build/outputs/apk/debug/*.apk
|
app/build/outputs/apk/debug/*.apk
|
||||||
- uses: rnkdsh/action-upload-diawi@v1.5.0
|
- uses: rnkdsh/action-upload-diawi@v1.5.0
|
||||||
id: diawi
|
id: diawi
|
||||||
|
# Do not fail the whole build if Diawi upload fails
|
||||||
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
token: ${{ secrets.DIAWI_TOKEN }}
|
token: ${{ secrets.DIAWI_TOKEN }}
|
||||||
if: ${{ github.event_name == 'pull_request' && env.token != '' }}
|
if: ${{ github.event_name == 'pull_request' && env.token != '' }}
|
||||||
|
|
|
||||||
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
|
|
@ -42,7 +42,7 @@ jobs:
|
||||||
name: elementx-debug
|
name: elementx-debug
|
||||||
path: |
|
path: |
|
||||||
app/build/outputs/apk/debug/*.apk
|
app/build/outputs/apk/debug/*.apk
|
||||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.3.3
|
- uses: mobile-dev-inc/action-maestro-cloud@v1.4.1
|
||||||
with:
|
with:
|
||||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||||
app-file: app/build/outputs/apk/debug/app-universal-debug.apk
|
app-file: app/build/outputs/apk/debug/app-universal-debug.apk
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<!--- TOC -->
|
<!--- TOC -->
|
||||||
|
|
||||||
* [Contributing code to Matrix](#contributing-code-to-matrix)
|
* [Contributing code to Matrix](#contributing-code-to-matrix)
|
||||||
|
* [Developer onboarding](#developer-onboarding)
|
||||||
* [Android Studio settings](#android-studio-settings)
|
* [Android Studio settings](#android-studio-settings)
|
||||||
* [Compilation](#compilation)
|
* [Compilation](#compilation)
|
||||||
* [Strings](#strings)
|
* [Strings](#strings)
|
||||||
|
|
@ -32,6 +33,10 @@ Element X Android support can be found in this room: [.
|
||||||
|
|
||||||
## Android Studio settings
|
## Android Studio settings
|
||||||
|
|
||||||
Please set the "hard wrap" setting of Android Studio to 160 chars, this is the setting we use internally to format the source code (Menu `Settings/Editor/Code Style` then `Hard wrap at`).
|
Please set the "hard wrap" setting of Android Studio to 160 chars, this is the setting we use internally to format the source code (Menu `Settings/Editor/Code Style` then `Hard wrap at`).
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import com.bumble.appyx.core.integration.NodeHost
|
import com.bumble.appyx.core.integration.NodeHost
|
||||||
|
|
@ -33,9 +34,10 @@ import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
|
||||||
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.utils.LocalSnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.LocalSnackbarDispatcher
|
||||||
import io.element.android.x.di.AppBindings
|
import io.element.android.x.di.AppBindings
|
||||||
|
import io.element.android.x.intent.SafeUriHandler
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MainActivity")
|
private val loggerTag = LoggerTag("MainActivity")
|
||||||
|
|
@ -63,6 +65,7 @@ class MainActivity : NodeComponentActivity() {
|
||||||
ElementTheme {
|
ElementTheme {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
|
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
|
||||||
|
LocalUriHandler provides SafeUriHandler(this),
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -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.x.intent
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.compose.ui.platform.UriHandler
|
||||||
|
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||||
|
|
||||||
|
class SafeUriHandler(private val activity: Activity) : UriHandler {
|
||||||
|
override fun openUri(uri: String) {
|
||||||
|
activity.openUrlInExternalApp(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,5 +22,5 @@
|
||||||
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.ElementX" parent="Theme.ElementAndroid" />
|
<style name="Theme.ElementX" parent="Theme.Material3.Dark" />
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,5 @@
|
||||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||||
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="Theme.ElementX" parent="Theme.ElementAndroid" />
|
<style name="Theme.ElementX" parent="Theme.Material3.Light" />
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||||
import io.element.android.libraries.ui.strings.R
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
|
|
@ -48,13 +48,13 @@ class LoggedInEventProcessor @Inject constructor(
|
||||||
fun observeEvents(coroutineScope: CoroutineScope) {
|
fun observeEvents(coroutineScope: CoroutineScope) {
|
||||||
observingJob = coroutineScope.launch {
|
observingJob = coroutineScope.launch {
|
||||||
displayLeftRoomMessage.onEach {
|
displayLeftRoomMessage.onEach {
|
||||||
displayMessage(R.string.common_current_user_left_room)
|
displayMessage(CommonStrings.common_current_user_left_room)
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
|
|
||||||
displayVerificationSuccessfulMessage
|
displayVerificationSuccessfulMessage
|
||||||
.drop(1)
|
.drop(1)
|
||||||
.onEach {
|
.onEach {
|
||||||
displayMessage(R.string.common_verification_complete)
|
displayMessage(CommonStrings.common_verification_complete)
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
override fun onRoomSettingsClicked(roomId: RoomId) {
|
override fun onRoomSettingsClicked(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomFlowNode.NavTarget.RoomDetails))
|
backstack.push(NavTarget.Room(roomId, initialElement = RoomFlowNode.NavTarget.RoomDetails))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onReportBugClicked() {
|
||||||
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
roomListEntryPoint
|
roomListEntryPoint
|
||||||
.nodeBuilder(this, buildContext)
|
.nodeBuilder(this, buildContext)
|
||||||
|
|
|
||||||
1
changelog.d/517.bugfix
Normal file
1
changelog.d/517.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Crash when clicking on a link with associated text.
|
||||||
|
|
@ -27,12 +27,12 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
import io.element.android.libraries.designsystem.LinkColor
|
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.theme.LinkColor
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnalyticsPreferencesView(
|
fun AnalyticsPreferencesView(
|
||||||
|
|
@ -43,11 +43,11 @@ fun AnalyticsPreferencesView(
|
||||||
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
|
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
PreferenceCategory(title = stringResource(id = StringR.string.screen_analytics_settings_share_data)) {
|
PreferenceCategory(title = stringResource(id = CommonStrings.screen_analytics_settings_share_data)) {
|
||||||
val firstPart = stringResource(id = StringR.string.screen_analytics_settings_help_us_improve, state.applicationName)
|
val firstPart = stringResource(id = CommonStrings.screen_analytics_settings_help_us_improve, state.applicationName)
|
||||||
val secondPart = buildAnnotatedStringWithColoredPart(
|
val secondPart = buildAnnotatedStringWithColoredPart(
|
||||||
StringR.string.screen_analytics_settings_read_terms,
|
CommonStrings.screen_analytics_settings_read_terms,
|
||||||
StringR.string.screen_analytics_settings_read_terms_content_link
|
CommonStrings.screen_analytics_settings_read_terms_content_link
|
||||||
)
|
)
|
||||||
val title = "$firstPart\n\n$secondPart"
|
val title = "$firstPart\n\n$secondPart"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ dependencies {
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.elementresources)
|
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
api(projects.features.analytics.api)
|
api(projects.features.analytics.api)
|
||||||
api(projects.services.analytics.api)
|
api(projects.services.analytics.api)
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,13 @@ import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
||||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnalyticsOptInView(
|
fun AnalyticsOptInView(
|
||||||
|
|
@ -162,7 +162,7 @@ private fun AnalyticsOptInContentRow(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(
|
.background(
|
||||||
color = LocalColors.current.quinary,
|
color = ElementTheme.legacyColors.quinary,
|
||||||
shape = bgShape,
|
shape = bgShape,
|
||||||
)
|
)
|
||||||
.padding(vertical = 12.dp, horizontal = 20.dp),
|
.padding(vertical = 12.dp, horizontal = 20.dp),
|
||||||
|
|
@ -174,8 +174,7 @@ private fun AnalyticsOptInContentRow(
|
||||||
.padding(2.dp),
|
.padding(2.dp),
|
||||||
imageVector = Icons.Rounded.Check,
|
imageVector = Icons.Rounded.Check,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
// TODO Compound, this color is not yet in the theme
|
tint = ElementTheme.colors.iconSuccessPrimary,
|
||||||
tint = Color(0xFF007A61)
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
|
|
@ -199,13 +198,13 @@ private fun AnalyticsOptInFooter(
|
||||||
onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) },
|
onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = StringR.string.action_ok))
|
Text(text = stringResource(id = CommonStrings.action_ok))
|
||||||
}
|
}
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) },
|
onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = StringR.string.action_not_now))
|
Text(text = stringResource(id = CommonStrings.action_not_now))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,9 @@ dependencies {
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.elementresources)
|
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
|
implementation(projects.libraries.deeplink)
|
||||||
implementation(projects.libraries.mediapickers.api)
|
implementation(projects.libraries.mediapickers.api)
|
||||||
implementation(projects.libraries.mediaupload.api)
|
implementation(projects.libraries.mediaupload.api)
|
||||||
implementation(projects.libraries.usersearch.impl)
|
implementation(projects.libraries.usersearch.impl)
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import io.element.android.libraries.designsystem.theme.components.CenterAlignedT
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -107,7 +107,7 @@ fun AddPeopleViewTopBar(
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
onClick = onNextPressed,
|
onClick = onNextPressed,
|
||||||
) {
|
) {
|
||||||
val textActionResId = if (hasSelectedUsers) StringR.string.action_next else StringR.string.action_skip
|
val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = textActionResId),
|
text = stringResource(id = textActionResId),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||||
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
|
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
|
||||||
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
|
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
|
@ -40,7 +40,7 @@ fun SearchMultipleUsersResultItem(
|
||||||
CheckableUnresolvedUserRow(
|
CheckableUnresolvedUserRow(
|
||||||
checked = isUserSelected,
|
checked = isUserSelected,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)),
|
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||||
id = searchResult.matrixUser.userId.value,
|
id = searchResult.matrixUser.userId.value,
|
||||||
onCheckedChange = onCheckedChange,
|
onCheckedChange = onCheckedChange,
|
||||||
)
|
)
|
||||||
|
|
@ -49,7 +49,7 @@ fun SearchMultipleUsersResultItem(
|
||||||
checked = isUserSelected,
|
checked = isUserSelected,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
matrixUser = searchResult.matrixUser,
|
matrixUser = searchResult.matrixUser,
|
||||||
avatarSize = AvatarSize.Custom(36.dp),
|
avatarSize = AvatarSize.UserListItem,
|
||||||
onCheckedChange = onCheckedChange,
|
onCheckedChange = onCheckedChange,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -63,8 +63,11 @@ internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { Con
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
Column {
|
Column {
|
||||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
|
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
|
||||||
|
Divider()
|
||||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true)
|
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true)
|
||||||
|
Divider()
|
||||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false)
|
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false)
|
||||||
|
Divider()
|
||||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = true)
|
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow
|
import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
|
@ -39,26 +39,27 @@ fun SearchSingleUserResultItem(
|
||||||
if (searchResult.isUnresolved) {
|
if (searchResult.isUnresolved) {
|
||||||
UnresolvedUserRow(
|
UnresolvedUserRow(
|
||||||
modifier = modifier.clickable(onClick = onClick),
|
modifier = modifier.clickable(onClick = onClick),
|
||||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)),
|
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||||
id = searchResult.matrixUser.userId.value,
|
id = searchResult.matrixUser.userId.value,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MatrixUserRow(
|
MatrixUserRow(
|
||||||
modifier = modifier.clickable(onClick = onClick),
|
modifier = modifier.clickable(onClick = onClick),
|
||||||
matrixUser = searchResult.matrixUser,
|
matrixUser = searchResult.matrixUser,
|
||||||
avatarSize = AvatarSize.Custom(36.dp),
|
avatarSize = AvatarSize.UserListItem,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview{ ContentToPreview() }
|
internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview { ContentToPreview() }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
Column {
|
Column {
|
||||||
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
|
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
|
||||||
|
Divider()
|
||||||
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true))
|
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||||
import io.element.android.libraries.ui.strings.R
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ fun SearchUserBar(
|
||||||
isMultiSelectionEnabled: Boolean,
|
isMultiSelectionEnabled: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
showBackButton: Boolean = true,
|
showBackButton: Boolean = true,
|
||||||
placeHolderTitle: String = stringResource(R.string.common_search_for_someone),
|
placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
|
||||||
onActiveChanged: (Boolean) -> Unit = {},
|
onActiveChanged: (Boolean) -> Unit = {},
|
||||||
onTextChanged: (String) -> Unit = {},
|
onTextChanged: (String) -> Unit = {},
|
||||||
onUserSelected: (MatrixUser) -> Unit = {},
|
onUserSelected: (MatrixUser) -> Unit = {},
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.execute
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -131,7 +131,7 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
dataStore.clearCachedData()
|
dataStore.clearCachedData()
|
||||||
analyticsService.capture(CreatedRoom(isDM = false))
|
analyticsService.capture(CreatedRoom(isDM = false))
|
||||||
}
|
}
|
||||||
}.execute(createRoomAction)
|
}.runCatchingUpdatingState(createRoomAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun uploadAvatar(avatarUri: Uri): String {
|
private suspend fun uploadAvatar(avatarUri: Uri): String {
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||||
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
|
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -84,7 +84,7 @@ fun ConfigureRoomView(
|
||||||
|
|
||||||
if (state.createRoomAction is Async.Success) {
|
if (state.createRoomAction is Async.Success) {
|
||||||
LaunchedEffect(state.createRoomAction) {
|
LaunchedEffect(state.createRoomAction) {
|
||||||
onRoomCreated(state.createRoomAction.state)
|
onRoomCreated(state.createRoomAction.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ fun ConfigureRoomView(
|
||||||
|
|
||||||
when (state.createRoomAction) {
|
when (state.createRoomAction) {
|
||||||
is Async.Loading -> {
|
is Async.Loading -> {
|
||||||
ProgressDialog(text = stringResource(StringR.string.common_creating_room))
|
ProgressDialog(text = stringResource(CommonStrings.common_creating_room))
|
||||||
}
|
}
|
||||||
|
|
||||||
is Async.Failure -> {
|
is Async.Failure -> {
|
||||||
|
|
@ -198,7 +198,7 @@ fun ConfigureRoomToolbar(
|
||||||
onClick = onNextPressed,
|
onClick = onNextPressed,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(StringR.string.action_create),
|
text = stringResource(CommonStrings.action_create),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -227,7 +227,7 @@ fun RoomNameWithAvatar(
|
||||||
LabelledTextField(
|
LabelledTextField(
|
||||||
label = stringResource(R.string.screen_create_room_room_name_label),
|
label = stringResource(R.string.screen_create_room_room_name_label),
|
||||||
value = roomName,
|
value = roomName,
|
||||||
placeholder = stringResource(StringR.string.common_room_name_placeholder),
|
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
onValueChange = onRoomNameChanged,
|
onValueChange = onRoomNameChanged,
|
||||||
)
|
)
|
||||||
|
|
@ -244,7 +244,7 @@ fun RoomTopic(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
label = stringResource(R.string.screen_create_room_topic_label),
|
label = stringResource(R.string.screen_create_room_topic_label),
|
||||||
value = topic,
|
value = topic,
|
||||||
placeholder = stringResource(StringR.string.common_topic_placeholder),
|
placeholder = stringResource(CommonStrings.common_topic_placeholder),
|
||||||
onValueChange = onTopicChanged,
|
onValueChange = onTopicChanged,
|
||||||
maxLines = 3,
|
maxLines = 3,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.root
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
import android.content.Context
|
import android.app.Activity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
@ -29,24 +29,18 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
|
||||||
import io.element.android.libraries.ui.strings.R
|
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class CreateRoomRootNode @AssistedInject constructor(
|
class CreateRoomRootNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: CreateRoomRootPresenter,
|
private val presenter: CreateRoomRootPresenter,
|
||||||
private val matrixClient: MatrixClient,
|
|
||||||
private val buildMeta: BuildMeta,
|
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
|
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
|
|
@ -73,31 +67,18 @@ class CreateRoomRootNode @AssistedInject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
val context = LocalContext.current
|
val activity = LocalContext.current as Activity
|
||||||
CreateRoomRootView(
|
CreateRoomRootView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClosePressed = this::navigateUp,
|
onClosePressed = this::navigateUp,
|
||||||
onNewRoomClicked = callback::onCreateNewRoom,
|
onNewRoomClicked = callback::onCreateNewRoom,
|
||||||
onOpenDM = callback::onStartChatSuccess,
|
onOpenDM = callback::onStartChatSuccess,
|
||||||
onInviteFriendsClicked = { invitePeople(context) },
|
onInviteFriendsClicked = { invitePeople(activity) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun invitePeople(context: Context) {
|
private fun invitePeople(activity: Activity) {
|
||||||
val permalinkResult = PermalinkBuilder.permalinkForUser(matrixClient.sessionId)
|
inviteFriendsUseCase.execute(activity)
|
||||||
permalinkResult.onSuccess { permalink ->
|
|
||||||
val appName = buildMeta.applicationName
|
|
||||||
startSharePlainTextIntent(
|
|
||||||
context = context,
|
|
||||||
activityResultLauncher = null,
|
|
||||||
chooserTitle = context.getString(R.string.action_invite_friends),
|
|
||||||
text = context.getString(R.string.invite_friends_text, appName, permalink),
|
|
||||||
extraTitle = context.getString(R.string.invite_friends_rich_title, appName),
|
|
||||||
noActivityFoundMessage = context.getString(io.element.android.libraries.androidutils.R.string.error_no_compatible_app_found)
|
|
||||||
)
|
|
||||||
}.onFailure {
|
|
||||||
Timber.e(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import io.element.android.features.createroom.impl.userlist.UserListPresenter
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
|
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.execute
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -95,6 +95,6 @@ class CreateRoomRootPresenter @Inject constructor(
|
||||||
suspend {
|
suspend {
|
||||||
matrixClient.createDM(user.userId).getOrThrow()
|
matrixClient.createDM(user.userId).getOrThrow()
|
||||||
.also { analyticsService.capture(CreatedRoom(isDM = true)) }
|
.also { analyticsService.capture(CreatedRoom(isDM = true)) }
|
||||||
}.execute(startDmAction)
|
}.runCatchingUpdatingState(startDmAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,8 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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
|
import io.element.android.libraries.designsystem.R as DrawableR
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -69,7 +69,7 @@ fun CreateRoomRootView(
|
||||||
) {
|
) {
|
||||||
if (state.startDmAction is Async.Success) {
|
if (state.startDmAction is Async.Success) {
|
||||||
LaunchedEffect(state.startDmAction) {
|
LaunchedEffect(state.startDmAction) {
|
||||||
onOpenDM(state.startDmAction.state)
|
onOpenDM(state.startDmAction.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +107,7 @@ fun CreateRoomRootView(
|
||||||
|
|
||||||
when (state.startDmAction) {
|
when (state.startDmAction) {
|
||||||
is Async.Loading -> {
|
is Async.Loading -> {
|
||||||
ProgressDialog(text = stringResource(id = StringR.string.common_starting_chat))
|
ProgressDialog(text = stringResource(id = CommonStrings.common_starting_chat))
|
||||||
}
|
}
|
||||||
|
|
||||||
is Async.Failure -> {
|
is Async.Failure -> {
|
||||||
|
|
@ -137,7 +137,7 @@ fun CreateRoomRootViewTopBar(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = StringR.string.action_start_chat),
|
text = stringResource(id = CommonStrings.action_start_chat),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
|
|
@ -146,7 +146,7 @@ fun CreateRoomRootViewTopBar(
|
||||||
IconButton(onClick = onClosePressed) {
|
IconButton(onClick = onClosePressed) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = stringResource(id = StringR.string.action_close),
|
contentDescription = stringResource(id = CommonStrings.action_close),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +169,7 @@ fun CreateRoomActionButtonsList(
|
||||||
)
|
)
|
||||||
CreateRoomActionButton(
|
CreateRoomActionButton(
|
||||||
iconRes = DrawableR.drawable.ic_share,
|
iconRes = DrawableR.drawable.ic_share,
|
||||||
text = stringResource(id = StringR.string.action_invite_friends_to_app, state.applicationName),
|
text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
|
||||||
onClick = onInvitePeopleClicked,
|
onClick = onInvitePeopleClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ dependencies {
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
implementation(projects.services.analytics.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
@ -50,6 +51,7 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.invitelist.test)
|
testImplementation(projects.features.invitelist.test)
|
||||||
|
testImplementation(projects.features.analytics.test)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,16 +25,20 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.features.invitelist.api.SeenInvitesStore
|
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.execute
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomSummary
|
import io.element.android.libraries.matrix.api.room.RoomSummary
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -44,6 +48,7 @@ import javax.inject.Inject
|
||||||
class InviteListPresenter @Inject constructor(
|
class InviteListPresenter @Inject constructor(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val store: SeenInvitesStore,
|
private val store: SeenInvitesStore,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
) : Presenter<InviteListState> {
|
) : Presenter<InviteListState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -133,9 +138,10 @@ class InviteListPresenter @Inject constructor(
|
||||||
suspend {
|
suspend {
|
||||||
client.getRoom(roomId)?.use {
|
client.getRoom(roomId)?.use {
|
||||||
it.acceptInvitation().getOrThrow()
|
it.acceptInvitation().getOrThrow()
|
||||||
|
analyticsService.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
|
||||||
}
|
}
|
||||||
roomId
|
roomId
|
||||||
}.execute(acceptedAction)
|
}.runCatchingUpdatingState(acceptedAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch {
|
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch {
|
||||||
|
|
@ -143,7 +149,7 @@ class InviteListPresenter @Inject constructor(
|
||||||
client.getRoom(roomId)?.use {
|
client.getRoom(roomId)?.use {
|
||||||
it.rejectInvitation().getOrThrow()
|
it.rejectInvitation().getOrThrow()
|
||||||
} ?: Unit
|
} ?: Unit
|
||||||
}.execute(declinedAction)
|
}.runCatchingUpdatingState(declinedAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run {
|
private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run {
|
||||||
|
|
@ -153,12 +159,14 @@ class InviteListPresenter @Inject constructor(
|
||||||
id = i.userId.value,
|
id = i.userId.value,
|
||||||
name = i.displayName,
|
name = i.displayName,
|
||||||
url = i.avatarUrl,
|
url = i.avatarUrl,
|
||||||
|
size = AvatarSize.RoomInviteItem,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
AvatarData(
|
AvatarData(
|
||||||
id = roomId.value,
|
id = roomId.value,
|
||||||
name = name,
|
name = name,
|
||||||
url = avatarURLString
|
url = avatarURLString,
|
||||||
|
size = AvatarSize.RoomInviteItem,
|
||||||
)
|
)
|
||||||
|
|
||||||
val alias = if (isDirect)
|
val alias = if (isDirect)
|
||||||
|
|
@ -181,6 +189,7 @@ class InviteListPresenter @Inject constructor(
|
||||||
id = userId.value,
|
id = userId.value,
|
||||||
name = displayName,
|
name = displayName,
|
||||||
url = avatarUrl,
|
url = avatarUrl,
|
||||||
|
size = AvatarSize.InviteSender,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InviteListView(
|
fun InviteListView(
|
||||||
|
|
@ -58,7 +58,7 @@ fun InviteListView(
|
||||||
) {
|
) {
|
||||||
if (state.acceptedAction is Async.Success) {
|
if (state.acceptedAction is Async.Success) {
|
||||||
LaunchedEffect(state.acceptedAction) {
|
LaunchedEffect(state.acceptedAction) {
|
||||||
onInviteAccepted(state.acceptedAction.state)
|
onInviteAccepted(state.acceptedAction.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,8 +82,8 @@ fun InviteListView(
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
content = stringResource(contentResource, state.declineConfirmationDialog.name),
|
content = stringResource(contentResource, state.declineConfirmationDialog.name),
|
||||||
title = stringResource(titleResource),
|
title = stringResource(titleResource),
|
||||||
submitText = stringResource(StringR.string.action_decline),
|
submitText = stringResource(CommonStrings.action_decline),
|
||||||
cancelText = stringResource(StringR.string.action_cancel),
|
cancelText = stringResource(CommonStrings.action_cancel),
|
||||||
emphasizeSubmitButton = true,
|
emphasizeSubmitButton = true,
|
||||||
onSubmitClicked = { state.eventSink(InviteListEvents.ConfirmDeclineInvite) },
|
onSubmitClicked = { state.eventSink(InviteListEvents.ConfirmDeclineInvite) },
|
||||||
onDismiss = { state.eventSink(InviteListEvents.CancelDeclineInvite) }
|
onDismiss = { state.eventSink(InviteListEvents.CancelDeclineInvite) }
|
||||||
|
|
@ -92,18 +92,18 @@ fun InviteListView(
|
||||||
|
|
||||||
if (state.acceptedAction is Async.Failure) {
|
if (state.acceptedAction is Async.Failure) {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
content = stringResource(StringR.string.error_unknown),
|
content = stringResource(CommonStrings.error_unknown),
|
||||||
title = stringResource(StringR.string.common_error),
|
title = stringResource(CommonStrings.common_error),
|
||||||
submitText = stringResource(StringR.string.action_ok),
|
submitText = stringResource(CommonStrings.action_ok),
|
||||||
onDismiss = { state.eventSink(InviteListEvents.DismissAcceptError) }
|
onDismiss = { state.eventSink(InviteListEvents.DismissAcceptError) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.declinedAction is Async.Failure) {
|
if (state.declinedAction is Async.Failure) {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
content = stringResource(StringR.string.error_unknown),
|
content = stringResource(CommonStrings.error_unknown),
|
||||||
title = stringResource(StringR.string.common_error),
|
title = stringResource(CommonStrings.common_error),
|
||||||
submitText = stringResource(StringR.string.action_ok),
|
submitText = stringResource(CommonStrings.action_ok),
|
||||||
onDismiss = { state.eventSink(InviteListEvents.DismissDeclineError) }
|
onDismiss = { state.eventSink(InviteListEvents.DismissDeclineError) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +124,7 @@ fun InviteListContent(
|
||||||
BackButton(onClick = onBackClicked)
|
BackButton(onClick = onBackClicked)
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
Text(text = stringResource(StringR.string.action_invites_list))
|
Text(text = stringResource(CommonStrings.action_invites_list))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.invitelist.impl.components
|
package io.element.android.features.invitelist.impl.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -28,14 +27,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -52,6 +48,7 @@ import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummaryProvider
|
import io.element.android.features.invitelist.impl.model.InviteListInviteSummaryProvider
|
||||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
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.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
|
@ -61,7 +58,7 @@ 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.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.noFontPadding
|
import io.element.android.libraries.designsystem.theme.noFontPadding
|
||||||
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
|
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
private val minHeight = 72.dp
|
private val minHeight = 72.dp
|
||||||
|
|
||||||
|
|
@ -74,8 +71,8 @@ internal fun InviteSummaryRow(
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = minHeight)
|
.heightIn(min = minHeight)
|
||||||
) {
|
) {
|
||||||
DefaultInviteSummaryRow(
|
DefaultInviteSummaryRow(
|
||||||
invite = invite,
|
invite = invite,
|
||||||
|
|
@ -93,20 +90,20 @@ internal fun DefaultInviteSummaryRow(
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
verticalAlignment = Alignment.Top
|
verticalAlignment = Alignment.Top
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
invite.roomAvatarData.copy(size = AvatarSize.Custom(52.dp)),
|
invite.roomAvatarData,
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 16.dp, end = 4.dp)
|
.padding(start = 16.dp, end = 4.dp)
|
||||||
.alignByBaseline()
|
.alignByBaseline()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
val bonusPadding = if (invite.isNew) 12.dp else 0.dp
|
val bonusPadding = if (invite.isNew) 12.dp else 0.dp
|
||||||
|
|
||||||
|
|
@ -143,31 +140,29 @@ internal fun DefaultInviteSummaryRow(
|
||||||
// CTAs
|
// CTAs
|
||||||
Row(Modifier.padding(top = 12.dp)) {
|
Row(Modifier.padding(top = 12.dp)) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
content = { Text(stringResource(StringR.string.action_decline), style = ElementTextStyles.Button) },
|
content = { Text(stringResource(CommonStrings.action_decline), style = ElementTextStyles.Button) },
|
||||||
onClick = onDeclineClicked,
|
onClick = onDeclineClicked,
|
||||||
modifier = Modifier.weight(1f).heightIn(max = 36.dp),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.heightIn(max = 36.dp),
|
||||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
|
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
content = { Text(stringResource(StringR.string.action_accept), style = ElementTextStyles.Button) },
|
content = { Text(stringResource(CommonStrings.action_accept), style = ElementTextStyles.Button) },
|
||||||
onClick = onAcceptClicked,
|
onClick = onAcceptClicked,
|
||||||
modifier = Modifier.weight(1f).heightIn(max = 36.dp),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.heightIn(max = 36.dp),
|
||||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
|
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val unreadIndicatorColor = if (invite.isNew) MaterialTheme.roomListUnreadIndicator() else Color.Transparent
|
val unreadIndicatorColor = if (invite.isNew) MaterialTheme.roomListUnreadIndicator() else Color.Transparent
|
||||||
|
UnreadIndicatorAtom(color = unreadIndicatorColor)
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(12.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(unreadIndicatorColor)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,7 +173,7 @@ private fun SenderRow(sender: InviteSender) {
|
||||||
modifier = Modifier.padding(top = 6.dp),
|
modifier = Modifier.padding(top = 6.dp),
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
|
avatarData = sender.avatarData,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.screen_invites_invited_you, sender.displayName, sender.userId.value).let { text ->
|
text = stringResource(R.string.screen_invites_invited_you, sender.displayName, sender.userId.value).let { text ->
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.invitelist.impl.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
|
|
@ -26,14 +27,14 @@ data class InviteListInviteSummary(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val roomName: String = "",
|
val roomName: String = "",
|
||||||
val roomAlias: String? = null,
|
val roomAlias: String? = null,
|
||||||
val roomAvatarData: AvatarData = AvatarData(roomId.value, roomName),
|
val roomAvatarData: AvatarData = AvatarData(roomId.value, roomName, size = AvatarSize.RoomInviteItem),
|
||||||
val sender: InviteSender? = null,
|
val sender: InviteSender? = null,
|
||||||
val isDirect: Boolean = false,
|
val isDirect: Boolean = false,
|
||||||
val isNew: Boolean = false,
|
val isNew: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class InviteSender(
|
data class InviteSender constructor(
|
||||||
val userId: UserId,
|
val userId: UserId,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val avatarData: AvatarData = AvatarData(userId.value, displayName),
|
val avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,11 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
|
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
|
|
@ -50,6 +52,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
),
|
),
|
||||||
FakeSeenInvitesStore(),
|
FakeSeenInvitesStore(),
|
||||||
|
FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -74,6 +77,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
),
|
),
|
||||||
FakeSeenInvitesStore(),
|
FakeSeenInvitesStore(),
|
||||||
|
FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -88,6 +92,7 @@ class InviteListPresenterTests {
|
||||||
id = A_USER_ID.value,
|
id = A_USER_ID.value,
|
||||||
name = A_USER_NAME,
|
name = A_USER_NAME,
|
||||||
url = AN_AVATAR_URL,
|
url = AN_AVATAR_URL,
|
||||||
|
size = AvatarSize.RoomInviteItem,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Truth.assertThat(withInviteState.inviteList[0].sender).isNull()
|
Truth.assertThat(withInviteState.inviteList[0].sender).isNull()
|
||||||
|
|
@ -102,6 +107,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
),
|
),
|
||||||
FakeSeenInvitesStore(),
|
FakeSeenInvitesStore(),
|
||||||
|
FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -115,6 +121,7 @@ class InviteListPresenterTests {
|
||||||
id = A_USER_ID.value,
|
id = A_USER_ID.value,
|
||||||
name = A_USER_NAME,
|
name = A_USER_NAME,
|
||||||
url = AN_AVATAR_URL,
|
url = AN_AVATAR_URL,
|
||||||
|
size = AvatarSize.InviteSender,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -128,6 +135,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
),
|
),
|
||||||
FakeSeenInvitesStore(),
|
FakeSeenInvitesStore(),
|
||||||
|
FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -152,6 +160,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
),
|
),
|
||||||
FakeSeenInvitesStore(),
|
FakeSeenInvitesStore(),
|
||||||
|
FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -176,6 +185,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
),
|
),
|
||||||
FakeSeenInvitesStore(),
|
FakeSeenInvitesStore(),
|
||||||
|
FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -199,7 +209,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -225,7 +235,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||||
val ex = Throwable("Ruh roh!")
|
val ex = Throwable("Ruh roh!")
|
||||||
room.givenRejectInviteResult(Result.failure(ex))
|
room.givenRejectInviteResult(Result.failure(ex))
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
@ -256,7 +266,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||||
val ex = Throwable("Ruh roh!")
|
val ex = Throwable("Ruh roh!")
|
||||||
room.givenRejectInviteResult(Result.failure(ex))
|
room.givenRejectInviteResult(Result.failure(ex))
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
@ -288,7 +298,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -311,7 +321,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||||
val ex = Throwable("Ruh roh!")
|
val ex = Throwable("Ruh roh!")
|
||||||
room.givenAcceptInviteResult(Result.failure(ex))
|
room.givenAcceptInviteResult(Result.failure(ex))
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
@ -336,7 +346,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||||
val ex = Throwable("Ruh roh!")
|
val ex = Throwable("Ruh roh!")
|
||||||
room.givenAcceptInviteResult(Result.failure(ex))
|
room.givenAcceptInviteResult(Result.failure(ex))
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
@ -365,6 +375,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
),
|
),
|
||||||
store,
|
store,
|
||||||
|
FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -401,6 +412,7 @@ class InviteListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
),
|
),
|
||||||
store,
|
store,
|
||||||
|
FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.ui.strings.R
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LeaveRoomView(
|
fun LeaveRoomView(
|
||||||
|
|
@ -50,19 +49,19 @@ private fun LeaveRoomConfirmationDialog(
|
||||||
when (state.confirmation) {
|
when (state.confirmation) {
|
||||||
is LeaveRoomState.Confirmation.Hidden -> {}
|
is LeaveRoomState.Confirmation.Hidden -> {}
|
||||||
is LeaveRoomState.Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
|
is LeaveRoomState.Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
|
||||||
text = StringR.string.leave_room_alert_private_subtitle,
|
text = CommonStrings.leave_room_alert_private_subtitle,
|
||||||
roomId = state.confirmation.roomId,
|
roomId = state.confirmation.roomId,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
is LeaveRoomState.Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog(
|
is LeaveRoomState.Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog(
|
||||||
text = StringR.string.leave_room_alert_empty_subtitle,
|
text = CommonStrings.leave_room_alert_empty_subtitle,
|
||||||
roomId = state.confirmation.roomId,
|
roomId = state.confirmation.roomId,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
is LeaveRoomState.Confirmation.Generic -> LeaveRoomConfirmationDialog(
|
is LeaveRoomState.Confirmation.Generic -> LeaveRoomConfirmationDialog(
|
||||||
text = StringR.string.leave_room_alert_subtitle,
|
text = CommonStrings.leave_room_alert_subtitle,
|
||||||
roomId = state.confirmation.roomId,
|
roomId = state.confirmation.roomId,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
)
|
)
|
||||||
|
|
@ -77,7 +76,7 @@ private fun LeaveRoomConfirmationDialog(
|
||||||
) {
|
) {
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
content = stringResource(text),
|
content = stringResource(text),
|
||||||
submitText = stringResource(R.string.action_leave),
|
submitText = stringResource(CommonStrings.action_leave),
|
||||||
onSubmitClicked = { eventSink(LeaveRoomEvent.LeaveRoom(roomId)) },
|
onSubmitClicked = { eventSink(LeaveRoomEvent.LeaveRoom(roomId)) },
|
||||||
onDismiss = { eventSink(LeaveRoomEvent.HideConfirmation) },
|
onDismiss = { eventSink(LeaveRoomEvent.HideConfirmation) },
|
||||||
)
|
)
|
||||||
|
|
@ -90,7 +89,7 @@ private fun LeaveRoomProgressDialog(
|
||||||
when (state.progress) {
|
when (state.progress) {
|
||||||
is LeaveRoomState.Progress.Hidden -> {}
|
is LeaveRoomState.Progress.Hidden -> {}
|
||||||
is LeaveRoomState.Progress.Shown -> ProgressDialog(
|
is LeaveRoomState.Progress.Shown -> ProgressDialog(
|
||||||
text = stringResource(StringR.string.common_leaving_room),
|
text = stringResource(CommonStrings.common_leaving_room),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +101,7 @@ private fun LeaveRoomErrorDialog(
|
||||||
when (state.error) {
|
when (state.error) {
|
||||||
is LeaveRoomState.Error.Hidden -> {}
|
is LeaveRoomState.Error.Hidden -> {}
|
||||||
is LeaveRoomState.Error.Shown -> ErrorDialog(
|
is LeaveRoomState.Error.Shown -> ErrorDialog(
|
||||||
content = stringResource(StringR.string.error_unknown),
|
content = stringResource(CommonStrings.error_unknown),
|
||||||
onDismiss = { state.eventSink(LeaveRoomEvent.HideError) }
|
onDismiss = { state.eventSink(LeaveRoomEvent.HideError) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,9 @@ import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
|
||||||
import io.element.android.features.location.api.internal.buildTileServerUrl
|
import io.element.android.features.location.api.internal.buildTileServerUrl
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
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.Icon
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -67,7 +67,7 @@ import kotlin.coroutines.suspendCoroutine
|
||||||
fun MapView(
|
fun MapView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
mapState: MapState = rememberMapState(),
|
mapState: MapState = rememberMapState(),
|
||||||
darkMode: Boolean = !ElementTheme.colors.isLight,
|
darkMode: Boolean = !ElementTheme.isLightTheme,
|
||||||
onLocationClick: () -> Unit,
|
onLocationClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
// When in preview, early return a Box with the received modifier preserving layout
|
// When in preview, early return a Box with the received modifier preserving layout
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,12 @@ import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImagePainter
|
import coil.compose.AsyncImagePainter
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
||||||
import io.element.android.features.location.api.internal.buildStaticMapsApiUrl
|
import io.element.android.features.location.api.internal.buildStaticMapsApiUrl
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -52,7 +52,7 @@ fun StaticMapView(
|
||||||
zoom: Double,
|
zoom: Double,
|
||||||
contentDescription: String?,
|
contentDescription: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
darkMode: Boolean = !ElementTheme.colors.isLight,
|
darkMode: Boolean = !ElementTheme.isLightTheme,
|
||||||
) {
|
) {
|
||||||
// Using BoxWithConstraints to:
|
// Using BoxWithConstraints to:
|
||||||
// 1) Size the inner Image to the same Dp size of the outer BoxWithConstraints.
|
// 1) Size the inner Image to the same Dp size of the outer BoxWithConstraints.
|
||||||
|
|
|
||||||
|
|
@ -33,21 +33,21 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.features.location.api.R
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.features.location.api.R
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.R as StringsR
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun StaticMapPlaceholder(
|
internal fun StaticMapPlaceholder(
|
||||||
showProgress: Boolean,
|
showProgress: Boolean,
|
||||||
contentDescription: String?,
|
contentDescription: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
darkMode: Boolean = !ElementTheme.colors.isLight,
|
darkMode: Boolean = !ElementTheme.isLightTheme,
|
||||||
onLoadMapClick: () -> Unit,
|
onLoadMapClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -76,7 +76,7 @@ internal fun StaticMapPlaceholder(
|
||||||
imageVector = Icons.Default.Refresh,
|
imageVector = Icons.Default.Refresh,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
Text(text = stringResource(id = StringsR.string.action_static_map_load))
|
Text(text = stringResource(id = CommonStrings.action_static_map_load))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ dependencies {
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.network)
|
implementation(projects.libraries.network)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.elementresources)
|
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(libs.androidx.browser)
|
implementation(libs.androidx.browser)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.login.impl
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -45,9 +46,9 @@ import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
|
|
@ -155,7 +156,7 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
activity = LocalContext.current as? Activity
|
activity = LocalContext.current as? Activity
|
||||||
darkTheme = !ElementTheme.colors.isLight
|
darkTheme = !ElementTheme.isLightTheme
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
activity = null
|
activity = null
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.execute
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -71,6 +71,6 @@ class ChangeServerPresenter @Inject constructor(
|
||||||
// Valid, remember user choice
|
// Valid, remember user choice
|
||||||
accountProviderDataSource.userSelection(data)
|
accountProviderDataSource.userSelection(data)
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
}.execute(changeServerAction, errorMapping = ChangeServerError::from)
|
}.runCatchingUpdatingState(changeServerAction, errorTransform = ChangeServerError::from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SlidingSyncNotSupportedDialog(
|
internal fun SlidingSyncNotSupportedDialog(
|
||||||
|
|
@ -32,11 +32,11 @@ internal fun SlidingSyncNotSupportedDialog(
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
submitText = stringResource(StringR.string.action_learn_more),
|
submitText = stringResource(CommonStrings.action_learn_more),
|
||||||
onSubmitClicked = onLearnMoreClicked,
|
onSubmitClicked = onLearnMoreClicked,
|
||||||
onCancelClicked = onDismiss,
|
onCancelClicked = onDismiss,
|
||||||
emphasizeSubmitButton = true,
|
emphasizeSubmitButton = true,
|
||||||
title = stringResource(StringR.string.dialog_title_error),
|
title = stringResource(CommonStrings.dialog_title_error),
|
||||||
content = stringResource(R.string.screen_change_server_error_no_sliding_sync_message),
|
content = stringResource(R.string.screen_change_server_error_no_sliding_sync_message),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,16 +21,16 @@ import io.element.android.features.login.impl.R
|
||||||
import io.element.android.libraries.matrix.api.auth.AuthErrorCode
|
import io.element.android.libraries.matrix.api.auth.AuthErrorCode
|
||||||
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
||||||
import io.element.android.libraries.matrix.api.auth.errorCode
|
import io.element.android.libraries.matrix.api.auth.errorCode
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
fun loginError(
|
fun loginError(
|
||||||
throwable: Throwable
|
throwable: Throwable
|
||||||
): Int {
|
): Int {
|
||||||
val authException = throwable as? AuthenticationException ?: return StringR.string.error_unknown
|
val authException = throwable as? AuthenticationException ?: return CommonStrings.error_unknown
|
||||||
return when (authException.errorCode) {
|
return when (authException.errorCode) {
|
||||||
AuthErrorCode.FORBIDDEN -> R.string.screen_login_error_invalid_credentials
|
AuthErrorCode.FORBIDDEN -> R.string.screen_login_error_invalid_credentials
|
||||||
AuthErrorCode.USER_DEACTIVATED -> R.string.screen_login_error_deactivated_account
|
AuthErrorCode.USER_DEACTIVATED -> R.string.screen_login_error_deactivated_account
|
||||||
AuthErrorCode.UNKNOWN -> StringR.string.error_unknown
|
AuthErrorCode.UNKNOWN -> CommonStrings.error_unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.execute
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -95,6 +95,6 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
throw IllegalStateException("Unsupported login flow")
|
throw IllegalStateException("Unsupported login flow")
|
||||||
}
|
}
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
}.execute(loginFlowAction, errorMapping = ChangeServerError::from)
|
}.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ fun ConfirmAccountProviderView(
|
||||||
}
|
}
|
||||||
is Async.Loading -> Unit // The Continue button shows the loading state
|
is Async.Loading -> Unit // The Continue button shows the loading state
|
||||||
is Async.Success -> {
|
is Async.Success -> {
|
||||||
when (val loginFlowState = state.loginFlow.state) {
|
when (val loginFlowState = state.loginFlow.data) {
|
||||||
is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
|
is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
|
||||||
LoginFlow.PasswordLogin -> onLoginPasswordNeeded()
|
LoginFlow.PasswordLogin -> onLoginPasswordNeeded()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ import io.element.android.libraries.designsystem.theme.components.autofill
|
||||||
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
|
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -213,7 +213,7 @@ internal fun LoginForm(
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
loginFieldState = ""
|
loginFieldState = ""
|
||||||
}) {
|
}) {
|
||||||
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear))
|
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(CommonStrings.action_clear))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
|
|
@ -248,7 +248,7 @@ internal fun LoginForm(
|
||||||
val image =
|
val image =
|
||||||
if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
|
if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
|
||||||
val description =
|
val description =
|
||||||
if (passwordVisible) stringResource(StringR.string.a11y_hide_password) else stringResource(StringR.string.a11y_show_password)
|
if (passwordVisible) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password)
|
||||||
|
|
||||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||||
Icon(imageVector = image, description)
|
Icon(imageVector = image, description)
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
|
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=611-61435
|
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=611-61435
|
||||||
|
|
@ -143,7 +143,7 @@ fun SearchAccountProviderView(
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Close,
|
imageVector = Icons.Filled.Close,
|
||||||
contentDescription = stringResource(StringR.string.action_clear)
|
contentDescription = stringResource(CommonStrings.action_clear)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,7 +171,7 @@ fun SearchAccountProviderView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Async.Success -> {
|
is Async.Success -> {
|
||||||
items(state.userInputResult.state) { homeserverData ->
|
items(state.userInputResult.data) { homeserverData ->
|
||||||
val item = homeserverData.toAccountProvider()
|
val item = homeserverData.toAccountProvider()
|
||||||
AccountProviderView(
|
AccountProviderView(
|
||||||
item = item,
|
item = item,
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ package io.element.android.features.login.impl.error
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
class ErrorFormatterTests {
|
class ErrorFormatterTests {
|
||||||
|
|
||||||
|
|
@ -28,19 +28,19 @@ class ErrorFormatterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `loginError - invalid unknown error returns unknown error message`() {
|
fun `loginError - invalid unknown error returns unknown error message`() {
|
||||||
val error = Throwable("Some unknown error")
|
val error = Throwable("Some unknown error")
|
||||||
assertThat(loginError(error)).isEqualTo(StringR.string.error_unknown)
|
assertThat(loginError(error)).isEqualTo(CommonStrings.error_unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `loginError - invalid auth error returns unknown error message`() {
|
fun `loginError - invalid auth error returns unknown error message`() {
|
||||||
val error = AuthenticationException.SlidingSyncNotAvailable("Some message. Also contains M_FORBIDDEN, but won't be parsed")
|
val error = AuthenticationException.SlidingSyncNotAvailable("Some message. Also contains M_FORBIDDEN, but won't be parsed")
|
||||||
assertThat(loginError(error)).isEqualTo(StringR.string.error_unknown)
|
assertThat(loginError(error)).isEqualTo(CommonStrings.error_unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `loginError - unknown error returns unknown error message`() {
|
fun `loginError - unknown error returns unknown error message`() {
|
||||||
val error = AuthenticationException.Generic("M_UNKNOWN")
|
val error = AuthenticationException.Generic("M_UNKNOWN")
|
||||||
assertThat(loginError(error)).isEqualTo(StringR.string.error_unknown)
|
assertThat(loginError(error)).isEqualTo(CommonStrings.error_unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogoutPreferenceView(
|
fun LogoutPreferenceView(
|
||||||
|
|
@ -81,7 +81,7 @@ fun LogoutPreferenceView(
|
||||||
fun LogoutPreferenceContent(
|
fun LogoutPreferenceContent(
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
PreferenceCategory(title = stringResource(id = StringR.string.settings_title_general)) {
|
PreferenceCategory(title = stringResource(id = CommonStrings.settings_title_general)) {
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
title = stringResource(id = R.string.screen_signout_preference_item),
|
title = stringResource(id = R.string.screen_signout_preference_item),
|
||||||
icon = Icons.Default.Logout,
|
icon = Icons.Default.Logout,
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.elementresources)
|
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.dateformatter.api)
|
implementation(projects.libraries.dateformatter.api)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import io.element.android.features.logout.api.LogoutPreferenceEvents
|
||||||
import io.element.android.features.logout.api.LogoutPreferencePresenter
|
import io.element.android.features.logout.api.LogoutPreferencePresenter
|
||||||
import io.element.android.features.logout.api.LogoutPreferenceState
|
import io.element.android.features.logout.api.LogoutPreferenceState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.execute
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -59,6 +59,6 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
|
||||||
private fun CoroutineScope.logout(logoutAction: MutableState<Async<Unit>>) = launch {
|
private fun CoroutineScope.logout(logoutAction: MutableState<Async<Unit>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
matrixClient.logout()
|
matrixClient.logout()
|
||||||
}.execute(logoutAction)
|
}.runCatchingUpdatingState(logoutAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ dependencies {
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.mediaupload.api)
|
implementation(projects.libraries.mediaupload.api)
|
||||||
implementation(projects.features.networkmonitor.api)
|
implementation(projects.features.networkmonitor.api)
|
||||||
|
implementation(projects.services.analytics.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
implementation(libs.accompanist.flowlayout)
|
implementation(libs.accompanist.flowlayout)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,6 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
sealed interface MessagesEvents {
|
sealed interface MessagesEvents {
|
||||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
||||||
data class SendReaction(val emoji: String, val eventId: EventId) : MessagesEvents
|
data class ToggleReaction(val emoji: String, val eventId: EventId) : MessagesEvents
|
||||||
object Dismiss : MessagesEvents
|
object Dismiss : MessagesEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
|
@ -30,13 +31,18 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
import io.element.android.services.analytics.api.extensions.toAnalyticsViewRoom
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
class MessagesNode @AssistedInject constructor(
|
class MessagesNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
private val presenterFactory: MessagesPresenter.Factory,
|
private val presenterFactory: MessagesPresenter.Factory,
|
||||||
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
||||||
|
|
||||||
|
|
@ -53,6 +59,14 @@ class MessagesNode @AssistedInject constructor(
|
||||||
fun onReportMessage(eventId: EventId, senderId: UserId)
|
fun onReportMessage(eventId: EventId, senderId: UserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
lifecycle.subscribe(
|
||||||
|
onCreate = {
|
||||||
|
analyticsService.capture(room.toAnalyticsViewRoom())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun onRoomDetailsClicked() {
|
private fun onRoomDetailsClicked() {
|
||||||
callback?.onRoomDetailsClicked()
|
callback?.onRoomDetailsClicked()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
id = room.roomId.value,
|
id = room.roomId.value,
|
||||||
name = room.name,
|
name = room.name,
|
||||||
url = room.avatarUrl,
|
url = room.avatarUrl,
|
||||||
size = AvatarSize.SMALL
|
size = AvatarSize.TimelineRoom
|
||||||
)
|
)
|
||||||
roomName.value = room.name
|
roomName.value = room.name
|
||||||
}
|
}
|
||||||
|
|
@ -130,8 +130,8 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
is MessagesEvents.HandleAction -> {
|
is MessagesEvents.HandleAction -> {
|
||||||
localCoroutineScope.handleTimelineAction(event.action, event.event, composerState)
|
localCoroutineScope.handleTimelineAction(event.action, event.event, composerState)
|
||||||
}
|
}
|
||||||
is MessagesEvents.SendReaction -> {
|
is MessagesEvents.ToggleReaction -> {
|
||||||
localCoroutineScope.sendReaction(event.emoji, event.eventId)
|
localCoroutineScope.toggleReaction(event.emoji, event.eventId)
|
||||||
}
|
}
|
||||||
is MessagesEvents.Dismiss -> actionListState.eventSink(ActionListEvents.Clear)
|
is MessagesEvents.Dismiss -> actionListState.eventSink(ActionListEvents.Clear)
|
||||||
}
|
}
|
||||||
|
|
@ -168,11 +168,11 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.sendReaction(
|
private fun CoroutineScope.toggleReaction(
|
||||||
emoji: String,
|
emoji: String,
|
||||||
eventId: EventId,
|
eventId: EventId,
|
||||||
) = launch(dispatchers.io) {
|
) = launch(dispatchers.io) {
|
||||||
room.sendReaction(emoji, eventId)
|
room.toggleReaction(emoji, eventId)
|
||||||
.onFailure { Timber.e(it) }
|
.onFailure { Timber.e(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.components.retrysendme
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.libraries.core.data.StableCharSequence
|
import io.element.android.libraries.core.data.StableCharSequence
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
|
|
||||||
|
|
@ -42,7 +43,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
fun aMessagesState() = MessagesState(
|
fun aMessagesState() = MessagesState(
|
||||||
roomId = RoomId("!id:domain"),
|
roomId = RoomId("!id:domain"),
|
||||||
roomName = "Room name",
|
roomName = "Room name",
|
||||||
roomAvatar = AvatarData("!id:domain", "Room name"),
|
roomAvatar = AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom),
|
||||||
userHasPermissionToSendMessage = true,
|
userHasPermissionToSendMessage = true,
|
||||||
composerState = aMessageComposerState().copy(
|
composerState = aMessageComposerState().copy(
|
||||||
text = StableCharSequence("Hello"),
|
text = StableCharSequence("Hello"),
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,9 @@ import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||||
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import io.element.android.libraries.ui.strings.R as StringsR
|
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -120,7 +120,7 @@ fun MessagesView(
|
||||||
|
|
||||||
fun onEmojiReactionClicked(emoji: String, event: TimelineItem.Event) {
|
fun onEmojiReactionClicked(emoji: String, event: TimelineItem.Event) {
|
||||||
if (event.eventId == null) return
|
if (event.eventId == null) return
|
||||||
state.eventSink(MessagesEvents.SendReaction(emoji, event.eventId))
|
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -150,7 +150,8 @@ fun MessagesView(
|
||||||
if (event.sendState is EventSendState.SendingFailed) {
|
if (event.sendState is EventSendState.SendingFailed) {
|
||||||
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
|
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
onReactionClicked = ::onEmojiReactionClicked
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
|
|
@ -174,7 +175,7 @@ fun MessagesView(
|
||||||
state = state.customReactionState,
|
state = state.customReactionState,
|
||||||
onEmojiSelected = { emoji ->
|
onEmojiSelected = { emoji ->
|
||||||
state.customReactionState.selectedEventId?.let { eventId ->
|
state.customReactionState.selectedEventId?.let { eventId ->
|
||||||
state.eventSink(MessagesEvents.SendReaction(emoji.unicode, eventId))
|
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
|
||||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +196,7 @@ private fun AttachmentStateView(
|
||||||
is AttachmentsState.Previewing -> LaunchedEffect(state) {
|
is AttachmentsState.Previewing -> LaunchedEffect(state) {
|
||||||
onPreviewAttachments(state.attachments)
|
onPreviewAttachments(state.attachments)
|
||||||
}
|
}
|
||||||
is AttachmentsState.Sending -> ProgressDialog(text = stringResource(id = StringsR.string.common_loading))
|
is AttachmentsState.Sending -> ProgressDialog(text = stringResource(id = CommonStrings.common_loading))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,6 +205,7 @@ fun MessagesViewContent(
|
||||||
state: MessagesState,
|
state: MessagesState,
|
||||||
onMessageClicked: (TimelineItem.Event) -> Unit,
|
onMessageClicked: (TimelineItem.Event) -> Unit,
|
||||||
onUserDataClicked: (UserId) -> Unit,
|
onUserDataClicked: (UserId) -> Unit,
|
||||||
|
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -223,6 +225,7 @@ fun MessagesViewContent(
|
||||||
onMessageLongClicked = onMessageLongClicked,
|
onMessageLongClicked = onMessageLongClicked,
|
||||||
onUserDataClicked = onUserDataClicked,
|
onUserDataClicked = onUserDataClicked,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
|
onReactionClicked = onReactionClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.userHasPermissionToSendMessage) {
|
if (state.userHasPermissionToSendMessage) {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ package io.element.android.features.messages.impl.actionlist
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
|
|
@ -29,15 +33,27 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
anActionListState().copy(
|
anActionListState().copy(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(),
|
event = aTimelineItemEvent(),
|
||||||
actions = persistentListOf(
|
actions = aTimelineItemActionList(),
|
||||||
TimelineItemAction.Reply,
|
|
||||||
TimelineItemAction.Forward,
|
|
||||||
TimelineItemAction.Copy,
|
|
||||||
TimelineItemAction.Edit,
|
|
||||||
TimelineItemAction.Redact,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
|
anActionListState().copy(
|
||||||
|
target = ActionListState.Target.Success(
|
||||||
|
event = aTimelineItemEvent(content = aTimelineItemImageContent()),
|
||||||
|
actions = aTimelineItemActionList(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
anActionListState().copy(
|
||||||
|
target = ActionListState.Target.Success(
|
||||||
|
event = aTimelineItemEvent(content = aTimelineItemVideoContent()),
|
||||||
|
actions = aTimelineItemActionList(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
anActionListState().copy(
|
||||||
|
target = ActionListState.Target.Success(
|
||||||
|
event = aTimelineItemEvent(content = aTimelineItemFileContent()),
|
||||||
|
actions = aTimelineItemActionList(),
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,3 +61,15 @@ fun anActionListState() = ActionListState(
|
||||||
target = ActionListState.Target.None,
|
target = ActionListState.Target.None,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {
|
||||||
|
return persistentListOf(
|
||||||
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.Redact,
|
||||||
|
TimelineItemAction.ReportContent,
|
||||||
|
TimelineItemAction.Developer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,27 +42,22 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SheetState
|
import androidx.compose.material3.SheetState
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.TextUnit
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
|
@ -74,6 +69,7 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.text.toSp
|
||||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
|
|
@ -169,9 +165,11 @@ private fun SheetContent(
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Column {
|
Column {
|
||||||
MessageSummary(event = target.event, modifier = Modifier
|
MessageSummary(
|
||||||
|
event = target.event, modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp))
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.height(14.dp))
|
Spacer(modifier = Modifier.height(14.dp))
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
|
|
@ -214,10 +212,10 @@ private fun SheetContent(
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
|
private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
|
||||||
val content: @Composable () -> Unit
|
val content: @Composable () -> Unit
|
||||||
var icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.SMALL)) }
|
var icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender)) }
|
||||||
val contentStyle = ElementTextStyles.Regular.bodyMD.copy(color = MaterialTheme.colorScheme.secondary)
|
val contentStyle = ElementTextStyles.Regular.bodyMD.copy(color = MaterialTheme.colorScheme.secondary)
|
||||||
val imageModifier = Modifier
|
val imageModifier = Modifier
|
||||||
.size(36.dp)
|
.size(AvatarSize.MessageActionSender.dp)
|
||||||
.clip(RoundedCornerShape(9.dp))
|
.clip(RoundedCornerShape(9.dp))
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -232,7 +230,6 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||||
when (event.content) {
|
when (event.content) {
|
||||||
is TimelineItemTextBasedContent,
|
is TimelineItemTextBasedContent,
|
||||||
is TimelineItemStateContent,
|
is TimelineItemStateContent,
|
||||||
is TimelineItemProfileChangeContent,
|
|
||||||
is TimelineItemEncryptedContent,
|
is TimelineItemEncryptedContent,
|
||||||
is TimelineItemRedactedContent,
|
is TimelineItemRedactedContent,
|
||||||
is TimelineItemUnknownContent -> content = { ContentForBody(textContent) }
|
is TimelineItemUnknownContent -> content = { ContentForBody(textContent) }
|
||||||
|
|
@ -282,7 +279,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||||
Row(modifier = modifier) {
|
Row(modifier = modifier) {
|
||||||
icon()
|
icon()
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Column {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Row {
|
Row {
|
||||||
if (event.senderDisplayName != null) {
|
if (event.senderDisplayName != null) {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -291,16 +288,16 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
|
||||||
event.sentTime,
|
|
||||||
style = ElementTextStyles.Regular.caption2,
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
textAlign = TextAlign.End,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
event.sentTime,
|
||||||
|
style = ElementTextStyles.Regular.caption2,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,7 +346,7 @@ private fun EmojiButton(
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
emoji,
|
emoji,
|
||||||
fontSize = 28.dpToSp(),
|
fontSize = 28.dp.toSp(),
|
||||||
modifier = modifier.clickable(
|
modifier = modifier.clickable(
|
||||||
enabled = true,
|
enabled = true,
|
||||||
onClick = { onClicked(emoji) },
|
onClick = { onClicked(emoji) },
|
||||||
|
|
@ -359,11 +356,6 @@ private fun EmojiButton(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Int.dpToSp(): TextUnit = with(LocalDensity.current) {
|
|
||||||
return dp.toSp()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) =
|
fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) =
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.designsystem.theme.ForcedDarkElementTheme
|
import io.element.android.libraries.theme.ForcedDarkElementTheme
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.executeResult
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -83,8 +83,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
mediaAttachment: Attachment.Media,
|
mediaAttachment: Attachment.Media,
|
||||||
sendActionState: MutableState<Async<Unit>>,
|
sendActionState: MutableState<Async<Unit>>,
|
||||||
) {
|
) {
|
||||||
suspend {
|
sendActionState.runUpdatingState {
|
||||||
mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.info.mimeType, mediaAttachment.compressIfPossible)
|
mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.info.mimeType, mediaAttachment.compressIfPossible)
|
||||||
}.executeResult(sendActionState)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.ui.strings.R
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.ui.strings.R as StringsR
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AttachmentsPreviewView(
|
fun AttachmentsPreviewView(
|
||||||
|
|
@ -92,7 +91,7 @@ private fun AttachmentSendStateView(
|
||||||
) {
|
) {
|
||||||
when (sendActionState) {
|
when (sendActionState) {
|
||||||
is Async.Loading -> {
|
is Async.Loading -> {
|
||||||
ProgressDialog(text = stringResource(id = R.string.common_loading))
|
ProgressDialog(text = stringResource(id = CommonStrings.common_loading))
|
||||||
}
|
}
|
||||||
|
|
||||||
is Async.Failure -> {
|
is Async.Failure -> {
|
||||||
|
|
@ -151,10 +150,10 @@ private fun AttachmentsPreviewBottomActions(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = onCancelClicked) {
|
TextButton(onClick = onCancelClicked) {
|
||||||
Text(stringResource(id = StringsR.string.action_cancel))
|
Text(stringResource(id = CommonStrings.action_cancel))
|
||||||
}
|
}
|
||||||
TextButton(onClick = onSendClicked) {
|
TextButton(onClick = onSendClicked) {
|
||||||
Text(stringResource(id = StringsR.string.action_send))
|
Text(stringResource(id = CommonStrings.action_send))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,14 @@
|
||||||
package io.element.android.features.messages.impl.attachments.preview.error
|
package io.element.android.features.messages.impl.attachments.preview.error
|
||||||
|
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.ui.strings.R
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
fun sendAttachmentError(
|
fun sendAttachmentError(
|
||||||
throwable: Throwable
|
throwable: Throwable
|
||||||
): Int {
|
): Int {
|
||||||
return if (throwable is MediaPreProcessor.Failure) {
|
return if (throwable is MediaPreProcessor.Failure) {
|
||||||
R.string.screen_media_upload_preview_error_failed_processing
|
CommonStrings.screen_media_upload_preview_error_failed_processing
|
||||||
} else {
|
} else {
|
||||||
R.string.screen_media_upload_preview_error_failed_sending
|
CommonStrings.screen_media_upload_preview_error_failed_sending
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.isLoading
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,13 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
|
@ -48,6 +48,7 @@ import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
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.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogDefaults
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogDefaults
|
||||||
|
|
@ -66,8 +67,8 @@ import io.element.android.libraries.designsystem.theme.roomListRoomName
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -111,7 +112,7 @@ fun ForwardMessagesView(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = { Text(stringResource(StringR.string.common_forward_message), style = ElementTextStyles.Bold.callout) },
|
title = { Text(stringResource(CommonStrings.common_forward_message), style = ElementTextStyles.Bold.callout) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
BackButton(onClick = { onBackButton(state) })
|
BackButton(onClick = { onBackButton(state) })
|
||||||
},
|
},
|
||||||
|
|
@ -120,7 +121,7 @@ fun ForwardMessagesView(
|
||||||
enabled = state.selectedRooms.isNotEmpty(),
|
enabled = state.selectedRooms.isNotEmpty(),
|
||||||
onClick = { state.eventSink(ForwardMessagesEvents.ForwardEvent) }
|
onClick = { state.eventSink(ForwardMessagesEvents.ForwardEvent) }
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(StringR.string.action_send))
|
Text(text = stringResource(CommonStrings.action_send))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -132,7 +133,7 @@ fun ForwardMessagesView(
|
||||||
.consumeWindowInsets(paddingValues)
|
.consumeWindowInsets(paddingValues)
|
||||||
) {
|
) {
|
||||||
SearchBar<ImmutableList<RoomSummaryDetails>>(
|
SearchBar<ImmutableList<RoomSummaryDetails>>(
|
||||||
placeHolderTitle = stringResource(StringR.string.action_search),
|
placeHolderTitle = stringResource(CommonStrings.action_search),
|
||||||
query = state.query,
|
query = state.query,
|
||||||
onQueryChange = { state.eventSink(ForwardMessagesEvents.UpdateQuery(it)) },
|
onQueryChange = { state.eventSink(ForwardMessagesEvents.UpdateQuery(it)) },
|
||||||
active = state.isSearchActive,
|
active = state.isSearchActive,
|
||||||
|
|
@ -227,18 +228,22 @@ internal fun RoomSummaryView(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clickable { onSelection(summary) }
|
.clickable { onSelection(summary) }
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(start = 16.dp, end = 4.dp)
|
||||||
.height(IntrinsicSize.Min),
|
.heightIn(56.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
val roomAlias = summary.canonicalAlias ?: summary.roomId.value
|
val roomAlias = summary.canonicalAlias ?: summary.roomId.value
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = AvatarData(id = roomAlias, name = summary.name, url = summary.avatarURLString),
|
avatarData = AvatarData(
|
||||||
|
id = roomAlias,
|
||||||
|
name = summary.name,
|
||||||
|
url = summary.avatarURLString,
|
||||||
|
size = AvatarSize.ForwardRoomListItem,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 12.dp, end = 4.dp, top = 8.dp, bottom = 8.dp)
|
.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp)
|
||||||
.alignByBaseline()
|
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
// Name
|
// Name
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
|
||||||
import io.element.android.libraries.designsystem.R
|
import io.element.android.libraries.designsystem.R
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||||
|
|
@ -242,7 +241,7 @@ fun MediaFileView(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = ElementTheme.colors.gray1400
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -250,7 +249,7 @@ fun MediaFileView(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = ElementTheme.colors.gray1400
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.text.roundToPx
|
||||||
|
import io.element.android.libraries.designsystem.text.toDp
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import me.saket.telephoto.zoomable.zoomable
|
import me.saket.telephoto.zoomable.zoomable
|
||||||
|
|
@ -51,7 +51,7 @@ fun PdfViewer(
|
||||||
modifier = modifier.zoomable(pdfViewerState.zoomableState),
|
modifier = modifier.zoomable(pdfViewerState.zoomableState),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
val maxWidthInPx = maxWidth.dpToPx()
|
val maxWidthInPx = maxWidth.roundToPx()
|
||||||
DisposableEffect(pdfViewerState) {
|
DisposableEffect(pdfViewerState) {
|
||||||
pdfViewerState.openForWidth(maxWidthInPx)
|
pdfViewerState.openForWidth(maxWidthInPx)
|
||||||
onDispose {
|
onDispose {
|
||||||
|
|
@ -107,15 +107,9 @@ private fun PdfPageView(
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(state.height.pxToDp())
|
.height(state.height.toDp())
|
||||||
.background(color = Color.White)
|
.background(color = Color.White)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() }
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Dp.dpToPx() = with(LocalDensity.current) { this@dpToPx.roundToPx() }
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.messages.impl.media.local.MediaInfo
|
import io.element.android.features.messages.impl.media.local.MediaInfo
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.designsystem.theme.ForcedDarkElementTheme
|
import io.element.android.libraries.theme.ForcedDarkElementTheme
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,10 @@ import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||||
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
|
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
|
||||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||||
import io.element.android.libraries.matrix.api.media.MediaFile
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import io.element.android.libraries.androidutils.R as UtilsR
|
import io.element.android.libraries.androidutils.R as UtilsR
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
class MediaViewerPresenter @AssistedInject constructor(
|
class MediaViewerPresenter @AssistedInject constructor(
|
||||||
@Assisted private val inputs: MediaViewerNode.Inputs,
|
@Assisted private val inputs: MediaViewerNode.Inputs,
|
||||||
|
|
@ -117,9 +117,9 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.saveOnDisk(localMedia: Async<LocalMedia>) = launch {
|
private fun CoroutineScope.saveOnDisk(localMedia: Async<LocalMedia>) = launch {
|
||||||
if (localMedia is Async.Success) {
|
if (localMedia is Async.Success) {
|
||||||
localMediaActions.saveOnDisk(localMedia.state)
|
localMediaActions.saveOnDisk(localMedia.data)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
val snackbarMessage = SnackbarMessage(StringR.string.common_file_saved_on_disk_android)
|
val snackbarMessage = SnackbarMessage(CommonStrings.common_file_saved_on_disk_android)
|
||||||
snackbarDispatcher.post(snackbarMessage)
|
snackbarDispatcher.post(snackbarMessage)
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
|
|
@ -131,7 +131,7 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.share(localMedia: Async<LocalMedia>) = launch {
|
private fun CoroutineScope.share(localMedia: Async<LocalMedia>) = launch {
|
||||||
if (localMedia is Async.Success) {
|
if (localMedia is Async.Success) {
|
||||||
localMediaActions.share(localMedia.state)
|
localMediaActions.share(localMedia.data)
|
||||||
.onFailure {
|
.onFailure {
|
||||||
val snackbarMessage = SnackbarMessage(mediaActionsError(it))
|
val snackbarMessage = SnackbarMessage(mediaActionsError(it))
|
||||||
snackbarDispatcher.post(snackbarMessage)
|
snackbarDispatcher.post(snackbarMessage)
|
||||||
|
|
@ -141,7 +141,7 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.open(localMedia: Async<LocalMedia>) = launch {
|
private fun CoroutineScope.open(localMedia: Async<LocalMedia>) = launch {
|
||||||
if (localMedia is Async.Success) {
|
if (localMedia is Async.Success) {
|
||||||
localMediaActions.open(localMedia.state)
|
localMediaActions.open(localMedia.data)
|
||||||
.onFailure {
|
.onFailure {
|
||||||
val snackbarMessage = SnackbarMessage(mediaActionsError(it))
|
val snackbarMessage = SnackbarMessage(mediaActionsError(it))
|
||||||
snackbarDispatcher.post(snackbarMessage)
|
snackbarDispatcher.post(snackbarMessage)
|
||||||
|
|
@ -153,7 +153,7 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
return if (throwable is ActivityNotFoundException) {
|
return if (throwable is ActivityNotFoundException) {
|
||||||
UtilsR.string.error_no_compatible_app_found
|
UtilsR.string.error_no_compatible_app_found
|
||||||
} else {
|
} else {
|
||||||
StringR.string.error_unknown
|
CommonStrings.error_unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ import io.element.android.features.messages.impl.media.local.LocalMediaView
|
||||||
import io.element.android.features.messages.impl.media.local.MediaInfo
|
import io.element.android.features.messages.impl.media.local.MediaInfo
|
||||||
import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState
|
import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.isLoading
|
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
|
|
@ -69,8 +68,8 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaViewerView(
|
fun MediaViewerView(
|
||||||
|
|
@ -131,7 +130,7 @@ fun MediaViewerView(
|
||||||
) {
|
) {
|
||||||
if (state.downloadedMedia is Async.Failure) {
|
if (state.downloadedMedia is Async.Failure) {
|
||||||
ErrorView(
|
ErrorView(
|
||||||
errorMessage = stringResource(id = StringR.string.error_unknown),
|
errorMessage = stringResource(id = CommonStrings.error_unknown),
|
||||||
onRetry = ::onRetry,
|
onRetry = ::onRetry,
|
||||||
onDismiss = ::onDismissError
|
onDismiss = ::onDismissError
|
||||||
)
|
)
|
||||||
|
|
@ -188,7 +187,7 @@ private fun MediaViewerTopBar(
|
||||||
eventSink(MediaViewerEvents.OpenWith)
|
eventSink(MediaViewerEvents.OpenWith)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Default.OpenInNew, contentDescription = stringResource(id = StringR.string.action_open_with))
|
Icon(imageVector = Icons.Default.OpenInNew, contentDescription = stringResource(id = CommonStrings.action_open_with))
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
enabled = actionsEnabled,
|
enabled = actionsEnabled,
|
||||||
|
|
@ -196,7 +195,7 @@ private fun MediaViewerTopBar(
|
||||||
eventSink(MediaViewerEvents.SaveOnDisk)
|
eventSink(MediaViewerEvents.SaveOnDisk)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Default.Download, contentDescription = stringResource(id = StringR.string.action_save))
|
Icon(imageVector = Icons.Default.Download, contentDescription = stringResource(id = CommonStrings.action_save))
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
enabled = actionsEnabled,
|
enabled = actionsEnabled,
|
||||||
|
|
@ -204,7 +203,7 @@ private fun MediaViewerTopBar(
|
||||||
eventSink(MediaViewerEvents.Share)
|
eventSink(MediaViewerEvents.Share)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Default.Share, contentDescription = stringResource(id = StringR.string.action_share))
|
Icon(imageVector = Icons.Default.Share, contentDescription = stringResource(id = CommonStrings.action_share))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,16 +29,15 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.executeResult
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
class ReportMessagePresenter @AssistedInject constructor(
|
class ReportMessagePresenter @AssistedInject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
|
|
@ -87,12 +86,12 @@ class ReportMessagePresenter @AssistedInject constructor(
|
||||||
blockUser: Boolean,
|
blockUser: Boolean,
|
||||||
result: MutableState<Async<Unit>>,
|
result: MutableState<Async<Unit>>,
|
||||||
) = launch {
|
) = launch {
|
||||||
suspend {
|
result.runUpdatingState {
|
||||||
val userIdToBlock = userId.takeIf { blockUser }
|
val userIdToBlock = userId.takeIf { blockUser }
|
||||||
room.reportContent(eventId, reason, userIdToBlock)
|
room.reportContent(eventId, reason, userIdToBlock)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
snackbarDispatcher.post(SnackbarMessage(StringR.string.common_report_submitted))
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_report_submitted))
|
||||||
}
|
}
|
||||||
}.executeResult(result)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ import io.element.android.libraries.designsystem.theme.components.CenterAlignedT
|
||||||
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
|
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -74,7 +74,7 @@ fun ReportMessageView(
|
||||||
}
|
}
|
||||||
is Async.Failure -> {
|
is Async.Failure -> {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
content = stringResource(StringR.string.error_unknown),
|
content = stringResource(CommonStrings.error_unknown),
|
||||||
onDismiss = { state.eventSink(ReportMessageEvents.ClearError) }
|
onDismiss = { state.eventSink(ReportMessageEvents.ClearError) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +86,7 @@ fun ReportMessageView(
|
||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(StringR.string.action_report_content),
|
stringResource(CommonStrings.action_report_content),
|
||||||
style = ElementTextStyles.Regular.callout,
|
style = ElementTextStyles.Regular.callout,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
)
|
)
|
||||||
|
|
@ -112,14 +112,14 @@ fun ReportMessageView(
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.reason,
|
value = state.reason,
|
||||||
onValueChange = { state.eventSink(ReportMessageEvents.UpdateReason(it)) },
|
onValueChange = { state.eventSink(ReportMessageEvents.UpdateReason(it)) },
|
||||||
placeholder = { Text(stringResource(StringR.string.report_content_hint)) },
|
placeholder = { Text(stringResource(CommonStrings.report_content_hint)) },
|
||||||
enabled = !isSending,
|
enabled = !isSending,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 90.dp)
|
.heightIn(min = 90.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(StringR.string.report_content_explanation),
|
text = stringResource(CommonStrings.report_content_explanation),
|
||||||
style = ElementTextStyles.Regular.caption1,
|
style = ElementTextStyles.Regular.caption1,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
|
|
@ -133,11 +133,11 @@ fun ReportMessageView(
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(StringR.string.screen_report_content_block_user),
|
text = stringResource(CommonStrings.screen_report_content_block_user),
|
||||||
style = ElementTextStyles.Regular.callout,
|
style = ElementTextStyles.Regular.callout,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(StringR.string.screen_report_content_block_user_hint),
|
text = stringResource(CommonStrings.screen_report_content_block_user_hint),
|
||||||
style = ElementTextStyles.Regular.bodyMD,
|
style = ElementTextStyles.Regular.bodyMD,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
)
|
)
|
||||||
|
|
@ -152,7 +152,7 @@ fun ReportMessageView(
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
ButtonWithProgress(
|
ButtonWithProgress(
|
||||||
text = stringResource(StringR.string.action_send),
|
text = stringResource(CommonStrings.action_send),
|
||||||
enabled = state.reason.isNotBlank() && !isSending,
|
enabled = state.reason.isNotBlank() && !isSending,
|
||||||
showProgress = isSending,
|
showProgress = isSending,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemReac
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
|
|
@ -32,6 +35,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventSendStat
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState(
|
fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState(
|
||||||
|
|
@ -83,15 +88,23 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
||||||
content = content,
|
content = content,
|
||||||
groupPosition = TimelineItemGroupPosition.First
|
groupPosition = TimelineItemGroupPosition.First
|
||||||
),
|
),
|
||||||
// A state event on top of it
|
// A grouped event on top of it
|
||||||
aTimelineItemEvent(
|
aGroupedEvents(),
|
||||||
isMine = true,
|
// A day separator
|
||||||
content = aTimelineItemStateEventContent(),
|
aTimelineItemDaySeparator(),
|
||||||
groupPosition = TimelineItemGroupPosition.None
|
// Loading
|
||||||
),
|
aTimelineItemLoading(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun aTimelineItemLoading(): TimelineItem.Virtual {
|
||||||
|
return TimelineItem.Virtual("virtual_loading", TimelineItemLoadingModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aTimelineItemDaySeparator(): TimelineItem.Virtual {
|
||||||
|
return TimelineItem.Virtual("virtual_day", aTimelineItemDaySeparatorModel("Today"))
|
||||||
|
}
|
||||||
|
|
||||||
internal fun aTimelineItemEvent(
|
internal fun aTimelineItemEvent(
|
||||||
eventId: EventId = EventId("\$" + Random.nextInt().toString()),
|
eventId: EventId = EventId("\$" + Random.nextInt().toString()),
|
||||||
transactionId: String? = null,
|
transactionId: String? = null,
|
||||||
|
|
@ -101,22 +114,19 @@ internal fun aTimelineItemEvent(
|
||||||
sendState: EventSendState = EventSendState.Sent(eventId),
|
sendState: EventSendState = EventSendState.Sent(eventId),
|
||||||
inReplyTo: InReplyTo? = null,
|
inReplyTo: InReplyTo? = null,
|
||||||
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
||||||
|
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
|
||||||
): TimelineItem.Event {
|
): TimelineItem.Event {
|
||||||
return TimelineItem.Event(
|
return TimelineItem.Event(
|
||||||
id = eventId.value,
|
id = eventId.value,
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
transactionId = transactionId,
|
transactionId = transactionId,
|
||||||
senderId = UserId("@senderId:domain"),
|
senderId = UserId("@senderId:domain"),
|
||||||
senderAvatar = AvatarData("@senderId:domain", "sender"),
|
senderAvatar = AvatarData("@senderId:domain", "sender", size = AvatarSize.TimelineSender),
|
||||||
content = content,
|
content = content,
|
||||||
reactionsState = TimelineItemReactions(
|
reactionsState = timelineItemReactions,
|
||||||
persistentListOf(
|
|
||||||
AggregatedReaction("👍", "1")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
sentTime = "12:34",
|
sentTime = "12:34",
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
senderDisplayName = "sender",
|
senderDisplayName = "Sender",
|
||||||
groupPosition = groupPosition,
|
groupPosition = groupPosition,
|
||||||
sendState = sendState,
|
sendState = sendState,
|
||||||
inReplyTo = inReplyTo,
|
inReplyTo = inReplyTo,
|
||||||
|
|
@ -124,6 +134,19 @@ internal fun aTimelineItemEvent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun aTimelineItemReactions(
|
||||||
|
count: Int = 1,
|
||||||
|
isHighlighted: Boolean = false,
|
||||||
|
): TimelineItemReactions {
|
||||||
|
return TimelineItemReactions(
|
||||||
|
reactions = buildList {
|
||||||
|
repeat(count) {
|
||||||
|
add(AggregatedReaction(key = "👍", count = 1 + it, isHighlighted = isHighlighted))
|
||||||
|
}
|
||||||
|
}.toPersistentList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun aTimelineItemDebugInfo(
|
internal fun aTimelineItemDebugInfo(
|
||||||
model: String = "Rust(Model())",
|
model: String = "Rust(Model())",
|
||||||
originalJson: String? = null,
|
originalJson: String? = null,
|
||||||
|
|
@ -131,3 +154,17 @@ internal fun aTimelineItemDebugInfo(
|
||||||
) = TimelineItemDebugInfo(
|
) = TimelineItemDebugInfo(
|
||||||
model, originalJson, latestEditedJson
|
model, originalJson, latestEditedJson
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun aGroupedEvents(): TimelineItem.GroupedEvents {
|
||||||
|
val event = aTimelineItemEvent(
|
||||||
|
isMine = true,
|
||||||
|
content = aTimelineItemStateEventContent(),
|
||||||
|
groupPosition = TimelineItemGroupPosition.None
|
||||||
|
)
|
||||||
|
return TimelineItem.GroupedEvents(
|
||||||
|
events = listOf(
|
||||||
|
event,
|
||||||
|
event,
|
||||||
|
).toImmutableList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ fun TimelineView(
|
||||||
onMessageClicked: (TimelineItem.Event) -> Unit,
|
onMessageClicked: (TimelineItem.Event) -> Unit,
|
||||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
onReactionClicked: (emoji: String, TimelineItem.Event) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun onReachedLoadMore() {
|
fun onReachedLoadMore() {
|
||||||
|
|
@ -89,7 +90,7 @@ fun TimelineView(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
reverseLayout = true,
|
reverseLayout = true,
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = state.timelineItems,
|
items = state.timelineItems,
|
||||||
|
|
@ -103,6 +104,7 @@ fun TimelineView(
|
||||||
onLongClick = onMessageLongClicked,
|
onLongClick = onMessageLongClicked,
|
||||||
onUserDataClick = onUserDataClicked,
|
onUserDataClick = onUserDataClicked,
|
||||||
inReplyToClick = ::inReplyToClicked,
|
inReplyToClick = ::inReplyToClicked,
|
||||||
|
onReactionClick = onReactionClicked,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
)
|
)
|
||||||
if (index == state.timelineItems.lastIndex) {
|
if (index == state.timelineItems.lastIndex) {
|
||||||
|
|
@ -127,6 +129,7 @@ fun TimelineItemRow(
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
@ -162,6 +165,7 @@ fun TimelineItemRow(
|
||||||
onLongClick = ::onLongClick,
|
onLongClick = ::onLongClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
|
onReactionClick = onReactionClick,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
@ -196,6 +200,7 @@ fun TimelineItemRow(
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
|
onReactionClick = onReactionClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -286,5 +291,6 @@ private fun ContentToPreview(content: TimelineItemEventContent) {
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onUserDataClicked = {},
|
onUserDataClicked = {},
|
||||||
onMessageLongClicked = {},
|
onMessageLongClicked = {},
|
||||||
|
onReactionClicked = { _, _ -> },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
|
@ -41,13 +42,16 @@ import io.element.android.features.messages.impl.timeline.model.bubble.BubbleSta
|
||||||
import io.element.android.libraries.core.extensions.to01
|
import io.element.android.libraries.core.extensions.to01
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
||||||
private val BUBBLE_RADIUS = 12.dp
|
private val BUBBLE_RADIUS = 12.dp
|
||||||
private val BUBBLE_INCOMING_OFFSET = 16.dp
|
private val BUBBLE_INCOMING_OFFSET = 16.dp
|
||||||
|
|
||||||
|
// Design says: The maximum width of a bubble is still 3/4 of the screen width
|
||||||
|
private const val BUBBLE_WIDTH_RATIO = 0.75f
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageEventBubble(
|
fun MessageEventBubble(
|
||||||
|
|
@ -94,30 +98,39 @@ fun MessageEventBubble(
|
||||||
}
|
}
|
||||||
|
|
||||||
val backgroundBubbleColor = if (state.isHighlighted) {
|
val backgroundBubbleColor = if (state.isHighlighted) {
|
||||||
ElementTheme.colors.messageHighlightedBackground
|
ElementTheme.legacyColors.messageHighlightedBackground
|
||||||
} else {
|
} else {
|
||||||
if (state.isMine) {
|
if (state.isMine) {
|
||||||
ElementTheme.colors.messageFromMeBackground
|
ElementTheme.legacyColors.messageFromMeBackground
|
||||||
} else {
|
} else {
|
||||||
ElementTheme.colors.messageFromOtherBackground
|
ElementTheme.legacyColors.messageFromOtherBackground
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val bubbleShape = bubbleShape()
|
val bubbleShape = bubbleShape()
|
||||||
Surface(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.widthIn(min = 80.dp)
|
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
|
||||||
.offsetForItem()
|
.padding(horizontal = 16.dp)
|
||||||
.clip(bubbleShape)
|
.offsetForItem(),
|
||||||
.combinedClickable(
|
// Need to set the contentAlignment again (it's already set in TimelineItemEventRow), for the case
|
||||||
onClick = onClick,
|
// when content width is low.
|
||||||
onLongClick = onLongClick,
|
contentAlignment = if (state.isMine) Alignment.CenterEnd else Alignment.CenterStart
|
||||||
indication = rememberRipple(),
|
) {
|
||||||
interactionSource = interactionSource
|
Surface(
|
||||||
),
|
modifier = Modifier
|
||||||
color = backgroundBubbleColor,
|
.widthIn(min = 80.dp)
|
||||||
shape = bubbleShape,
|
.clip(bubbleShape)
|
||||||
content = content
|
.combinedClickable(
|
||||||
)
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
indication = rememberRipple(),
|
||||||
|
interactionSource = interactionSource
|
||||||
|
),
|
||||||
|
color = backgroundBubbleColor,
|
||||||
|
shape = bubbleShape,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
|
|
||||||
private val CORNER_RADIUS = 8.dp
|
private val CORNER_RADIUS = 8.dp
|
||||||
|
|
@ -49,7 +49,7 @@ fun MessageStateEventContainer(
|
||||||
content: @Composable () -> Unit = {},
|
content: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val backgroundColor = if (isHighlighted) {
|
val backgroundColor = if (isHighlighted) {
|
||||||
ElementTheme.colors.messageHighlightedBackground
|
ElementTheme.legacyColors.messageHighlightedBackground
|
||||||
} else {
|
} else {
|
||||||
Color.Companion.Transparent
|
Color.Companion.Transparent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -27,6 +30,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -37,23 +41,40 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
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.components.Text
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier) {
|
fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||||
|
// First Surface is to render a border with the same background color as the background
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier.clickable(onClick = onClick::invoke),
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
// TODO Should use compound.bgSubtlePrimary
|
||||||
|
color = ElementTheme.legacyColors.gray300,
|
||||||
border = BorderStroke(2.dp, MaterialTheme.colorScheme.background),
|
border = BorderStroke(2.dp, MaterialTheme.colorScheme.background),
|
||||||
shape = RoundedCornerShape(corner = CornerSize(12.dp)),
|
shape = RoundedCornerShape(corner = CornerSize(14.dp)),
|
||||||
) {
|
) {
|
||||||
Row(
|
Box(modifier = Modifier.padding(2.dp)) {
|
||||||
modifier = Modifier.padding(vertical = 5.dp, horizontal = 8.dp),
|
val reactionModifier = if (reaction.isHighlighted) {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
Modifier
|
||||||
) {
|
// TODO Check the color, should use compound.borderInteractivePrimary
|
||||||
// TODO `reaction.isHighlighted` is not used.
|
.border(BorderStroke(1.dp, Color(0xFF808994)), RoundedCornerShape(corner = CornerSize(12.dp)))
|
||||||
Text(text = reaction.key, fontSize = 12.sp)
|
} else {
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Modifier
|
||||||
Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp)
|
}
|
||||||
|
Row(
|
||||||
|
modifier = reactionModifier.padding(vertical = 4.dp, horizontal = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = reaction.key, fontSize = 15.sp)
|
||||||
|
if (reaction.count > 1) {
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = reaction.count.toString(),
|
||||||
|
color = if (reaction.isHighlighted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,5 +91,5 @@ internal fun MessagesReactionButtonDarkPreview(@PreviewParameter(AggregatedReact
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(reaction: AggregatedReaction) {
|
private fun ContentToPreview(reaction: AggregatedReaction) {
|
||||||
MessagesReactionButton(reaction)
|
MessagesReactionButton(reaction, onClick = { })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,10 @@ import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||||
import io.element.android.libraries.ui.strings.R
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineEventTimestampView(
|
fun TimelineEventTimestampView(
|
||||||
|
|
@ -55,7 +54,7 @@ fun TimelineEventTimestampView(
|
||||||
val formattedTime = event.sentTime
|
val formattedTime = event.sentTime
|
||||||
val hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed
|
val hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed
|
||||||
val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse()
|
val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse()
|
||||||
val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null
|
val tint = if (hasMessageSendingFailed) MaterialTheme.colorScheme.error else null
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(
|
.clickable(
|
||||||
|
|
@ -70,7 +69,7 @@ fun TimelineEventTimestampView(
|
||||||
) {
|
) {
|
||||||
if (isMessageEdited) {
|
if (isMessageEdited) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.common_edited_suffix),
|
stringResource(CommonStrings.common_edited_suffix),
|
||||||
style = ElementTextStyles.Regular.caption2,
|
style = ElementTextStyles.Regular.caption2,
|
||||||
color = tint ?: MaterialTheme.colorScheme.secondary,
|
color = tint ?: MaterialTheme.colorScheme.secondary,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
|
@ -27,11 +28,9 @@ import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -40,22 +39,32 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.LastBaseline
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
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.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.unit.sp
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.event.toExtraPadding
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
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.ElementTextStyles
|
||||||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
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.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
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.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -66,6 +75,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemEventRow(
|
fun TimelineItemEventRow(
|
||||||
|
|
@ -76,6 +87,7 @@ fun TimelineItemEventRow(
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
@ -84,71 +96,81 @@ fun TimelineItemEventRow(
|
||||||
onUserDataClick(event.senderId)
|
onUserDataClick(event.senderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onReactionClicked(emoji: String) =
|
||||||
|
onReactionClick(emoji, event)
|
||||||
|
|
||||||
fun inReplyToClicked() {
|
fun inReplyToClicked() {
|
||||||
val inReplyToEventId = (event.inReplyTo as? InReplyTo.Ready)?.eventId ?: return
|
val inReplyToEventId = (event.inReplyTo as? InReplyTo.Ready)?.eventId ?: return
|
||||||
inReplyToClick(inReplyToEventId)
|
inReplyToClick(inReplyToEventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
val (parentAlignment, contentAlignment) = if (event.isMine) {
|
// To avoid using negative offset, we display in this Box a column with:
|
||||||
Pair(Alignment.CenterEnd, Alignment.End)
|
// - Spacer to give room to the Sender information if they must be displayed;
|
||||||
} else {
|
// - The message bubble;
|
||||||
Pair(Alignment.CenterStart, Alignment.Start)
|
// - Spacer for the reactions if there are some.
|
||||||
}
|
// Then the Sender information and the reactions are displayed on top of it.
|
||||||
|
// This fixes some clickable issue and some unexpected margin on top and bottom of each message row
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight(),
|
.wrapContentHeight(),
|
||||||
contentAlignment = parentAlignment
|
contentAlignment = if (event.isMine) Alignment.CenterEnd else Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
Row {
|
Column {
|
||||||
Column(horizontalAlignment = contentAlignment) {
|
if (event.showSenderInformation) {
|
||||||
if (event.showSenderInformation) {
|
Spacer(modifier = Modifier.height(event.senderAvatar.size.dp - 8.dp))
|
||||||
MessageSenderInformation(
|
}
|
||||||
event.safeSenderName,
|
val bubbleState = BubbleState(
|
||||||
event.senderAvatar,
|
groupPosition = event.groupPosition,
|
||||||
Modifier
|
isMine = event.isMine,
|
||||||
.zIndex(1f)
|
isHighlighted = isHighlighted,
|
||||||
.offset(y = 12.dp)
|
)
|
||||||
.clickable(onClick = ::onUserDataClicked)
|
MessageEventBubble(
|
||||||
)
|
state = bubbleState,
|
||||||
}
|
interactionSource = interactionSource,
|
||||||
val bubbleState = BubbleState(
|
onClick = onClick,
|
||||||
groupPosition = event.groupPosition,
|
onLongClick = onLongClick,
|
||||||
isMine = event.isMine,
|
) {
|
||||||
isHighlighted = isHighlighted,
|
MessageEventBubbleContent(
|
||||||
)
|
event = event,
|
||||||
MessageEventBubble(
|
|
||||||
state = bubbleState,
|
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onClick,
|
onMessageClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onMessageLongClick = onLongClick,
|
||||||
modifier = Modifier
|
inReplyToClick = ::inReplyToClicked,
|
||||||
.zIndex(-1f)
|
onTimestampClicked = {
|
||||||
.widthIn(max = 320.dp)
|
onTimestampClicked(event)
|
||||||
) {
|
}
|
||||||
MessageEventBubbleContent(
|
|
||||||
event = event,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onMessageClick = onClick,
|
|
||||||
onMessageLongClick = onLongClick,
|
|
||||||
inReplyToClick = ::inReplyToClicked,
|
|
||||||
onTimestampClicked = {
|
|
||||||
onTimestampClicked(event)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TimelineItemReactionsView(
|
|
||||||
reactionsState = event.reactionsState,
|
|
||||||
modifier = Modifier
|
|
||||||
.zIndex(1f)
|
|
||||||
.offset(x = if (event.isMine) 0.dp else 20.dp, y = -(4.dp))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Align to the top of the box
|
||||||
|
if (event.showSenderInformation) {
|
||||||
|
MessageSenderInformation(
|
||||||
|
event.safeSenderName,
|
||||||
|
event.senderAvatar,
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.clickable(onClick = ::onUserDataClicked)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Align to the bottom of the box
|
||||||
|
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||||
|
TimelineItemReactionsView(
|
||||||
|
reactionsState = event.reactionsState,
|
||||||
|
onReactionClicked = ::onReactionClicked,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(if (event.isMine) Alignment.BottomEnd else Alignment.BottomStart)
|
||||||
|
.padding(start = if (event.isMine) 16.dp else 36.dp, end = 16.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// This is assuming that we are in a ColumnScope, but this is OK, for both Preview and real usage.
|
||||||
if (event.groupPosition.isNew()) {
|
if (event.groupPosition.isNew()) {
|
||||||
Spacer(modifier = modifier.height(8.dp))
|
Spacer(modifier = modifier.height(16.dp))
|
||||||
} else {
|
} else {
|
||||||
Spacer(modifier = modifier.height(2.dp))
|
Spacer(modifier = modifier.height(2.dp))
|
||||||
}
|
}
|
||||||
|
|
@ -157,20 +179,38 @@ fun TimelineItemEventRow(
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageSenderInformation(
|
private fun MessageSenderInformation(
|
||||||
sender: String,
|
sender: String,
|
||||||
senderAvatar: AvatarData?,
|
senderAvatar: AvatarData,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(modifier = modifier) {
|
val avatarStrokeSize = 3.dp
|
||||||
if (senderAvatar != null) {
|
val avatarStrokeColor = MaterialTheme.colorScheme.background
|
||||||
|
val avatarSize = senderAvatar.size.dp
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
// Background of Avatar, to erase the corner of the message content
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size = avatarSize + avatarStrokeSize)
|
||||||
|
.clipToBounds()
|
||||||
|
) {
|
||||||
|
drawCircle(
|
||||||
|
color = avatarStrokeColor,
|
||||||
|
center = Offset(x = (avatarSize / 2).toPx(), y = (avatarSize / 2).toPx()),
|
||||||
|
radius = (avatarSize / 2 + avatarStrokeSize).toPx()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Content
|
||||||
|
Row {
|
||||||
Avatar(senderAvatar)
|
Avatar(senderAvatar)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = sender,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Text(
|
|
||||||
text = sender,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
modifier = Modifier
|
|
||||||
.alignBy(LastBaseline)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,6 +236,7 @@ private fun MessageEventBubbleContent(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onMessageClick,
|
onClick = onMessageClick,
|
||||||
onLongClick = onMessageLongClick,
|
onLongClick = onMessageLongClick,
|
||||||
|
extraPadding = event.toExtraPadding(),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -215,20 +256,20 @@ private fun MessageEventBubbleContent(
|
||||||
onClick = onTimestampClicked,
|
onClick = onTimestampClicked,
|
||||||
modifier = timestampModifier
|
modifier = timestampModifier
|
||||||
.padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding
|
.padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding
|
||||||
.background(LocalColors.current.gray300, RoundedCornerShape(10.0.dp))
|
.background(ElementTheme.legacyColors.gray300, RoundedCornerShape(10.0.dp))
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(horizontal = 4.dp, vertical = 2.dp) // Inner padding
|
.padding(horizontal = 4.dp, vertical = 2.dp) // Inner padding
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Column(modifier) {
|
Box(modifier) {
|
||||||
ContentView(modifier = contentModifier.padding(start = 12.dp, end = 12.dp, top = 8.dp))
|
ContentView(modifier = contentModifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp))
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
event = event,
|
||||||
onClick = onTimestampClicked,
|
onClick = onTimestampClicked,
|
||||||
modifier = timestampModifier
|
modifier = timestampModifier
|
||||||
.align(Alignment.End)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -328,7 +369,7 @@ private fun ReplyToContent(
|
||||||
text = text.orEmpty(),
|
text = text.orEmpty(),
|
||||||
style = ElementTextStyles.Regular.caption1,
|
style = ElementTextStyles.Regular.caption1,
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
color = LocalColors.current.placeholder,
|
color = ElementTheme.legacyColors.placeholder,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
|
@ -358,3 +399,94 @@ private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready) =
|
||||||
)
|
)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemEventRowLightPreview() =
|
||||||
|
ElementPreviewLight { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemEventRowDarkPreview() =
|
||||||
|
ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview() {
|
||||||
|
Column {
|
||||||
|
sequenceOf(false, true).forEach {
|
||||||
|
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."
|
||||||
|
)
|
||||||
|
),
|
||||||
|
isHighlighted = false,
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
|
onUserDataClick = {},
|
||||||
|
inReplyToClick = {},
|
||||||
|
onReactionClick = { _, _ -> },
|
||||||
|
onTimestampClicked = {},
|
||||||
|
)
|
||||||
|
TimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(
|
||||||
|
isMine = it,
|
||||||
|
content = aTimelineItemImageContent().copy(
|
||||||
|
aspectRatio = 5f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
isHighlighted = false,
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
|
onUserDataClick = {},
|
||||||
|
inReplyToClick = {},
|
||||||
|
onReactionClick = { _, _ -> },
|
||||||
|
onTimestampClicked = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemEventRowTimestampLightPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) =
|
||||||
|
ElementPreviewLight { ContentTimestampToPreview(event) }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemEventRowTimestampDarkPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) =
|
||||||
|
ElementPreviewDark { ContentTimestampToPreview(event) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentTimestampToPreview(event: TimelineItem.Event) {
|
||||||
|
Column {
|
||||||
|
val oldContent = event.content as TimelineItemTextContent
|
||||||
|
listOf(
|
||||||
|
"Text",
|
||||||
|
"Text longer, displayed on 1 line",
|
||||||
|
"Text which should be rendered on several lines",
|
||||||
|
).forEach { str ->
|
||||||
|
listOf(false, true).forEach { useDocument ->
|
||||||
|
TimelineItemEventRow(
|
||||||
|
event = event.copy(
|
||||||
|
content = oldContent.copy(
|
||||||
|
body = str,
|
||||||
|
htmlDocument = if (useDocument) Jsoup.parse(str) else null,
|
||||||
|
),
|
||||||
|
reactionsState = aTimelineItemReactions(count = 0),
|
||||||
|
senderDisplayName = if (useDocument) "Document case" else "Text case",
|
||||||
|
),
|
||||||
|
isHighlighted = false,
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
|
onUserDataClick = {},
|
||||||
|
inReplyToClick = {},
|
||||||
|
onReactionClick = { _, _ -> },
|
||||||
|
onTimestampClicked = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,18 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
fun TimelineItemReactionsView(
|
fun TimelineItemReactionsView(
|
||||||
reactionsState: TimelineItemReactions,
|
reactionsState: TimelineItemReactions,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
onReactionClicked: (emoji: String) -> Unit
|
||||||
) {
|
) {
|
||||||
if (reactionsState.reactions.isEmpty()) return
|
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
mainAxisSpacing = 2.dp,
|
mainAxisSpacing = 2.dp,
|
||||||
crossAxisSpacing = 8.dp,
|
crossAxisSpacing = 8.dp,
|
||||||
) {
|
) {
|
||||||
reactionsState.reactions.forEach { reaction ->
|
reactionsState.reactions.forEach { reaction ->
|
||||||
MessagesReactionButton(reaction = reaction)
|
MessagesReactionButton(
|
||||||
|
reaction = reaction,
|
||||||
|
onClick = { onReactionClicked(reaction.key) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,6 +59,7 @@ internal fun TimelineItemReactionsViewDarkPreview() =
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
TimelineItemReactionsView(
|
TimelineItemReactionsView(
|
||||||
reactionsState = aTimelineItemReactions()
|
reactionsState = aTimelineItemReactions(),
|
||||||
|
onReactionClicked = { }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,18 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding
|
import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemStateEventRow(
|
fun TimelineItemStateEventRow(
|
||||||
|
|
@ -60,8 +67,33 @@ fun TimelineItemStateEventRow(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
extraPadding = noExtraPadding,
|
||||||
modifier = Modifier.defaultTimelineContentPadding()
|
modifier = Modifier.defaultTimelineContentPadding()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemStateEventRowLightPreview() =
|
||||||
|
ElementPreviewLight { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemStateEventRowDarkPreview() =
|
||||||
|
ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview() {
|
||||||
|
TimelineItemStateEventRow(
|
||||||
|
event = aTimelineItemEvent(
|
||||||
|
isMine = false,
|
||||||
|
content = aTimelineItemStateEventContent(),
|
||||||
|
groupPosition = TimelineItemGroupPosition.None
|
||||||
|
),
|
||||||
|
isHighlighted = false,
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.event
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
// Allow to not overlap the timestamp with the text, in the message bubble.
|
||||||
|
// Compute the size of the worst case.
|
||||||
|
data class ExtraPadding(val str: String)
|
||||||
|
|
||||||
|
val noExtraPadding = ExtraPadding("")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [io.element.android.features.messages.impl.timeline.components.TimelineEventTimestampView] for the related View.
|
||||||
|
* And https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1819%253A99506 for the design.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TimelineItem.Event.toExtraPadding(): ExtraPadding {
|
||||||
|
val formattedTime = sentTime
|
||||||
|
val hasMessageSendingFailed = sendState is EventSendState.SendingFailed
|
||||||
|
val isMessageEdited = (content as? TimelineItemTextBasedContent)?.isEdited.orFalse()
|
||||||
|
|
||||||
|
var strLen = 2
|
||||||
|
if (isMessageEdited) {
|
||||||
|
strLen += stringResource(id = CommonStrings.common_edited_suffix).length + 2
|
||||||
|
}
|
||||||
|
strLen += formattedTime.length
|
||||||
|
if (hasMessageSendingFailed) {
|
||||||
|
strLen += 5
|
||||||
|
}
|
||||||
|
// A space and a few unbreakable spaces
|
||||||
|
return ExtraPadding(" " + "\u00A0".repeat(strLen))
|
||||||
|
}
|
||||||
|
|
@ -17,10 +17,8 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components.event
|
package io.element.android.features.messages.impl.timeline.components.event
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
|
|
@ -35,6 +33,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
fun TimelineItemEventContentView(
|
fun TimelineItemEventContentView(
|
||||||
content: TimelineItemEventContent,
|
content: TimelineItemEventContent,
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
|
extraPadding: ExtraPadding,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
|
@ -42,14 +41,17 @@ fun TimelineItemEventContentView(
|
||||||
when (content) {
|
when (content) {
|
||||||
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
|
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
|
||||||
content = content,
|
content = content,
|
||||||
|
extraPadding = extraPadding,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
is TimelineItemRedactedContent -> TimelineItemRedactedView(
|
is TimelineItemRedactedContent -> TimelineItemRedactedView(
|
||||||
content = content,
|
content = content,
|
||||||
|
extraPadding = extraPadding,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
is TimelineItemTextBasedContent -> TimelineItemTextView(
|
is TimelineItemTextBasedContent -> TimelineItemTextView(
|
||||||
content = content,
|
content = content,
|
||||||
|
extraPadding = extraPadding,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onTextClicked = onClick,
|
onTextClicked = onClick,
|
||||||
|
|
@ -57,6 +59,7 @@ fun TimelineItemEventContentView(
|
||||||
)
|
)
|
||||||
is TimelineItemUnknownContent -> TimelineItemUnknownView(
|
is TimelineItemUnknownContent -> TimelineItemUnknownView(
|
||||||
content = content,
|
content = content,
|
||||||
|
extraPadding = extraPadding,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
is TimelineItemImageContent -> TimelineItemImageView(
|
is TimelineItemImageContent -> TimelineItemImageView(
|
||||||
|
|
@ -69,6 +72,7 @@ fun TimelineItemEventContentView(
|
||||||
)
|
)
|
||||||
is TimelineItemFileContent -> TimelineItemFileView(
|
is TimelineItemFileContent -> TimelineItemFileView(
|
||||||
content = content,
|
content = content,
|
||||||
|
extraPadding = extraPadding,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
is TimelineItemStateContent -> TimelineItemStateView(
|
is TimelineItemStateContent -> TimelineItemStateView(
|
||||||
|
|
|
||||||
|
|
@ -26,18 +26,19 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemEncryptedView(
|
fun TimelineItemEncryptedView(
|
||||||
content: TimelineItemEncryptedContent,
|
content: TimelineItemEncryptedContent,
|
||||||
|
extraPadding: ExtraPadding,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
TimelineItemInformativeView(
|
TimelineItemInformativeView(
|
||||||
text = stringResource(id = StringR.string.common_decryption_error),
|
text = stringResource(id = CommonStrings.common_decryption_error),
|
||||||
iconDescription = stringResource(id = StringR.string.dialog_title_warning),
|
iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
|
||||||
icon = Icons.Default.Warning,
|
icon = Icons.Default.Warning,
|
||||||
|
extraPadding = extraPadding,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +58,7 @@ private fun ContentToPreview() {
|
||||||
TimelineItemEncryptedView(
|
TimelineItemEncryptedView(
|
||||||
content = TimelineItemEncryptedContent(
|
content = TimelineItemEncryptedContent(
|
||||||
data = UnableToDecryptContent.Data.Unknown
|
data = UnableToDecryptContent.Data.Unknown
|
||||||
)
|
),
|
||||||
|
extraPadding = noExtraPadding
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemFileView(
|
fun TimelineItemFileView(
|
||||||
content: TimelineItemFileContent,
|
content: TimelineItemFileContent,
|
||||||
|
extraPadding: ExtraPadding,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -76,7 +77,7 @@ fun TimelineItemFileView(
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = content.fileExtensionAndSize,
|
text = content.fileExtensionAndSize + extraPadding.str,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
@ -98,5 +99,8 @@ internal fun TimelineItemFileViewDarkPreview(@PreviewParameter(TimelineItemFileC
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(content: TimelineItemFileContent) {
|
private fun ContentToPreview(content: TimelineItemFileContent) {
|
||||||
TimelineItemFileView(content)
|
TimelineItemFileView(
|
||||||
|
content,
|
||||||
|
extraPadding = noExtraPadding,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ fun TimelineItemInformativeView(
|
||||||
text: String,
|
text: String,
|
||||||
iconDescription: String,
|
iconDescription: String,
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
|
extraPadding: ExtraPadding,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -58,7 +59,7 @@ fun TimelineItemInformativeView(
|
||||||
fontStyle = FontStyle.Italic,
|
fontStyle = FontStyle.Italic,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
text = text
|
text = text + extraPadding.str
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -76,6 +77,7 @@ private fun ContentToPreview() {
|
||||||
TimelineItemInformativeView(
|
TimelineItemInformativeView(
|
||||||
text = "Info",
|
text = "Info",
|
||||||
iconDescription = "",
|
iconDescription = "",
|
||||||
icon = Icons.Default.Delete
|
icon = Icons.Default.Delete,
|
||||||
|
extraPadding = noExtraPadding,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,19 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemRedactedView(
|
fun TimelineItemRedactedView(
|
||||||
content: TimelineItemRedactedContent,
|
content: TimelineItemRedactedContent,
|
||||||
|
extraPadding: ExtraPadding,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
TimelineItemInformativeView(
|
TimelineItemInformativeView(
|
||||||
text = stringResource(id = StringR.string.common_message_removed),
|
text = stringResource(id = CommonStrings.common_message_removed),
|
||||||
iconDescription = stringResource(id = StringR.string.common_message_removed),
|
iconDescription = stringResource(id = CommonStrings.common_message_removed),
|
||||||
icon = Icons.Default.Delete,
|
icon = Icons.Default.Delete,
|
||||||
|
extraPadding = extraPadding,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -53,5 +54,8 @@ internal fun TimelineItemRedactedViewDarkPreview() =
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
TimelineItemRedactedView(TimelineItemRedactedContent)
|
TimelineItemRedactedView(
|
||||||
|
TimelineItemRedactedContent,
|
||||||
|
extraPadding = noExtraPadding
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ import android.text.util.Linkify.PHONE_NUMBERS
|
||||||
import android.text.util.Linkify.WEB_URLS
|
import android.text.util.Linkify.WEB_URLS
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -29,38 +32,46 @@ import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.text.util.LinkifyCompat
|
import androidx.core.text.util.LinkifyCompat
|
||||||
import io.element.android.features.messages.impl.timeline.components.html.HtmlDocument
|
import io.element.android.features.messages.impl.timeline.components.html.HtmlDocument
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentProvider
|
||||||
import io.element.android.libraries.designsystem.LinkColor
|
|
||||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.theme.LinkColor
|
||||||
|
import io.element.android.libraries.designsystem.text.toAnnotatedString
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemTextView(
|
fun TimelineItemTextView(
|
||||||
content: TimelineItemTextBasedContent,
|
content: TimelineItemTextBasedContent,
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
|
extraPadding: ExtraPadding,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onTextClicked: () -> Unit = {},
|
onTextClicked: () -> Unit = {},
|
||||||
onTextLongClicked: () -> Unit = {},
|
onTextLongClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val htmlDocument = content.htmlDocument
|
val htmlDocument = content.htmlDocument
|
||||||
if (htmlDocument != null) {
|
if (htmlDocument != null) {
|
||||||
HtmlDocument(
|
// For now we ignore the extra padding for html content, so add some spacing
|
||||||
document = htmlDocument,
|
// below the content (as previous behavior)
|
||||||
modifier = modifier,
|
Column(modifier = modifier) {
|
||||||
onTextClicked = onTextClicked,
|
HtmlDocument(
|
||||||
onTextLongClicked = onTextLongClicked,
|
document = htmlDocument,
|
||||||
interactionSource = interactionSource
|
modifier = Modifier,
|
||||||
)
|
onTextClicked = onTextClicked,
|
||||||
|
onTextLongClicked = onTextLongClicked,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
val linkStyle = SpanStyle(
|
val linkStyle = SpanStyle(
|
||||||
color = LinkColor,
|
color = LinkColor,
|
||||||
)
|
)
|
||||||
val styledText = remember(content.body) { content.body.linkify(linkStyle) }
|
val styledText = remember(content.body) { content.body.linkify(linkStyle) + extraPadding.str.toAnnotatedString() }
|
||||||
ClickableLinkText(
|
ClickableLinkText(
|
||||||
text = styledText,
|
text = styledText,
|
||||||
linkAnnotationTag = "URL",
|
linkAnnotationTag = "URL",
|
||||||
|
|
@ -109,6 +120,10 @@ internal fun TimelineItemTextViewDarkPreview(@PreviewParameter(TimelineItemTextB
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContentToPreview(content: TimelineItemTextBasedContent) {
|
fun ContentToPreview(content: TimelineItemTextBasedContent) {
|
||||||
TimelineItemTextView(content, MutableInteractionSource())
|
TimelineItemTextView(
|
||||||
|
content = content,
|
||||||
|
interactionSource = MutableInteractionSource(),
|
||||||
|
extraPadding = ExtraPadding(" (padding)"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,19 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemUnknownView(
|
fun TimelineItemUnknownView(
|
||||||
content: TimelineItemUnknownContent,
|
content: TimelineItemUnknownContent,
|
||||||
|
extraPadding: ExtraPadding,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
TimelineItemInformativeView(
|
TimelineItemInformativeView(
|
||||||
text = stringResource(id = StringR.string.common_unsupported_event),
|
text = stringResource(id = CommonStrings.common_unsupported_event),
|
||||||
iconDescription = stringResource(id = StringR.string.dialog_title_warning),
|
iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
|
||||||
icon = Icons.Default.Info,
|
icon = Icons.Default.Info,
|
||||||
|
extraPadding = extraPadding,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -53,5 +54,8 @@ internal fun TimelineItemUnknownViewDarkPreview() =
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
TimelineItemUnknownView(TimelineItemUnknownContent)
|
TimelineItemUnknownView(
|
||||||
|
content = TimelineItemUnknownContent,
|
||||||
|
extraPadding = noExtraPadding
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
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.Surface
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
@ -54,7 +54,7 @@ fun GroupHeaderView(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val backgroundColor = if (isHighlighted) {
|
val backgroundColor = if (isHighlighted) {
|
||||||
ElementTheme.colors.messageHighlightedBackground
|
ElementTheme.legacyColors.messageHighlightedBackground
|
||||||
} else {
|
} else {
|
||||||
Color.Companion.Transparent
|
Color.Companion.Transparent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import io.element.android.libraries.designsystem.LinkColor
|
|
||||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -55,6 +54,7 @@ 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.components.Text
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
|
import io.element.android.libraries.theme.LinkColor
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
|
@ -535,7 +535,7 @@ private fun AnnotatedString.Builder.appendLink(link: Element) {
|
||||||
val permalinkData = PermalinkParser.parse(uriString)
|
val permalinkData = PermalinkParser.parse(uriString)
|
||||||
when (permalinkData) {
|
when (permalinkData) {
|
||||||
is PermalinkData.FallbackLink -> {
|
is PermalinkData.FallbackLink -> {
|
||||||
pushStringAnnotation(tag = "URL", annotation = link.ownText())
|
pushStringAnnotation(tag = "URL", annotation = permalinkData.uri.toString())
|
||||||
withStyle(
|
withStyle(
|
||||||
style = SpanStyle(color = LinkColor)
|
style = SpanStyle(color = LinkColor)
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.SheetState
|
import androidx.compose.material3.SheetState
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
|
@ -37,7 +38,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.features.messages.impl.R
|
import io.element.android.features.messages.impl.R
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -129,7 +129,7 @@ private fun ColumnScope.RetrySendMenuContents(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(stringResource(R.string.screen_room_retry_send_menu_remove_action))
|
Text(stringResource(R.string.screen_room_retry_send_menu_remove_action))
|
||||||
},
|
},
|
||||||
colors = ListItemDefaults.colors(headlineColor = LocalColors.current.textActionCritical),
|
colors = ListItemDefaults.colors(headlineColor = MaterialTheme.colorScheme.error),
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
sheetState.hide()
|
sheetState.hide()
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -39,30 +40,31 @@ internal fun TimelineItemDaySeparatorView(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.padding(8.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = model.formattedDate,
|
text = model.formattedDate,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineItemDaySeparatorViewLightPreview(@PreviewParameter(
|
internal fun TimelineItemDaySeparatorViewLightPreview(
|
||||||
TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
@PreviewParameter(TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
||||||
) =
|
) =
|
||||||
ElementPreviewLight { ContentToPreview(model) }
|
ElementPreviewLight { ContentToPreview(model) }
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineItemDaySeparatorViewDarkPreview(@PreviewParameter(
|
internal fun TimelineItemDaySeparatorViewDarkPreview(
|
||||||
TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
@PreviewParameter(TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
||||||
) =
|
) =
|
||||||
ElementPreviewDark { ContentToPreview(model) }
|
ElementPreviewDark { ContentToPreview(model) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemReac
|
||||||
import io.element.android.libraries.core.bool.orTrue
|
import io.element.android.libraries.core.bool.orTrue
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
|
|
@ -34,6 +35,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineItemEventFactory @Inject constructor(
|
class TimelineItemEventFactory @Inject constructor(
|
||||||
private val contentFactory: TimelineItemContentFactory,
|
private val contentFactory: TimelineItemContentFactory,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
|
|
@ -67,7 +69,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
id = currentSender.value,
|
id = currentSender.value,
|
||||||
name = senderDisplayName ?: currentSender.value,
|
name = senderDisplayName ?: currentSender.value,
|
||||||
url = senderAvatarUrl,
|
url = senderAvatarUrl,
|
||||||
size = AvatarSize.SMALL
|
size = AvatarSize.TimelineSender
|
||||||
)
|
)
|
||||||
return TimelineItem.Event(
|
return TimelineItem.Event(
|
||||||
id = currentTimelineItem.uniqueId,
|
id = currentTimelineItem.uniqueId,
|
||||||
|
|
@ -89,8 +91,13 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
|
|
||||||
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
|
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
|
||||||
val aggregatedReactions = event.reactions.map {
|
val aggregatedReactions = event.reactions.map {
|
||||||
AggregatedReaction(key = it.key, count = it.count.toString(), isHighlighted = false)
|
AggregatedReaction(
|
||||||
|
key = it.key,
|
||||||
|
count = it.count.toInt(),
|
||||||
|
isHighlighted = it.senderIds.contains(matrixClient.sessionId),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
aggregatedReactions.sortedByDescending { it.count }
|
||||||
return TimelineItemReactions(aggregatedReactions.toImmutableList())
|
return TimelineItemReactions(aggregatedReactions.toImmutableList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,13 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.model
|
package io.element.android.features.messages.impl.timeline.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property key the reaction key (e.g. "👍")
|
||||||
|
* @property count the number of users who reacted with this key
|
||||||
|
* @property isHighlighted true if the reaction has (also) been sent by the current user.
|
||||||
|
*/
|
||||||
data class AggregatedReaction(
|
data class AggregatedReaction(
|
||||||
val key: String,
|
val key: String,
|
||||||
val count: String,
|
val count: Int,
|
||||||
val isHighlighted: Boolean = false
|
val isHighlighted: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,20 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
open class AggregatedReactionProvider : PreviewParameterProvider<AggregatedReaction> {
|
open class AggregatedReactionProvider : PreviewParameterProvider<AggregatedReaction> {
|
||||||
override val values: Sequence<AggregatedReaction>
|
override val values: Sequence<AggregatedReaction>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(false, true).flatMap {
|
||||||
anAggregatedReaction(),
|
sequenceOf(
|
||||||
anAggregatedReaction().copy(count = "88"),
|
anAggregatedReaction(isHighlighted = it),
|
||||||
anAggregatedReaction().copy(isHighlighted = true),
|
anAggregatedReaction(isHighlighted = it, count = 88),
|
||||||
anAggregatedReaction().copy(count = "88", isHighlighted = true),
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun anAggregatedReaction() = AggregatedReaction(
|
fun anAggregatedReaction(
|
||||||
key = "👍",
|
key: String = "👍",
|
||||||
count = "1", // TODO Why is it a String?
|
count: Int = 1,
|
||||||
isHighlighted = false,
|
isHighlighted: Boolean = false,
|
||||||
|
) = AggregatedReaction(
|
||||||
|
key = key,
|
||||||
|
count = count,
|
||||||
|
isHighlighted = isHighlighted,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,11 @@ class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEv
|
||||||
class TimelineItemTextBasedContentProvider : PreviewParameterProvider<TimelineItemTextBasedContent> {
|
class TimelineItemTextBasedContentProvider : PreviewParameterProvider<TimelineItemTextBasedContent> {
|
||||||
override val values = sequenceOf(
|
override val values = sequenceOf(
|
||||||
aTimelineItemEmoteContent(),
|
aTimelineItemEmoteContent(),
|
||||||
aTimelineItemEmoteContent().copy(htmlDocument = Jsoup.parse("Emote")),
|
aTimelineItemEmoteContent().copy(htmlDocument = Jsoup.parse("Emote Document")),
|
||||||
aTimelineItemNoticeContent(),
|
aTimelineItemNoticeContent(),
|
||||||
aTimelineItemNoticeContent().copy(htmlDocument = Jsoup.parse("Notice")),
|
aTimelineItemNoticeContent().copy(htmlDocument = Jsoup.parse("Notice Document")),
|
||||||
aTimelineItemTextContent(),
|
aTimelineItemTextContent(),
|
||||||
aTimelineItemTextContent().copy(htmlDocument = Jsoup.parse("Text")),
|
aTimelineItemTextContent().copy(htmlDocument = Jsoup.parse("Text Document")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ open class TimelineItemFileContentProvider : PreviewParameterProvider<TimelineIt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTimelineItemFileContent(fileName: String) = TimelineItemFileContent(
|
fun aTimelineItemFileContent(fileName: String = "A file.pdf") = TimelineItemFileContent(
|
||||||
body = fileName,
|
body = fileName,
|
||||||
thumbnailSource = MediaSource(url = ""),
|
thumbnailSource = MediaSource(url = ""),
|
||||||
fileSource = MediaSource(url = ""),
|
fileSource = MediaSource(url = ""),
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ 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.TimelineItemVideoContent
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.ui.strings.R
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(RoomScope::class)
|
@ContributesBinding(RoomScope::class)
|
||||||
|
|
@ -42,12 +42,12 @@ class MessageSummaryFormatterImpl @Inject constructor(
|
||||||
is TimelineItemTextBasedContent -> event.content.body
|
is TimelineItemTextBasedContent -> event.content.body
|
||||||
is TimelineItemStateContent -> event.content.body
|
is TimelineItemStateContent -> event.content.body
|
||||||
is TimelineItemProfileChangeContent -> event.content.body
|
is TimelineItemProfileChangeContent -> event.content.body
|
||||||
is TimelineItemEncryptedContent -> context.getString(R.string.common_unable_to_decrypt)
|
is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt)
|
||||||
is TimelineItemRedactedContent -> context.getString(R.string.common_message_removed)
|
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
|
||||||
is TimelineItemUnknownContent -> context.getString(R.string.common_unsupported_event)
|
is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event)
|
||||||
is TimelineItemImageContent -> context.getString(R.string.common_image)
|
is TimelineItemImageContent -> context.getString(CommonStrings.common_image)
|
||||||
is TimelineItemVideoContent -> context.getString(R.string.common_video)
|
is TimelineItemVideoContent -> context.getString(CommonStrings.common_video)
|
||||||
is TimelineItemFileContent -> context.getString(R.string.common_file)
|
is TimelineItemFileContent -> context.getString(CommonStrings.common_file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ class MessagesPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle sending a reaction`() = runTest {
|
fun `present - handle toggling a reaction`() = runTest {
|
||||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = createMessagePresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
val presenter = createMessagePresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
||||||
|
|
@ -89,17 +89,35 @@ class MessagesPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(MessagesEvents.SendReaction("👍", AN_EVENT_ID))
|
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||||
assertThat(room.sendReactionCount).isEqualTo(1)
|
assertThat(room.myReactions.count()).isEqualTo(1)
|
||||||
|
|
||||||
// No crashes when sending a reaction failed
|
// No crashes when sending a reaction failed
|
||||||
room.givenSendReactionResult(Result.failure(IllegalStateException("Failed to send reaction")))
|
room.givenToggleReactionResult(Result.failure(IllegalStateException("Failed to send reaction")))
|
||||||
initialState.eventSink.invoke(MessagesEvents.SendReaction("👍", AN_EVENT_ID))
|
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||||
assertThat(room.sendReactionCount).isEqualTo(2)
|
assertThat(room.myReactions.count()).isEqualTo(1)
|
||||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle toggling a reaction twice`() = runTest {
|
||||||
|
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val presenter = createMessagePresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||||
|
assertThat(room.myReactions.count()).isEqualTo(1)
|
||||||
|
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||||
|
assertThat(room.myReactions.count()).isEqualTo(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle action forward`() = runTest {
|
fun `present - handle action forward`() = runTest {
|
||||||
val navigator = FakeMessagesNavigator()
|
val navigator = FakeMessagesNavigator()
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,12 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.fixtures
|
package io.element.android.features.messages.fixtures
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||||
|
|
@ -30,7 +31,6 @@ import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.room.aTimelineItemDebugInfo
|
import io.element.android.libraries.matrix.test.room.aTimelineItemDebugInfo
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
|
|
||||||
internal fun aMessageEvent(
|
internal fun aMessageEvent(
|
||||||
eventId: EventId? = AN_EVENT_ID,
|
eventId: EventId? = AN_EVENT_ID,
|
||||||
|
|
@ -43,11 +43,11 @@ internal fun aMessageEvent(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
senderId = A_USER_ID,
|
senderId = A_USER_ID,
|
||||||
senderDisplayName = A_USER_NAME,
|
senderDisplayName = A_USER_NAME,
|
||||||
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
|
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME, size = AvatarSize.TimelineSender),
|
||||||
content = content,
|
content = content,
|
||||||
sentTime = "",
|
sentTime = "",
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
reactionsState = TimelineItemReactions(persistentListOf()),
|
reactionsState = aTimelineItemReactions(count = 0),
|
||||||
sendState = EventSendState.Sent(AN_EVENT_ID),
|
sendState = EventSendState.Sent(AN_EVENT_ID),
|
||||||
inReplyTo = inReplyTo,
|
inReplyTo = inReplyTo,
|
||||||
debugInfo = debugInfo,
|
debugInfo = debugInfo,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
||||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||||
return TimelineItemsFactory(
|
return TimelineItemsFactory(
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
eventItemFactory = TimelineItemEventFactory(
|
eventItemFactory = TimelineItemEventFactory(
|
||||||
TimelineItemContentFactory(
|
contentFactory = TimelineItemContentFactory(
|
||||||
messageFactory = TimelineItemContentMessageFactory(FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation()),
|
messageFactory = TimelineItemContentMessageFactory(FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation()),
|
||||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||||
stickerFactory = TimelineItemContentStickerFactory(),
|
stickerFactory = TimelineItemContentStickerFactory(),
|
||||||
|
|
@ -54,7 +55,8 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||||
stateFactory = TimelineItemContentStateFactory(timelineEventFormatter),
|
stateFactory = TimelineItemContentStateFactory(timelineEventFormatter),
|
||||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
||||||
)
|
),
|
||||||
|
matrixClient = FakeMatrixClient(),
|
||||||
),
|
),
|
||||||
virtualItemFactory = TimelineItemVirtualFactory(
|
virtualItemFactory = TimelineItemVirtualFactory(
|
||||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,9 @@ package io.element.android.features.messages.timeline.groups
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.messages.fixtures.aMessageEvent
|
import io.element.android.features.messages.fixtures.aMessageEvent
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
|
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
|
||||||
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||||
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||||
|
|
@ -42,7 +41,7 @@ class TimelineItemGrouperTest {
|
||||||
senderAvatar = anAvatarData(),
|
senderAvatar = anAvatarData(),
|
||||||
senderDisplayName = "",
|
senderDisplayName = "",
|
||||||
content = TimelineItemStateEventContent(body = "a state event"),
|
content = TimelineItemStateEventContent(body = "a state event"),
|
||||||
reactionsState = TimelineItemReactions(emptyList<AggregatedReaction>().toImmutableList()),
|
reactionsState = aTimelineItemReactions(count = 0),
|
||||||
sendState = EventSendState.Sent(AN_EVENT_ID),
|
sendState = EventSendState.Sent(AN_EVENT_ID),
|
||||||
inReplyTo = null,
|
inReplyTo = null,
|
||||||
debugInfo = aTimelineItemDebugInfo(),
|
debugInfo = aTimelineItemDebugInfo(),
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectivityIndicatorView(
|
fun ConnectivityIndicatorView(
|
||||||
|
|
@ -83,7 +82,7 @@ private fun Indicator(modifier: Modifier = Modifier) {
|
||||||
Row(
|
Row(
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(LocalColors.current.gray400)
|
.background(MaterialTheme.colorScheme.secondaryContainer)
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
.padding(vertical = 6.dp),
|
.padding(vertical = 6.dp),
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
|
@ -98,7 +97,7 @@ private fun Indicator(modifier: Modifier = Modifier) {
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(StringR.string.common_offline),
|
text = stringResource(CommonStrings.common_offline),
|
||||||
style = ElementTextStyles.Regular.bodyMD.copy(fontWeight = FontWeight.Medium),
|
style = ElementTextStyles.Regular.bodyMD.copy(fontWeight = FontWeight.Medium),
|
||||||
color = tint,
|
color = tint,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue