Merge branch 'develop' into feature/fga/room_list_api
This commit is contained in:
commit
8e5c2a749a
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"
|
||||
],
|
||||
"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
|
||||
- uses: rnkdsh/action-upload-diawi@v1.5.0
|
||||
id: diawi
|
||||
# Do not fail the whole build if Diawi upload fails
|
||||
continue-on-error: true
|
||||
env:
|
||||
token: ${{ secrets.DIAWI_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
|
||||
path: |
|
||||
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:
|
||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||
app-file: app/build/outputs/apk/debug/app-universal-debug.apk
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<!--- TOC -->
|
||||
|
||||
* [Contributing code to Matrix](#contributing-code-to-matrix)
|
||||
* [Developer onboarding](#developer-onboarding)
|
||||
* [Android Studio settings](#android-studio-settings)
|
||||
* [Compilation](#compilation)
|
||||
* [Strings](#strings)
|
||||
|
|
@ -32,6 +33,10 @@ Element X Android support can be found in this room: [.
|
||||
|
||||
## 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`).
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
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 io.element.android.libraries.architecture.bindings
|
||||
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.x.di.AppBindings
|
||||
import io.element.android.x.intent.SafeUriHandler
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("MainActivity")
|
||||
|
|
@ -63,6 +65,7 @@ class MainActivity : NodeComponentActivity() {
|
|||
ElementTheme {
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
|
||||
LocalUriHandler provides SafeUriHandler(this),
|
||||
) {
|
||||
Box(
|
||||
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>
|
||||
</style>
|
||||
|
||||
<style name="Theme.ElementX" parent="Theme.ElementAndroid" />
|
||||
<style name="Theme.ElementX" parent="Theme.Material3.Dark" />
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,5 +21,5 @@
|
|||
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.ElementX</item>
|
||||
</style>
|
||||
<style name="Theme.ElementX" parent="Theme.ElementAndroid" />
|
||||
<style name="Theme.ElementX" parent="Theme.Material3.Light" />
|
||||
</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.verification.SessionVerificationService
|
||||
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.Job
|
||||
import kotlinx.coroutines.flow.drop
|
||||
|
|
@ -48,13 +48,13 @@ class LoggedInEventProcessor @Inject constructor(
|
|||
fun observeEvents(coroutineScope: CoroutineScope) {
|
||||
observingJob = coroutineScope.launch {
|
||||
displayLeftRoomMessage.onEach {
|
||||
displayMessage(R.string.common_current_user_left_room)
|
||||
displayMessage(CommonStrings.common_current_user_left_room)
|
||||
}.launchIn(this)
|
||||
|
||||
displayVerificationSuccessfulMessage
|
||||
.drop(1)
|
||||
.onEach {
|
||||
displayMessage(R.string.common_verification_complete)
|
||||
displayMessage(CommonStrings.common_verification_complete)
|
||||
}.launchIn(this)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,6 +257,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
override fun onRoomSettingsClicked(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomFlowNode.NavTarget.RoomDetails))
|
||||
}
|
||||
|
||||
override fun onReportBugClicked() {
|
||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||
}
|
||||
}
|
||||
roomListEntryPoint
|
||||
.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.PreviewParameter
|
||||
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.PreferenceSwitch
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
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
|
||||
fun AnalyticsPreferencesView(
|
||||
|
|
@ -43,11 +43,11 @@ fun AnalyticsPreferencesView(
|
|||
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
|
||||
}
|
||||
|
||||
PreferenceCategory(title = stringResource(id = StringR.string.screen_analytics_settings_share_data)) {
|
||||
val firstPart = stringResource(id = StringR.string.screen_analytics_settings_help_us_improve, state.applicationName)
|
||||
PreferenceCategory(title = stringResource(id = CommonStrings.screen_analytics_settings_share_data)) {
|
||||
val firstPart = stringResource(id = CommonStrings.screen_analytics_settings_help_us_improve, state.applicationName)
|
||||
val secondPart = buildAnnotatedStringWithColoredPart(
|
||||
StringR.string.screen_analytics_settings_read_terms,
|
||||
StringR.string.screen_analytics_settings_read_terms_content_link
|
||||
CommonStrings.screen_analytics_settings_read_terms,
|
||||
CommonStrings.screen_analytics_settings_read_terms_content_link
|
||||
)
|
||||
val title = "$firstPart\n\n$secondPart"
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ dependencies {
|
|||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.features.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.ElementPreviewLight
|
||||
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.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
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
|
||||
fun AnalyticsOptInView(
|
||||
|
|
@ -162,7 +162,7 @@ private fun AnalyticsOptInContentRow(
|
|||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = LocalColors.current.quinary,
|
||||
color = ElementTheme.legacyColors.quinary,
|
||||
shape = bgShape,
|
||||
)
|
||||
.padding(vertical = 12.dp, horizontal = 20.dp),
|
||||
|
|
@ -174,8 +174,7 @@ private fun AnalyticsOptInContentRow(
|
|||
.padding(2.dp),
|
||||
imageVector = Icons.Rounded.Check,
|
||||
contentDescription = null,
|
||||
// TODO Compound, this color is not yet in the theme
|
||||
tint = Color(0xFF007A61)
|
||||
tint = ElementTheme.colors.iconSuccessPrimary,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
|
|
@ -199,13 +198,13 @@ private fun AnalyticsOptInFooter(
|
|||
onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(text = stringResource(id = StringR.string.action_ok))
|
||||
Text(text = stringResource(id = CommonStrings.action_ok))
|
||||
}
|
||||
TextButton(
|
||||
onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) },
|
||||
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.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.deeplink)
|
||||
implementation(projects.libraries.mediapickers.api)
|
||||
implementation(projects.libraries.mediaupload.api)
|
||||
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.Text
|
||||
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)
|
||||
@Composable
|
||||
|
|
@ -107,7 +107,7 @@ fun AddPeopleViewTopBar(
|
|||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
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 = stringResource(id = textActionResId),
|
||||
fontSize = 16.sp,
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.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.CheckableUnresolvedUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
|
|
@ -40,7 +40,7 @@ fun SearchMultipleUsersResultItem(
|
|||
CheckableUnresolvedUserRow(
|
||||
checked = isUserSelected,
|
||||
modifier = modifier,
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)),
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = searchResult.matrixUser.userId.value,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
|
|
@ -49,7 +49,7 @@ fun SearchMultipleUsersResultItem(
|
|||
checked = isUserSelected,
|
||||
modifier = modifier,
|
||||
matrixUser = searchResult.matrixUser,
|
||||
avatarSize = AvatarSize.Custom(36.dp),
|
||||
avatarSize = AvatarSize.UserListItem,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
|
|
@ -63,8 +63,11 @@ internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { Con
|
|||
private fun ContentToPreview() {
|
||||
Column {
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
|
||||
Divider()
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true)
|
||||
Divider()
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false)
|
||||
Divider()
|
||||
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.ui.Modifier
|
||||
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.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.UnresolvedUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
|
|
@ -39,26 +39,27 @@ fun SearchSingleUserResultItem(
|
|||
if (searchResult.isUnresolved) {
|
||||
UnresolvedUserRow(
|
||||
modifier = modifier.clickable(onClick = onClick),
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)),
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = searchResult.matrixUser.userId.value,
|
||||
)
|
||||
} else {
|
||||
MatrixUserRow(
|
||||
modifier = modifier.clickable(onClick = onClick),
|
||||
matrixUser = searchResult.matrixUser,
|
||||
avatarSize = AvatarSize.Custom(36.dp),
|
||||
avatarSize = AvatarSize.UserListItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview{ ContentToPreview() }
|
||||
internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column {
|
||||
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
|
||||
Divider()
|
||||
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.matrix.api.user.MatrixUser
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ fun SearchUserBar(
|
|||
isMultiSelectionEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
showBackButton: Boolean = true,
|
||||
placeHolderTitle: String = stringResource(R.string.common_search_for_someone),
|
||||
placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
|
||||
onActiveChanged: (Boolean) -> Unit = {},
|
||||
onTextChanged: (String) -> 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.libraries.architecture.Async
|
||||
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.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -131,7 +131,7 @@ class ConfigureRoomPresenter @Inject constructor(
|
|||
dataStore.clearCachedData()
|
||||
analyticsService.capture(CreatedRoom(isDM = false))
|
||||
}
|
||||
}.execute(createRoomAction)
|
||||
}.runCatchingUpdatingState(createRoomAction)
|
||||
}
|
||||
|
||||
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.SelectedUsersList
|
||||
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.launch
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
|
|
@ -84,7 +84,7 @@ fun ConfigureRoomView(
|
|||
|
||||
if (state.createRoomAction is Async.Success) {
|
||||
LaunchedEffect(state.createRoomAction) {
|
||||
onRoomCreated(state.createRoomAction.state)
|
||||
onRoomCreated(state.createRoomAction.data)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ fun ConfigureRoomView(
|
|||
|
||||
when (state.createRoomAction) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog(text = stringResource(StringR.string.common_creating_room))
|
||||
ProgressDialog(text = stringResource(CommonStrings.common_creating_room))
|
||||
}
|
||||
|
||||
is Async.Failure -> {
|
||||
|
|
@ -198,7 +198,7 @@ fun ConfigureRoomToolbar(
|
|||
onClick = onNextPressed,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(StringR.string.action_create),
|
||||
text = stringResource(CommonStrings.action_create),
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
}
|
||||
|
|
@ -227,7 +227,7 @@ fun RoomNameWithAvatar(
|
|||
LabelledTextField(
|
||||
label = stringResource(R.string.screen_create_room_room_name_label),
|
||||
value = roomName,
|
||||
placeholder = stringResource(StringR.string.common_room_name_placeholder),
|
||||
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
|
||||
singleLine = true,
|
||||
onValueChange = onRoomNameChanged,
|
||||
)
|
||||
|
|
@ -244,7 +244,7 @@ fun RoomTopic(
|
|||
modifier = modifier,
|
||||
label = stringResource(R.string.screen_create_room_topic_label),
|
||||
value = topic,
|
||||
placeholder = stringResource(StringR.string.common_topic_placeholder),
|
||||
placeholder = stringResource(CommonStrings.common_topic_placeholder),
|
||||
onValueChange = onTopicChanged,
|
||||
maxLines = 3,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import android.content.Context
|
||||
import android.app.Activity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
|
@ -29,24 +29,18 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
||||
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.permalink.PermalinkBuilder
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class CreateRoomRootNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: CreateRoomRootPresenter,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
interface Callback : Plugin {
|
||||
|
|
@ -73,31 +67,18 @@ class CreateRoomRootNode @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val context = LocalContext.current
|
||||
val activity = LocalContext.current as Activity
|
||||
CreateRoomRootView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onClosePressed = this::navigateUp,
|
||||
onNewRoomClicked = callback::onCreateNewRoom,
|
||||
onOpenDM = callback::onStartChatSuccess,
|
||||
onInviteFriendsClicked = { invitePeople(context) },
|
||||
onInviteFriendsClicked = { invitePeople(activity) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun invitePeople(context: Context) {
|
||||
val permalinkResult = PermalinkBuilder.permalinkForUser(matrixClient.sessionId)
|
||||
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)
|
||||
}
|
||||
private fun invitePeople(activity: Activity) {
|
||||
inviteFriendsUseCase.execute(activity)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.libraries.architecture.Async
|
||||
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.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -95,6 +95,6 @@ class CreateRoomRootPresenter @Inject constructor(
|
|||
suspend {
|
||||
matrixClient.createDM(user.userId).getOrThrow()
|
||||
.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.Text
|
||||
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.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
|
@ -69,7 +69,7 @@ fun CreateRoomRootView(
|
|||
) {
|
||||
if (state.startDmAction is Async.Success) {
|
||||
LaunchedEffect(state.startDmAction) {
|
||||
onOpenDM(state.startDmAction.state)
|
||||
onOpenDM(state.startDmAction.data)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ fun CreateRoomRootView(
|
|||
|
||||
when (state.startDmAction) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog(text = stringResource(id = StringR.string.common_starting_chat))
|
||||
ProgressDialog(text = stringResource(id = CommonStrings.common_starting_chat))
|
||||
}
|
||||
|
||||
is Async.Failure -> {
|
||||
|
|
@ -137,7 +137,7 @@ fun CreateRoomRootViewTopBar(
|
|||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = StringR.string.action_start_chat),
|
||||
text = stringResource(id = CommonStrings.action_start_chat),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
|
|
@ -146,7 +146,7 @@ fun CreateRoomRootViewTopBar(
|
|||
IconButton(onClick = onClosePressed) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(id = StringR.string.action_close),
|
||||
contentDescription = stringResource(id = CommonStrings.action_close),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
|
@ -169,7 +169,7 @@ fun CreateRoomActionButtonsList(
|
|||
)
|
||||
CreateRoomActionButton(
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.services.analytics.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
@ -50,6 +51,7 @@ dependencies {
|
|||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.invitelist.test)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,16 +25,20 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||
import io.element.android.libraries.architecture.Async
|
||||
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.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
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.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -44,6 +48,7 @@ import javax.inject.Inject
|
|||
class InviteListPresenter @Inject constructor(
|
||||
private val client: MatrixClient,
|
||||
private val store: SeenInvitesStore,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<InviteListState> {
|
||||
|
||||
@Composable
|
||||
|
|
@ -133,9 +138,10 @@ class InviteListPresenter @Inject constructor(
|
|||
suspend {
|
||||
client.getRoom(roomId)?.use {
|
||||
it.acceptInvitation().getOrThrow()
|
||||
analyticsService.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
|
||||
}
|
||||
roomId
|
||||
}.execute(acceptedAction)
|
||||
}.runCatchingUpdatingState(acceptedAction)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch {
|
||||
|
|
@ -143,7 +149,7 @@ class InviteListPresenter @Inject constructor(
|
|||
client.getRoom(roomId)?.use {
|
||||
it.rejectInvitation().getOrThrow()
|
||||
} ?: Unit
|
||||
}.execute(declinedAction)
|
||||
}.runCatchingUpdatingState(declinedAction)
|
||||
}
|
||||
|
||||
private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run {
|
||||
|
|
@ -153,12 +159,14 @@ class InviteListPresenter @Inject constructor(
|
|||
id = i.userId.value,
|
||||
name = i.displayName,
|
||||
url = i.avatarUrl,
|
||||
size = AvatarSize.RoomInviteItem,
|
||||
)
|
||||
else
|
||||
AvatarData(
|
||||
id = roomId.value,
|
||||
name = name,
|
||||
url = avatarURLString
|
||||
url = avatarURLString,
|
||||
size = AvatarSize.RoomInviteItem,
|
||||
)
|
||||
|
||||
val alias = if (isDirect)
|
||||
|
|
@ -181,6 +189,7 @@ class InviteListPresenter @Inject constructor(
|
|||
id = userId.value,
|
||||
name = displayName,
|
||||
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.TopAppBar
|
||||
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
|
||||
fun InviteListView(
|
||||
|
|
@ -58,7 +58,7 @@ fun InviteListView(
|
|||
) {
|
||||
if (state.acceptedAction is Async.Success) {
|
||||
LaunchedEffect(state.acceptedAction) {
|
||||
onInviteAccepted(state.acceptedAction.state)
|
||||
onInviteAccepted(state.acceptedAction.data)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,8 +82,8 @@ fun InviteListView(
|
|||
ConfirmationDialog(
|
||||
content = stringResource(contentResource, state.declineConfirmationDialog.name),
|
||||
title = stringResource(titleResource),
|
||||
submitText = stringResource(StringR.string.action_decline),
|
||||
cancelText = stringResource(StringR.string.action_cancel),
|
||||
submitText = stringResource(CommonStrings.action_decline),
|
||||
cancelText = stringResource(CommonStrings.action_cancel),
|
||||
emphasizeSubmitButton = true,
|
||||
onSubmitClicked = { state.eventSink(InviteListEvents.ConfirmDeclineInvite) },
|
||||
onDismiss = { state.eventSink(InviteListEvents.CancelDeclineInvite) }
|
||||
|
|
@ -92,18 +92,18 @@ fun InviteListView(
|
|||
|
||||
if (state.acceptedAction is Async.Failure) {
|
||||
ErrorDialog(
|
||||
content = stringResource(StringR.string.error_unknown),
|
||||
title = stringResource(StringR.string.common_error),
|
||||
submitText = stringResource(StringR.string.action_ok),
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
title = stringResource(CommonStrings.common_error),
|
||||
submitText = stringResource(CommonStrings.action_ok),
|
||||
onDismiss = { state.eventSink(InviteListEvents.DismissAcceptError) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.declinedAction is Async.Failure) {
|
||||
ErrorDialog(
|
||||
content = stringResource(StringR.string.error_unknown),
|
||||
title = stringResource(StringR.string.common_error),
|
||||
submitText = stringResource(StringR.string.action_ok),
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
title = stringResource(CommonStrings.common_error),
|
||||
submitText = stringResource(CommonStrings.action_ok),
|
||||
onDismiss = { state.eventSink(InviteListEvents.DismissDeclineError) }
|
||||
)
|
||||
}
|
||||
|
|
@ -124,7 +124,7 @@ fun InviteListContent(
|
|||
BackButton(onClick = onBackClicked)
|
||||
},
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.InviteSender
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
|
|
@ -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.noFontPadding
|
||||
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
|
||||
|
||||
|
|
@ -74,8 +71,8 @@ internal fun InviteSummaryRow(
|
|||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = minHeight)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = minHeight)
|
||||
) {
|
||||
DefaultInviteSummaryRow(
|
||||
invite = invite,
|
||||
|
|
@ -93,20 +90,20 @@ internal fun DefaultInviteSummaryRow(
|
|||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Avatar(
|
||||
invite.roomAvatarData.copy(size = AvatarSize.Custom(52.dp)),
|
||||
invite.roomAvatarData,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.alignByBaseline()
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.alignByBaseline()
|
||||
.weight(1f)
|
||||
) {
|
||||
val bonusPadding = if (invite.isNew) 12.dp else 0.dp
|
||||
|
||||
|
|
@ -143,31 +140,29 @@ internal fun DefaultInviteSummaryRow(
|
|||
// CTAs
|
||||
Row(Modifier.padding(top = 12.dp)) {
|
||||
OutlinedButton(
|
||||
content = { Text(stringResource(StringR.string.action_decline), style = ElementTextStyles.Button) },
|
||||
content = { Text(stringResource(CommonStrings.action_decline), style = ElementTextStyles.Button) },
|
||||
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),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Button(
|
||||
content = { Text(stringResource(StringR.string.action_accept), style = ElementTextStyles.Button) },
|
||||
content = { Text(stringResource(CommonStrings.action_accept), style = ElementTextStyles.Button) },
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val unreadIndicatorColor = if (invite.isNew) MaterialTheme.roomListUnreadIndicator() else Color.Transparent
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(unreadIndicatorColor)
|
||||
)
|
||||
UnreadIndicatorAtom(color = unreadIndicatorColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +173,7 @@ private fun SenderRow(sender: InviteSender) {
|
|||
modifier = Modifier.padding(top = 6.dp),
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
|
||||
avatarData = sender.avatarData,
|
||||
)
|
||||
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 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.UserId
|
||||
|
||||
|
|
@ -26,14 +27,14 @@ data class InviteListInviteSummary(
|
|||
val roomId: RoomId,
|
||||
val roomName: String = "",
|
||||
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 isDirect: Boolean = false,
|
||||
val isNew: Boolean = false,
|
||||
)
|
||||
|
||||
data class InviteSender(
|
||||
data class InviteSender constructor(
|
||||
val userId: UserId,
|
||||
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.turbine.test
|
||||
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.libraries.architecture.Async
|
||||
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.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
|
|
@ -50,6 +52,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
),
|
||||
FakeSeenInvitesStore(),
|
||||
FakeAnalyticsService(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -74,6 +77,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
),
|
||||
FakeSeenInvitesStore(),
|
||||
FakeAnalyticsService(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -88,6 +92,7 @@ class InviteListPresenterTests {
|
|||
id = A_USER_ID.value,
|
||||
name = A_USER_NAME,
|
||||
url = AN_AVATAR_URL,
|
||||
size = AvatarSize.RoomInviteItem,
|
||||
)
|
||||
)
|
||||
Truth.assertThat(withInviteState.inviteList[0].sender).isNull()
|
||||
|
|
@ -102,6 +107,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
),
|
||||
FakeSeenInvitesStore(),
|
||||
FakeAnalyticsService(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -115,6 +121,7 @@ class InviteListPresenterTests {
|
|||
id = A_USER_ID.value,
|
||||
name = A_USER_NAME,
|
||||
url = AN_AVATAR_URL,
|
||||
size = AvatarSize.InviteSender,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -128,6 +135,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
),
|
||||
FakeSeenInvitesStore(),
|
||||
FakeAnalyticsService(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -152,6 +160,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
),
|
||||
FakeSeenInvitesStore(),
|
||||
FakeAnalyticsService(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -176,6 +185,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
),
|
||||
FakeSeenInvitesStore(),
|
||||
FakeAnalyticsService(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -199,7 +209,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
@ -225,7 +235,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||
val ex = Throwable("Ruh roh!")
|
||||
room.givenRejectInviteResult(Result.failure(ex))
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
|
@ -256,7 +266,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||
val ex = Throwable("Ruh roh!")
|
||||
room.givenRejectInviteResult(Result.failure(ex))
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
|
@ -288,7 +298,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
@ -311,7 +321,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||
val ex = Throwable("Ruh roh!")
|
||||
room.givenAcceptInviteResult(Result.failure(ex))
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
|
@ -336,7 +346,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
|
||||
val ex = Throwable("Ruh roh!")
|
||||
room.givenAcceptInviteResult(Result.failure(ex))
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
|
@ -365,6 +375,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
),
|
||||
store,
|
||||
FakeAnalyticsService(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -401,6 +412,7 @@ class InviteListPresenterTests {
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
),
|
||||
store,
|
||||
FakeAnalyticsService(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
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.ElementPreviewLight
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun LeaveRoomView(
|
||||
|
|
@ -50,19 +49,19 @@ private fun LeaveRoomConfirmationDialog(
|
|||
when (state.confirmation) {
|
||||
is LeaveRoomState.Confirmation.Hidden -> {}
|
||||
is LeaveRoomState.Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
|
||||
text = StringR.string.leave_room_alert_private_subtitle,
|
||||
text = CommonStrings.leave_room_alert_private_subtitle,
|
||||
roomId = state.confirmation.roomId,
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
|
||||
is LeaveRoomState.Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog(
|
||||
text = StringR.string.leave_room_alert_empty_subtitle,
|
||||
text = CommonStrings.leave_room_alert_empty_subtitle,
|
||||
roomId = state.confirmation.roomId,
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
|
||||
is LeaveRoomState.Confirmation.Generic -> LeaveRoomConfirmationDialog(
|
||||
text = StringR.string.leave_room_alert_subtitle,
|
||||
text = CommonStrings.leave_room_alert_subtitle,
|
||||
roomId = state.confirmation.roomId,
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
|
|
@ -77,7 +76,7 @@ private fun LeaveRoomConfirmationDialog(
|
|||
) {
|
||||
ConfirmationDialog(
|
||||
content = stringResource(text),
|
||||
submitText = stringResource(R.string.action_leave),
|
||||
submitText = stringResource(CommonStrings.action_leave),
|
||||
onSubmitClicked = { eventSink(LeaveRoomEvent.LeaveRoom(roomId)) },
|
||||
onDismiss = { eventSink(LeaveRoomEvent.HideConfirmation) },
|
||||
)
|
||||
|
|
@ -90,7 +89,7 @@ private fun LeaveRoomProgressDialog(
|
|||
when (state.progress) {
|
||||
is LeaveRoomState.Progress.Hidden -> {}
|
||||
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) {
|
||||
is LeaveRoomState.Error.Hidden -> {}
|
||||
is LeaveRoomState.Error.Shown -> ErrorDialog(
|
||||
content = stringResource(StringR.string.error_unknown),
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
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.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.FloatingActionButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import timber.log.Timber
|
||||
|
|
@ -67,7 +67,7 @@ import kotlin.coroutines.suspendCoroutine
|
|||
fun MapView(
|
||||
modifier: Modifier = Modifier,
|
||||
mapState: MapState = rememberMapState(),
|
||||
darkMode: Boolean = !ElementTheme.colors.isLight,
|
||||
darkMode: Boolean = !ElementTheme.isLightTheme,
|
||||
onLocationClick: () -> Unit,
|
||||
) {
|
||||
// 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.rememberAsyncImagePainter
|
||||
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.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
|
||||
|
||||
/**
|
||||
|
|
@ -52,7 +52,7 @@ fun StaticMapView(
|
|||
zoom: Double,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
darkMode: Boolean = !ElementTheme.colors.isLight,
|
||||
darkMode: Boolean = !ElementTheme.isLightTheme,
|
||||
) {
|
||||
// Using BoxWithConstraints to:
|
||||
// 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.PreviewParameterProvider
|
||||
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.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.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.features.location.api.R
|
||||
import io.element.android.libraries.ui.strings.R as StringsR
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
internal fun StaticMapPlaceholder(
|
||||
showProgress: Boolean,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
darkMode: Boolean = !ElementTheme.colors.isLight,
|
||||
darkMode: Boolean = !ElementTheme.isLightTheme,
|
||||
onLoadMapClick: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
|
|
@ -76,7 +76,7 @@ internal fun StaticMapPlaceholder(
|
|||
imageVector = Icons.Default.Refresh,
|
||||
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.network)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(libs.androidx.browser)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.login.impl
|
|||
|
||||
import android.app.Activity
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
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.createNode
|
||||
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.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
|
|
@ -155,7 +156,7 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
activity = LocalContext.current as? Activity
|
||||
darkTheme = !ElementTheme.colors.isLight
|
||||
darkTheme = !ElementTheme.isLightTheme
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
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.libraries.architecture.Async
|
||||
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.matrix.api.auth.MatrixAuthenticationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -71,6 +71,6 @@ class ChangeServerPresenter @Inject constructor(
|
|||
// Valid, remember user choice
|
||||
accountProviderDataSource.userSelection(data)
|
||||
}.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 io.element.android.features.login.impl.R
|
||||
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
|
||||
internal fun SlidingSyncNotSupportedDialog(
|
||||
|
|
@ -32,11 +32,11 @@ internal fun SlidingSyncNotSupportedDialog(
|
|||
ConfirmationDialog(
|
||||
modifier = modifier,
|
||||
onDismiss = onDismiss,
|
||||
submitText = stringResource(StringR.string.action_learn_more),
|
||||
submitText = stringResource(CommonStrings.action_learn_more),
|
||||
onSubmitClicked = onLearnMoreClicked,
|
||||
onCancelClicked = onDismiss,
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.AuthenticationException
|
||||
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
|
||||
fun loginError(
|
||||
throwable: Throwable
|
||||
): Int {
|
||||
val authException = throwable as? AuthenticationException ?: return StringR.string.error_unknown
|
||||
val authException = throwable as? AuthenticationException ?: return CommonStrings.error_unknown
|
||||
return when (authException.errorCode) {
|
||||
AuthErrorCode.FORBIDDEN -> R.string.screen_login_error_invalid_credentials
|
||||
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.libraries.architecture.Async
|
||||
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.matrix.api.auth.MatrixAuthenticationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -95,6 +95,6 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
throw IllegalStateException("Unsupported login flow")
|
||||
}
|
||||
}.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.Success -> {
|
||||
when (val loginFlowState = state.loginFlow.state) {
|
||||
when (val loginFlowState = state.loginFlow.data) {
|
||||
is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
|
||||
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.testtags.TestTags
|
||||
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)
|
||||
@Composable
|
||||
|
|
@ -213,7 +213,7 @@ internal fun LoginForm(
|
|||
IconButton(onClick = {
|
||||
loginFieldState = ""
|
||||
}) {
|
||||
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear))
|
||||
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(CommonStrings.action_clear))
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
|
|
@ -248,7 +248,7 @@ internal fun LoginForm(
|
|||
val image =
|
||||
if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
|
||||
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 }) {
|
||||
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.testtags.TestTags
|
||||
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
|
||||
|
|
@ -143,7 +143,7 @@ fun SearchAccountProviderView(
|
|||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = stringResource(StringR.string.action_clear)
|
||||
contentDescription = stringResource(CommonStrings.action_clear)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -171,7 +171,7 @@ fun SearchAccountProviderView(
|
|||
}
|
||||
}
|
||||
is Async.Success -> {
|
||||
items(state.userInputResult.state) { homeserverData ->
|
||||
items(state.userInputResult.data) { homeserverData ->
|
||||
val item = homeserverData.toAccountProvider()
|
||||
AccountProviderView(
|
||||
item = item,
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ package io.element.android.features.login.impl.error
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import org.junit.Test
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
class ErrorFormatterTests {
|
||||
|
||||
|
|
@ -28,19 +28,19 @@ class ErrorFormatterTests {
|
|||
@Test
|
||||
fun `loginError - invalid unknown error returns unknown error message`() {
|
||||
val error = Throwable("Some unknown error")
|
||||
assertThat(loginError(error)).isEqualTo(StringR.string.error_unknown)
|
||||
assertThat(loginError(error)).isEqualTo(CommonStrings.error_unknown)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loginError - invalid auth error returns unknown error message`() {
|
||||
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
|
||||
fun `loginError - unknown error returns unknown error message`() {
|
||||
val error = AuthenticationException.Generic("M_UNKNOWN")
|
||||
assertThat(loginError(error)).isEqualTo(StringR.string.error_unknown)
|
||||
assertThat(loginError(error)).isEqualTo(CommonStrings.error_unknown)
|
||||
}
|
||||
|
||||
@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.preview.ElementPreviewDark
|
||||
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
|
||||
fun LogoutPreferenceView(
|
||||
|
|
@ -81,7 +81,7 @@ fun LogoutPreferenceView(
|
|||
fun LogoutPreferenceContent(
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
PreferenceCategory(title = stringResource(id = StringR.string.settings_title_general)) {
|
||||
PreferenceCategory(title = stringResource(id = CommonStrings.settings_title_general)) {
|
||||
PreferenceText(
|
||||
title = stringResource(id = R.string.screen_signout_preference_item),
|
||||
icon = Icons.Default.Logout,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ dependencies {
|
|||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
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.LogoutPreferenceState
|
||||
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.matrix.api.MatrixClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -59,6 +59,6 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
|
|||
private fun CoroutineScope.logout(logoutAction: MutableState<Async<Unit>>) = launch {
|
||||
suspend {
|
||||
matrixClient.logout()
|
||||
}.execute(logoutAction)
|
||||
}.runCatchingUpdatingState(logoutAction)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ dependencies {
|
|||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.mediaupload.api)
|
||||
implementation(projects.features.networkmonitor.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.datetime)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
|
||||
sealed interface 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
|
|
@ -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.matrix.api.core.EventId
|
||||
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.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.api.extensions.toAnalyticsViewRoom
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class MessagesNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val room: MatrixRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val presenterFactory: MessagesPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
||||
|
||||
|
|
@ -53,6 +59,14 @@ class MessagesNode @AssistedInject constructor(
|
|||
fun onReportMessage(eventId: EventId, senderId: UserId)
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
analyticsService.capture(room.toAnalyticsViewRoom())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onRoomDetailsClicked() {
|
||||
callback?.onRoomDetailsClicked()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
id = room.roomId.value,
|
||||
name = room.name,
|
||||
url = room.avatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
size = AvatarSize.TimelineRoom
|
||||
)
|
||||
roomName.value = room.name
|
||||
}
|
||||
|
|
@ -130,8 +130,8 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
is MessagesEvents.HandleAction -> {
|
||||
localCoroutineScope.handleTimelineAction(event.action, event.event, composerState)
|
||||
}
|
||||
is MessagesEvents.SendReaction -> {
|
||||
localCoroutineScope.sendReaction(event.emoji, event.eventId)
|
||||
is MessagesEvents.ToggleReaction -> {
|
||||
localCoroutineScope.toggleReaction(event.emoji, event.eventId)
|
||||
}
|
||||
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,
|
||||
eventId: EventId,
|
||||
) = launch(dispatchers.io) {
|
||||
room.sendReaction(emoji, eventId)
|
||||
room.toggleReaction(emoji, eventId)
|
||||
.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.libraries.core.data.StableCharSequence
|
||||
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.textcomposer.MessageComposerMode
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
|||
fun aMessagesState() = MessagesState(
|
||||
roomId = RoomId("!id:domain"),
|
||||
roomName = "Room name",
|
||||
roomAvatar = AvatarData("!id:domain", "Room name"),
|
||||
roomAvatar = AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom),
|
||||
userHasPermissionToSendMessage = true,
|
||||
composerState = aMessageComposerState().copy(
|
||||
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.matrix.api.core.UserId
|
||||
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 timber.log.Timber
|
||||
import io.element.android.libraries.ui.strings.R as StringsR
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -120,7 +120,7 @@ fun MessagesView(
|
|||
|
||||
fun onEmojiReactionClicked(emoji: String, event: TimelineItem.Event) {
|
||||
if (event.eventId == null) return
|
||||
state.eventSink(MessagesEvents.SendReaction(emoji, event.eventId))
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.eventId))
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
|
|
@ -150,7 +150,8 @@ fun MessagesView(
|
|||
if (event.sendState is EventSendState.SendingFailed) {
|
||||
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
|
||||
}
|
||||
}
|
||||
},
|
||||
onReactionClicked = ::onEmojiReactionClicked
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
|
|
@ -174,7 +175,7 @@ fun MessagesView(
|
|||
state = state.customReactionState,
|
||||
onEmojiSelected = { emoji ->
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -195,7 +196,7 @@ private fun AttachmentStateView(
|
|||
is AttachmentsState.Previewing -> LaunchedEffect(state) {
|
||||
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,
|
||||
onMessageClicked: (TimelineItem.Event) -> Unit,
|
||||
onUserDataClicked: (UserId) -> Unit,
|
||||
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -223,6 +225,7 @@ fun MessagesViewContent(
|
|||
onMessageLongClicked = onMessageLongClicked,
|
||||
onUserDataClicked = onUserDataClicked,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
onReactionClicked = onReactionClicked,
|
||||
)
|
||||
}
|
||||
if (state.userHasPermissionToSendMessage) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ package io.element.android.features.messages.impl.actionlist
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
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.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
|
||||
|
||||
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
|
|
@ -29,15 +33,27 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
|||
anActionListState().copy(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(),
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
)
|
||||
),
|
||||
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,
|
||||
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.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.event.TimelineItemEncryptedContent
|
||||
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.TimelineItemProfileChangeContent
|
||||
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.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.preview.ElementPreviewDark
|
||||
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.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
|
|
@ -169,9 +165,11 @@ private fun SheetContent(
|
|||
) {
|
||||
item {
|
||||
Column {
|
||||
MessageSummary(event = target.event, modifier = Modifier
|
||||
MessageSummary(
|
||||
event = target.event, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp))
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
Divider()
|
||||
}
|
||||
|
|
@ -214,10 +212,10 @@ private fun SheetContent(
|
|||
@Composable
|
||||
private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
|
||||
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 imageModifier = Modifier
|
||||
.size(36.dp)
|
||||
.size(AvatarSize.MessageActionSender.dp)
|
||||
.clip(RoundedCornerShape(9.dp))
|
||||
|
||||
@Composable
|
||||
|
|
@ -232,7 +230,6 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
|||
when (event.content) {
|
||||
is TimelineItemTextBasedContent,
|
||||
is TimelineItemStateContent,
|
||||
is TimelineItemProfileChangeContent,
|
||||
is TimelineItemEncryptedContent,
|
||||
is TimelineItemRedactedContent,
|
||||
is TimelineItemUnknownContent -> content = { ContentForBody(textContent) }
|
||||
|
|
@ -282,7 +279,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
|||
Row(modifier = modifier) {
|
||||
icon()
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row {
|
||||
if (event.senderDisplayName != null) {
|
||||
Text(
|
||||
|
|
@ -291,16 +288,16 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
|||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
event.sentTime,
|
||||
style = ElementTextStyles.Regular.caption2,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.End,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
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(
|
||||
emoji,
|
||||
fontSize = 28.dpToSp(),
|
||||
fontSize = 28.dp.toSp(),
|
||||
modifier = modifier.clickable(
|
||||
enabled = true,
|
||||
onClick = { onClicked(emoji) },
|
||||
|
|
@ -359,11 +356,6 @@ private fun EmojiButton(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Int.dpToSp(): TextUnit = with(LocalDensity.current) {
|
||||
return dp.toSp()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
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.libraries.architecture.NodeInputs
|
||||
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
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import dagger.assisted.AssistedInject
|
|||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.executeResult
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -83,8 +83,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
mediaAttachment: Attachment.Media,
|
||||
sendActionState: MutableState<Async<Unit>>,
|
||||
) {
|
||||
suspend {
|
||||
sendActionState.runUpdatingState {
|
||||
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.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
import io.element.android.libraries.ui.strings.R as StringsR
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun AttachmentsPreviewView(
|
||||
|
|
@ -92,7 +91,7 @@ private fun AttachmentSendStateView(
|
|||
) {
|
||||
when (sendActionState) {
|
||||
is Async.Loading -> {
|
||||
ProgressDialog(text = stringResource(id = R.string.common_loading))
|
||||
ProgressDialog(text = stringResource(id = CommonStrings.common_loading))
|
||||
}
|
||||
|
||||
is Async.Failure -> {
|
||||
|
|
@ -151,10 +150,10 @@ private fun AttachmentsPreviewBottomActions(
|
|||
modifier = modifier,
|
||||
) {
|
||||
TextButton(onClick = onCancelClicked) {
|
||||
Text(stringResource(id = StringsR.string.action_cancel))
|
||||
Text(stringResource(id = CommonStrings.action_cancel))
|
||||
}
|
||||
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
|
||||
|
||||
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(
|
||||
throwable: Throwable
|
||||
): Int {
|
||||
return if (throwable is MediaPreProcessor.Failure) {
|
||||
R.string.screen_media_upload_preview_error_failed_processing
|
||||
CommonStrings.screen_media_upload_preview_error_failed_processing
|
||||
} 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 io.element.android.libraries.architecture.Async
|
||||
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.matrix.api.MatrixClient
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.avatar.Avatar
|
||||
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.dialogs.ErrorDialog
|
||||
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.room.RoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
|
@ -111,7 +112,7 @@ fun ForwardMessagesView(
|
|||
modifier = modifier,
|
||||
topBar = {
|
||||
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 = {
|
||||
BackButton(onClick = { onBackButton(state) })
|
||||
},
|
||||
|
|
@ -120,7 +121,7 @@ fun ForwardMessagesView(
|
|||
enabled = state.selectedRooms.isNotEmpty(),
|
||||
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)
|
||||
) {
|
||||
SearchBar<ImmutableList<RoomSummaryDetails>>(
|
||||
placeHolderTitle = stringResource(StringR.string.action_search),
|
||||
placeHolderTitle = stringResource(CommonStrings.action_search),
|
||||
query = state.query,
|
||||
onQueryChange = { state.eventSink(ForwardMessagesEvents.UpdateQuery(it)) },
|
||||
active = state.isSearchActive,
|
||||
|
|
@ -227,18 +228,22 @@ internal fun RoomSummaryView(
|
|||
modifier = modifier
|
||||
.clickable { onSelection(summary) }
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.heightIn(56.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val roomAlias = summary.canonicalAlias ?: summary.roomId.value
|
||||
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(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 4.dp, top = 8.dp, bottom = 8.dp)
|
||||
.alignByBaseline()
|
||||
.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
// 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.isMimeTypeVideo
|
||||
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.Text
|
||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||
|
|
@ -242,7 +241,7 @@ fun MediaFileView(
|
|||
fontSize = 16.sp,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
color = ElementTheme.colors.gray1400
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
|
|
@ -250,7 +249,7 @@ fun MediaFileView(
|
|||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
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.asImageBitmap
|
||||
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 io.element.android.libraries.designsystem.text.roundToPx
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import me.saket.telephoto.zoomable.zoomable
|
||||
|
|
@ -51,7 +51,7 @@ fun PdfViewer(
|
|||
modifier = modifier.zoomable(pdfViewerState.zoomableState),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val maxWidthInPx = maxWidth.dpToPx()
|
||||
val maxWidthInPx = maxWidth.roundToPx()
|
||||
DisposableEffect(pdfViewerState) {
|
||||
pdfViewerState.openForWidth(maxWidthInPx)
|
||||
onDispose {
|
||||
|
|
@ -107,15 +107,9 @@ private fun PdfPageView(
|
|||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(state.height.pxToDp())
|
||||
.height(state.height.toDp())
|
||||
.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.libraries.architecture.NodeInputs
|
||||
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.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.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import io.element.android.libraries.androidutils.R as UtilsR
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
class MediaViewerPresenter @AssistedInject constructor(
|
||||
@Assisted private val inputs: MediaViewerNode.Inputs,
|
||||
|
|
@ -117,9 +117,9 @@ class MediaViewerPresenter @AssistedInject constructor(
|
|||
|
||||
private fun CoroutineScope.saveOnDisk(localMedia: Async<LocalMedia>) = launch {
|
||||
if (localMedia is Async.Success) {
|
||||
localMediaActions.saveOnDisk(localMedia.state)
|
||||
localMediaActions.saveOnDisk(localMedia.data)
|
||||
.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)
|
||||
}
|
||||
.onFailure {
|
||||
|
|
@ -131,7 +131,7 @@ class MediaViewerPresenter @AssistedInject constructor(
|
|||
|
||||
private fun CoroutineScope.share(localMedia: Async<LocalMedia>) = launch {
|
||||
if (localMedia is Async.Success) {
|
||||
localMediaActions.share(localMedia.state)
|
||||
localMediaActions.share(localMedia.data)
|
||||
.onFailure {
|
||||
val snackbarMessage = SnackbarMessage(mediaActionsError(it))
|
||||
snackbarDispatcher.post(snackbarMessage)
|
||||
|
|
@ -141,7 +141,7 @@ class MediaViewerPresenter @AssistedInject constructor(
|
|||
|
||||
private fun CoroutineScope.open(localMedia: Async<LocalMedia>) = launch {
|
||||
if (localMedia is Async.Success) {
|
||||
localMediaActions.open(localMedia.state)
|
||||
localMediaActions.open(localMedia.data)
|
||||
.onFailure {
|
||||
val snackbarMessage = SnackbarMessage(mediaActionsError(it))
|
||||
snackbarDispatcher.post(snackbarMessage)
|
||||
|
|
@ -153,7 +153,7 @@ class MediaViewerPresenter @AssistedInject constructor(
|
|||
return if (throwable is ActivityNotFoundException) {
|
||||
UtilsR.string.error_no_compatible_app_found
|
||||
} 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.rememberLocalMediaViewState
|
||||
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.designsystem.components.button.BackButton
|
||||
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.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.delay
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
fun MediaViewerView(
|
||||
|
|
@ -131,7 +130,7 @@ fun MediaViewerView(
|
|||
) {
|
||||
if (state.downloadedMedia is Async.Failure) {
|
||||
ErrorView(
|
||||
errorMessage = stringResource(id = StringR.string.error_unknown),
|
||||
errorMessage = stringResource(id = CommonStrings.error_unknown),
|
||||
onRetry = ::onRetry,
|
||||
onDismiss = ::onDismissError
|
||||
)
|
||||
|
|
@ -188,7 +187,7 @@ private fun MediaViewerTopBar(
|
|||
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(
|
||||
enabled = actionsEnabled,
|
||||
|
|
@ -196,7 +195,7 @@ private fun MediaViewerTopBar(
|
|||
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(
|
||||
enabled = actionsEnabled,
|
||||
|
|
@ -204,7 +203,7 @@ private fun MediaViewerTopBar(
|
|||
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 io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.executeResult
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
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.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
class ReportMessagePresenter @AssistedInject constructor(
|
||||
private val room: MatrixRoom,
|
||||
|
|
@ -87,12 +86,12 @@ class ReportMessagePresenter @AssistedInject constructor(
|
|||
blockUser: Boolean,
|
||||
result: MutableState<Async<Unit>>,
|
||||
) = launch {
|
||||
suspend {
|
||||
result.runUpdatingState {
|
||||
val userIdToBlock = userId.takeIf { blockUser }
|
||||
room.reportContent(eventId, reason, userIdToBlock)
|
||||
.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.Scaffold
|
||||
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)
|
||||
@Composable
|
||||
|
|
@ -74,7 +74,7 @@ fun ReportMessageView(
|
|||
}
|
||||
is Async.Failure -> {
|
||||
ErrorDialog(
|
||||
content = stringResource(StringR.string.error_unknown),
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
onDismiss = { state.eventSink(ReportMessageEvents.ClearError) }
|
||||
)
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ fun ReportMessageView(
|
|||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
stringResource(StringR.string.action_report_content),
|
||||
stringResource(CommonStrings.action_report_content),
|
||||
style = ElementTextStyles.Regular.callout,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
|
|
@ -112,14 +112,14 @@ fun ReportMessageView(
|
|||
OutlinedTextField(
|
||||
value = state.reason,
|
||||
onValueChange = { state.eventSink(ReportMessageEvents.UpdateReason(it)) },
|
||||
placeholder = { Text(stringResource(StringR.string.report_content_hint)) },
|
||||
placeholder = { Text(stringResource(CommonStrings.report_content_hint)) },
|
||||
enabled = !isSending,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 90.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(StringR.string.report_content_explanation),
|
||||
text = stringResource(CommonStrings.report_content_explanation),
|
||||
style = ElementTextStyles.Regular.caption1,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.Start,
|
||||
|
|
@ -133,11 +133,11 @@ fun ReportMessageView(
|
|||
) {
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
text = stringResource(StringR.string.screen_report_content_block_user),
|
||||
text = stringResource(CommonStrings.screen_report_content_block_user),
|
||||
style = ElementTextStyles.Regular.callout,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(StringR.string.screen_report_content_block_user_hint),
|
||||
text = stringResource(CommonStrings.screen_report_content_block_user_hint),
|
||||
style = ElementTextStyles.Regular.bodyMD,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
|
|
@ -152,7 +152,7 @@ fun ReportMessageView(
|
|||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
ButtonWithProgress(
|
||||
text = stringResource(StringR.string.action_send),
|
||||
text = stringResource(CommonStrings.action_send),
|
||||
enabled = state.reason.isNotBlank() && !isSending,
|
||||
showProgress = isSending,
|
||||
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.aTimelineItemStateEventContent
|
||||
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.AvatarSize
|
||||
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.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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlin.random.Random
|
||||
|
||||
fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState(
|
||||
|
|
@ -83,15 +88,23 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
|||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.First
|
||||
),
|
||||
// A state event on top of it
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
groupPosition = TimelineItemGroupPosition.None
|
||||
),
|
||||
// A grouped event on top of it
|
||||
aGroupedEvents(),
|
||||
// A day separator
|
||||
aTimelineItemDaySeparator(),
|
||||
// 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(
|
||||
eventId: EventId = EventId("\$" + Random.nextInt().toString()),
|
||||
transactionId: String? = null,
|
||||
|
|
@ -101,22 +114,19 @@ internal fun aTimelineItemEvent(
|
|||
sendState: EventSendState = EventSendState.Sent(eventId),
|
||||
inReplyTo: InReplyTo? = null,
|
||||
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
||||
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
|
||||
): TimelineItem.Event {
|
||||
return TimelineItem.Event(
|
||||
id = eventId.value,
|
||||
eventId = eventId,
|
||||
transactionId = transactionId,
|
||||
senderId = UserId("@senderId:domain"),
|
||||
senderAvatar = AvatarData("@senderId:domain", "sender"),
|
||||
senderAvatar = AvatarData("@senderId:domain", "sender", size = AvatarSize.TimelineSender),
|
||||
content = content,
|
||||
reactionsState = TimelineItemReactions(
|
||||
persistentListOf(
|
||||
AggregatedReaction("👍", "1")
|
||||
)
|
||||
),
|
||||
reactionsState = timelineItemReactions,
|
||||
sentTime = "12:34",
|
||||
isMine = isMine,
|
||||
senderDisplayName = "sender",
|
||||
senderDisplayName = "Sender",
|
||||
groupPosition = groupPosition,
|
||||
sendState = sendState,
|
||||
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(
|
||||
model: String = "Rust(Model())",
|
||||
originalJson: String? = null,
|
||||
|
|
@ -131,3 +154,17 @@ internal fun aTimelineItemDebugInfo(
|
|||
) = TimelineItemDebugInfo(
|
||||
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,
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||
onReactionClicked: (emoji: String, TimelineItem.Event) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onReachedLoadMore() {
|
||||
|
|
@ -89,7 +90,7 @@ fun TimelineView(
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
state = lazyListState,
|
||||
reverseLayout = true,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = state.timelineItems,
|
||||
|
|
@ -103,6 +104,7 @@ fun TimelineView(
|
|||
onLongClick = onMessageLongClicked,
|
||||
onUserDataClick = onUserDataClicked,
|
||||
inReplyToClick = ::inReplyToClicked,
|
||||
onReactionClick = onReactionClicked,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
)
|
||||
if (index == state.timelineItems.lastIndex) {
|
||||
|
|
@ -127,6 +129,7 @@ fun TimelineItemRow(
|
|||
onClick: (TimelineItem.Event) -> Unit,
|
||||
onLongClick: (TimelineItem.Event) -> Unit,
|
||||
inReplyToClick: (EventId) -> Unit,
|
||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
|
@ -162,6 +165,7 @@ fun TimelineItemRow(
|
|||
onLongClick = ::onLongClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
inReplyToClick = inReplyToClick,
|
||||
onReactionClick = onReactionClick,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
@ -196,6 +200,7 @@ fun TimelineItemRow(
|
|||
inReplyToClick = inReplyToClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
onReactionClick = onReactionClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -286,5 +291,6 @@ private fun ContentToPreview(content: TimelineItemEventContent) {
|
|||
onTimestampClicked = {},
|
||||
onUserDataClicked = {},
|
||||
onMessageLongClicked = {},
|
||||
onReactionClicked = { _, _ -> },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.designsystem.preview.ElementPreviewDark
|
||||
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.Text
|
||||
|
||||
private val BUBBLE_RADIUS = 12.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)
|
||||
@Composable
|
||||
fun MessageEventBubble(
|
||||
|
|
@ -94,30 +98,39 @@ fun MessageEventBubble(
|
|||
}
|
||||
|
||||
val backgroundBubbleColor = if (state.isHighlighted) {
|
||||
ElementTheme.colors.messageHighlightedBackground
|
||||
ElementTheme.legacyColors.messageHighlightedBackground
|
||||
} else {
|
||||
if (state.isMine) {
|
||||
ElementTheme.colors.messageFromMeBackground
|
||||
ElementTheme.legacyColors.messageFromMeBackground
|
||||
} else {
|
||||
ElementTheme.colors.messageFromOtherBackground
|
||||
ElementTheme.legacyColors.messageFromOtherBackground
|
||||
}
|
||||
}
|
||||
val bubbleShape = bubbleShape()
|
||||
Surface(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.widthIn(min = 80.dp)
|
||||
.offsetForItem()
|
||||
.clip(bubbleShape)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
indication = rememberRipple(),
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
color = backgroundBubbleColor,
|
||||
shape = bubbleShape,
|
||||
content = content
|
||||
)
|
||||
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
|
||||
.padding(horizontal = 16.dp)
|
||||
.offsetForItem(),
|
||||
// Need to set the contentAlignment again (it's already set in TimelineItemEventRow), for the case
|
||||
// when content width is low.
|
||||
contentAlignment = if (state.isMine) Alignment.CenterEnd else Alignment.CenterStart
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.widthIn(min = 80.dp)
|
||||
.clip(bubbleShape)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
indication = rememberRipple(),
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
color = backgroundBubbleColor,
|
||||
shape = bubbleShape,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
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.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
|
||||
private val CORNER_RADIUS = 8.dp
|
||||
|
|
@ -49,7 +49,7 @@ fun MessageStateEventContainer(
|
|||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
val backgroundColor = if (isHighlighted) {
|
||||
ElementTheme.colors.messageHighlightedBackground
|
||||
ElementTheme.legacyColors.messageHighlightedBackground
|
||||
} else {
|
||||
Color.Companion.Transparent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
package io.element.android.features.messages.impl.timeline.components
|
||||
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -27,6 +30,7 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
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.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
||||
@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(
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = modifier.clickable(onClick = onClick::invoke),
|
||||
// TODO Should use compound.bgSubtlePrimary
|
||||
color = ElementTheme.legacyColors.gray300,
|
||||
border = BorderStroke(2.dp, MaterialTheme.colorScheme.background),
|
||||
shape = RoundedCornerShape(corner = CornerSize(12.dp)),
|
||||
shape = RoundedCornerShape(corner = CornerSize(14.dp)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 5.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// TODO `reaction.isHighlighted` is not used.
|
||||
Text(text = reaction.key, fontSize = 12.sp)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp)
|
||||
Box(modifier = Modifier.padding(2.dp)) {
|
||||
val reactionModifier = if (reaction.isHighlighted) {
|
||||
Modifier
|
||||
// TODO Check the color, should use compound.borderInteractivePrimary
|
||||
.border(BorderStroke(1.dp, Color(0xFF808994)), RoundedCornerShape(corner = CornerSize(12.dp)))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
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
|
||||
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.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.libraries.designsystem.theme.components.Text
|
||||
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
|
||||
fun TimelineEventTimestampView(
|
||||
|
|
@ -55,7 +54,7 @@ fun TimelineEventTimestampView(
|
|||
val formattedTime = event.sentTime
|
||||
val hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed
|
||||
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(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
|
|
@ -70,7 +69,7 @@ fun TimelineEventTimestampView(
|
|||
) {
|
||||
if (isMessageEdited) {
|
||||
Text(
|
||||
stringResource(R.string.common_edited_suffix),
|
||||
stringResource(CommonStrings.common_edited_suffix),
|
||||
style = ElementTextStyles.Regular.caption2,
|
||||
color = tint ?: MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline.components
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -40,22 +39,32 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.style.TextAlign
|
||||
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.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.toExtraPadding
|
||||
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.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.aTimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
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.matrix.api.core.EventId
|
||||
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.AttachmentThumbnailInfo
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
@Composable
|
||||
fun TimelineItemEventRow(
|
||||
|
|
@ -76,6 +87,7 @@ fun TimelineItemEventRow(
|
|||
onUserDataClick: (UserId) -> Unit,
|
||||
inReplyToClick: (EventId) -> Unit,
|
||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
|
@ -84,71 +96,81 @@ fun TimelineItemEventRow(
|
|||
onUserDataClick(event.senderId)
|
||||
}
|
||||
|
||||
fun onReactionClicked(emoji: String) =
|
||||
onReactionClick(emoji, event)
|
||||
|
||||
fun inReplyToClicked() {
|
||||
val inReplyToEventId = (event.inReplyTo as? InReplyTo.Ready)?.eventId ?: return
|
||||
inReplyToClick(inReplyToEventId)
|
||||
}
|
||||
|
||||
val (parentAlignment, contentAlignment) = if (event.isMine) {
|
||||
Pair(Alignment.CenterEnd, Alignment.End)
|
||||
} else {
|
||||
Pair(Alignment.CenterStart, Alignment.Start)
|
||||
}
|
||||
|
||||
// To avoid using negative offset, we display in this Box a column with:
|
||||
// - Spacer to give room to the Sender information if they must be displayed;
|
||||
// - The message bubble;
|
||||
// - 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(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
contentAlignment = parentAlignment
|
||||
contentAlignment = if (event.isMine) Alignment.CenterEnd else Alignment.CenterStart
|
||||
) {
|
||||
Row {
|
||||
Column(horizontalAlignment = contentAlignment) {
|
||||
if (event.showSenderInformation) {
|
||||
MessageSenderInformation(
|
||||
event.safeSenderName,
|
||||
event.senderAvatar,
|
||||
Modifier
|
||||
.zIndex(1f)
|
||||
.offset(y = 12.dp)
|
||||
.clickable(onClick = ::onUserDataClicked)
|
||||
)
|
||||
}
|
||||
val bubbleState = BubbleState(
|
||||
groupPosition = event.groupPosition,
|
||||
isMine = event.isMine,
|
||||
isHighlighted = isHighlighted,
|
||||
)
|
||||
MessageEventBubble(
|
||||
state = bubbleState,
|
||||
Column {
|
||||
if (event.showSenderInformation) {
|
||||
Spacer(modifier = Modifier.height(event.senderAvatar.size.dp - 8.dp))
|
||||
}
|
||||
val bubbleState = BubbleState(
|
||||
groupPosition = event.groupPosition,
|
||||
isMine = event.isMine,
|
||||
isHighlighted = isHighlighted,
|
||||
)
|
||||
MessageEventBubble(
|
||||
state = bubbleState,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
) {
|
||||
MessageEventBubbleContent(
|
||||
event = event,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = Modifier
|
||||
.zIndex(-1f)
|
||||
.widthIn(max = 320.dp)
|
||||
) {
|
||||
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))
|
||||
onMessageClick = onClick,
|
||||
onMessageLongClick = onLongClick,
|
||||
inReplyToClick = ::inReplyToClicked,
|
||||
onTimestampClicked = {
|
||||
onTimestampClicked(event)
|
||||
}
|
||||
)
|
||||
}
|
||||
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()) {
|
||||
Spacer(modifier = modifier.height(8.dp))
|
||||
Spacer(modifier = modifier.height(16.dp))
|
||||
} else {
|
||||
Spacer(modifier = modifier.height(2.dp))
|
||||
}
|
||||
|
|
@ -157,20 +179,38 @@ fun TimelineItemEventRow(
|
|||
@Composable
|
||||
private fun MessageSenderInformation(
|
||||
sender: String,
|
||||
senderAvatar: AvatarData?,
|
||||
senderAvatar: AvatarData,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(modifier = modifier) {
|
||||
if (senderAvatar != null) {
|
||||
val avatarStrokeSize = 3.dp
|
||||
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)
|
||||
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,
|
||||
onClick = onMessageClick,
|
||||
onLongClick = onMessageLongClick,
|
||||
extraPadding = event.toExtraPadding(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -215,20 +256,20 @@ private fun MessageEventBubbleContent(
|
|||
onClick = onTimestampClicked,
|
||||
modifier = timestampModifier
|
||||
.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)
|
||||
.padding(horizontal = 4.dp, vertical = 2.dp) // Inner padding
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Column(modifier) {
|
||||
ContentView(modifier = contentModifier.padding(start = 12.dp, end = 12.dp, top = 8.dp))
|
||||
Box(modifier) {
|
||||
ContentView(modifier = contentModifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp))
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
modifier = timestampModifier
|
||||
.align(Alignment.End)
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -328,7 +369,7 @@ private fun ReplyToContent(
|
|||
text = text.orEmpty(),
|
||||
style = ElementTextStyles.Regular.caption1,
|
||||
textAlign = TextAlign.Start,
|
||||
color = LocalColors.current.placeholder,
|
||||
color = ElementTheme.legacyColors.placeholder,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
|
@ -358,3 +399,94 @@ private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready) =
|
|||
)
|
||||
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(
|
||||
reactionsState: TimelineItemReactions,
|
||||
modifier: Modifier = Modifier,
|
||||
onReactionClicked: (emoji: String) -> Unit
|
||||
) {
|
||||
if (reactionsState.reactions.isEmpty()) return
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
mainAxisSpacing = 2.dp,
|
||||
crossAxisSpacing = 8.dp,
|
||||
) {
|
||||
reactionsState.reactions.forEach { reaction ->
|
||||
MessagesReactionButton(reaction = reaction)
|
||||
MessagesReactionButton(
|
||||
reaction = reaction,
|
||||
onClick = { onReactionClicked(reaction.key) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +59,7 @@ internal fun TimelineItemReactionsViewDarkPreview() =
|
|||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
TimelineItemReactionsView(
|
||||
reactionsState = aTimelineItemReactions()
|
||||
reactionsState = aTimelineItemReactions(),
|
||||
onReactionClicked = { }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,18 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.noExtraPadding
|
||||
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.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
||||
@Composable
|
||||
fun TimelineItemStateEventRow(
|
||||
|
|
@ -60,8 +67,33 @@ fun TimelineItemStateEventRow(
|
|||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
extraPadding = noExtraPadding,
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
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.TimelineItemEventContent
|
||||
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(
|
||||
content: TimelineItemEventContent,
|
||||
interactionSource: MutableInteractionSource,
|
||||
extraPadding: ExtraPadding,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
|
|
@ -42,14 +41,17 @@ fun TimelineItemEventContentView(
|
|||
when (content) {
|
||||
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemRedactedContent -> TimelineItemRedactedView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemTextBasedContent -> TimelineItemTextView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier,
|
||||
onTextClicked = onClick,
|
||||
|
|
@ -57,6 +59,7 @@ fun TimelineItemEventContentView(
|
|||
)
|
||||
is TimelineItemUnknownContent -> TimelineItemUnknownView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemImageContent -> TimelineItemImageView(
|
||||
|
|
@ -69,6 +72,7 @@ fun TimelineItemEventContentView(
|
|||
)
|
||||
is TimelineItemFileContent -> TimelineItemFileView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
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.ElementPreviewLight
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun TimelineItemEncryptedView(
|
||||
content: TimelineItemEncryptedContent,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TimelineItemInformativeView(
|
||||
text = stringResource(id = StringR.string.common_decryption_error),
|
||||
iconDescription = stringResource(id = StringR.string.dialog_title_warning),
|
||||
text = stringResource(id = CommonStrings.common_decryption_error),
|
||||
iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
|
||||
icon = Icons.Default.Warning,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
@ -57,6 +58,7 @@ private fun ContentToPreview() {
|
|||
TimelineItemEncryptedView(
|
||||
content = TimelineItemEncryptedContent(
|
||||
data = UnableToDecryptContent.Data.Unknown
|
||||
)
|
||||
),
|
||||
extraPadding = noExtraPadding
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
@Composable
|
||||
fun TimelineItemFileView(
|
||||
content: TimelineItemFileContent,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
|
|
@ -76,7 +77,7 @@ fun TimelineItemFileView(
|
|||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = content.fileExtensionAndSize,
|
||||
text = content.fileExtensionAndSize + extraPadding.str,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 12.sp,
|
||||
maxLines = 1,
|
||||
|
|
@ -98,5 +99,8 @@ internal fun TimelineItemFileViewDarkPreview(@PreviewParameter(TimelineItemFileC
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview(content: TimelineItemFileContent) {
|
||||
TimelineItemFileView(content)
|
||||
TimelineItemFileView(
|
||||
content,
|
||||
extraPadding = noExtraPadding,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ fun TimelineItemInformativeView(
|
|||
text: String,
|
||||
iconDescription: String,
|
||||
icon: ImageVector,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
|
|
@ -58,7 +59,7 @@ fun TimelineItemInformativeView(
|
|||
fontStyle = FontStyle.Italic,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 14.sp,
|
||||
text = text
|
||||
text = text + extraPadding.str
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +77,7 @@ private fun ContentToPreview() {
|
|||
TimelineItemInformativeView(
|
||||
text = "Info",
|
||||
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.libraries.designsystem.preview.ElementPreviewDark
|
||||
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
|
||||
fun TimelineItemRedactedView(
|
||||
content: TimelineItemRedactedContent,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TimelineItemInformativeView(
|
||||
text = stringResource(id = StringR.string.common_message_removed),
|
||||
iconDescription = stringResource(id = StringR.string.common_message_removed),
|
||||
text = stringResource(id = CommonStrings.common_message_removed),
|
||||
iconDescription = stringResource(id = CommonStrings.common_message_removed),
|
||||
icon = Icons.Default.Delete,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
@ -53,5 +54,8 @@ internal fun TimelineItemRedactedViewDarkPreview() =
|
|||
|
||||
@Composable
|
||||
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 androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.remember
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.model.event.TimelineItemTextBasedContent
|
||||
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.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.theme.LinkColor
|
||||
import io.element.android.libraries.designsystem.text.toAnnotatedString
|
||||
|
||||
@Composable
|
||||
fun TimelineItemTextView(
|
||||
content: TimelineItemTextBasedContent,
|
||||
interactionSource: MutableInteractionSource,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier,
|
||||
onTextClicked: () -> Unit = {},
|
||||
onTextLongClicked: () -> Unit = {},
|
||||
) {
|
||||
val htmlDocument = content.htmlDocument
|
||||
if (htmlDocument != null) {
|
||||
HtmlDocument(
|
||||
document = htmlDocument,
|
||||
modifier = modifier,
|
||||
onTextClicked = onTextClicked,
|
||||
onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
// For now we ignore the extra padding for html content, so add some spacing
|
||||
// below the content (as previous behavior)
|
||||
Column(modifier = modifier) {
|
||||
HtmlDocument(
|
||||
document = htmlDocument,
|
||||
modifier = Modifier,
|
||||
onTextClicked = onTextClicked,
|
||||
onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
} else {
|
||||
Box(modifier) {
|
||||
val linkStyle = SpanStyle(
|
||||
color = LinkColor,
|
||||
)
|
||||
val styledText = remember(content.body) { content.body.linkify(linkStyle) }
|
||||
val styledText = remember(content.body) { content.body.linkify(linkStyle) + extraPadding.str.toAnnotatedString() }
|
||||
ClickableLinkText(
|
||||
text = styledText,
|
||||
linkAnnotationTag = "URL",
|
||||
|
|
@ -109,6 +120,10 @@ internal fun TimelineItemTextViewDarkPreview(@PreviewParameter(TimelineItemTextB
|
|||
|
||||
@Composable
|
||||
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.libraries.designsystem.preview.ElementPreviewDark
|
||||
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
|
||||
fun TimelineItemUnknownView(
|
||||
content: TimelineItemUnknownContent,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TimelineItemInformativeView(
|
||||
text = stringResource(id = StringR.string.common_unsupported_event),
|
||||
iconDescription = stringResource(id = StringR.string.dialog_title_warning),
|
||||
text = stringResource(id = CommonStrings.common_unsupported_event),
|
||||
iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
|
||||
icon = Icons.Default.Info,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
@ -53,5 +54,8 @@ internal fun TimelineItemUnknownViewDarkPreview() =
|
|||
|
||||
@Composable
|
||||
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 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.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
|
@ -54,7 +54,7 @@ fun GroupHeaderView(
|
|||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val backgroundColor = if (isHighlighted) {
|
||||
ElementTheme.colors.messageHighlightedBackground
|
||||
ElementTheme.legacyColors.messageHighlightedBackground
|
||||
} else {
|
||||
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.sp
|
||||
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.preview.ElementPreviewDark
|
||||
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.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.theme.LinkColor
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
|
@ -535,7 +535,7 @@ private fun AnnotatedString.Builder.appendLink(link: Element) {
|
|||
val permalinkData = PermalinkParser.parse(uriString)
|
||||
when (permalinkData) {
|
||||
is PermalinkData.FallbackLink -> {
|
||||
pushStringAnnotation(tag = "URL", annotation = link.ownText())
|
||||
pushStringAnnotation(tag = "URL", annotation = permalinkData.uri.toString())
|
||||
withStyle(
|
||||
style = SpanStyle(color = LinkColor)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
|
|
@ -37,7 +38,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
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.features.messages.impl.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -129,7 +129,7 @@ private fun ColumnScope.RetrySendMenuContents(
|
|||
headlineContent = {
|
||||
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 {
|
||||
coroutineScope.launch {
|
||||
sheetState.hide()
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -39,30 +40,31 @@ internal fun TimelineItemDaySeparatorView(
|
|||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(8.dp),
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = model.formattedDate,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemDaySeparatorViewLightPreview(@PreviewParameter(
|
||||
TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
||||
internal fun TimelineItemDaySeparatorViewLightPreview(
|
||||
@PreviewParameter(TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
||||
) =
|
||||
ElementPreviewLight { ContentToPreview(model) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemDaySeparatorViewDarkPreview(@PreviewParameter(
|
||||
TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
||||
internal fun TimelineItemDaySeparatorViewDarkPreview(
|
||||
@PreviewParameter(TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
||||
) =
|
||||
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.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.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
|
|
@ -34,6 +35,7 @@ import javax.inject.Inject
|
|||
|
||||
class TimelineItemEventFactory @Inject constructor(
|
||||
private val contentFactory: TimelineItemContentFactory,
|
||||
private val matrixClient: MatrixClient,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
|
|
@ -67,7 +69,7 @@ class TimelineItemEventFactory @Inject constructor(
|
|||
id = currentSender.value,
|
||||
name = senderDisplayName ?: currentSender.value,
|
||||
url = senderAvatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
size = AvatarSize.TimelineSender
|
||||
)
|
||||
return TimelineItem.Event(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
|
|
@ -89,8 +91,13 @@ class TimelineItemEventFactory @Inject constructor(
|
|||
|
||||
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
|
||||
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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,13 @@
|
|||
|
||||
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(
|
||||
val key: String,
|
||||
val count: String,
|
||||
val count: Int,
|
||||
val isHighlighted: Boolean = false
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,16 +20,20 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
|
||||
open class AggregatedReactionProvider : PreviewParameterProvider<AggregatedReaction> {
|
||||
override val values: Sequence<AggregatedReaction>
|
||||
get() = sequenceOf(
|
||||
anAggregatedReaction(),
|
||||
anAggregatedReaction().copy(count = "88"),
|
||||
anAggregatedReaction().copy(isHighlighted = true),
|
||||
anAggregatedReaction().copy(count = "88", isHighlighted = true),
|
||||
)
|
||||
get() = sequenceOf(false, true).flatMap {
|
||||
sequenceOf(
|
||||
anAggregatedReaction(isHighlighted = it),
|
||||
anAggregatedReaction(isHighlighted = it, count = 88),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun anAggregatedReaction() = AggregatedReaction(
|
||||
key = "👍",
|
||||
count = "1", // TODO Why is it a String?
|
||||
isHighlighted = false,
|
||||
fun anAggregatedReaction(
|
||||
key: String = "👍",
|
||||
count: Int = 1,
|
||||
isHighlighted: Boolean = false,
|
||||
) = AggregatedReaction(
|
||||
key = key,
|
||||
count = count,
|
||||
isHighlighted = isHighlighted,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@ class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEv
|
|||
class TimelineItemTextBasedContentProvider : PreviewParameterProvider<TimelineItemTextBasedContent> {
|
||||
override val values = sequenceOf(
|
||||
aTimelineItemEmoteContent(),
|
||||
aTimelineItemEmoteContent().copy(htmlDocument = Jsoup.parse("Emote")),
|
||||
aTimelineItemEmoteContent().copy(htmlDocument = Jsoup.parse("Emote Document")),
|
||||
aTimelineItemNoticeContent(),
|
||||
aTimelineItemNoticeContent().copy(htmlDocument = Jsoup.parse("Notice")),
|
||||
aTimelineItemNoticeContent().copy(htmlDocument = Jsoup.parse("Notice Document")),
|
||||
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,
|
||||
thumbnailSource = 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.libraries.di.ApplicationContext
|
||||
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
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
|
|
@ -42,12 +42,12 @@ class MessageSummaryFormatterImpl @Inject constructor(
|
|||
is TimelineItemTextBasedContent -> event.content.body
|
||||
is TimelineItemStateContent -> event.content.body
|
||||
is TimelineItemProfileChangeContent -> event.content.body
|
||||
is TimelineItemEncryptedContent -> context.getString(R.string.common_unable_to_decrypt)
|
||||
is TimelineItemRedactedContent -> context.getString(R.string.common_message_removed)
|
||||
is TimelineItemUnknownContent -> context.getString(R.string.common_unsupported_event)
|
||||
is TimelineItemImageContent -> context.getString(R.string.common_image)
|
||||
is TimelineItemVideoContent -> context.getString(R.string.common_video)
|
||||
is TimelineItemFileContent -> context.getString(R.string.common_file)
|
||||
is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt)
|
||||
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
|
||||
is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event)
|
||||
is TimelineItemImageContent -> context.getString(CommonStrings.common_image)
|
||||
is TimelineItemVideoContent -> context.getString(CommonStrings.common_video)
|
||||
is TimelineItemFileContent -> context.getString(CommonStrings.common_file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ class MessagesPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle sending a reaction`() = runTest {
|
||||
fun `present - handle toggling a reaction`() = runTest {
|
||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = createMessagePresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
||||
|
|
@ -89,17 +89,35 @@ class MessagesPresenterTest {
|
|||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(MessagesEvents.SendReaction("👍", AN_EVENT_ID))
|
||||
assertThat(room.sendReactionCount).isEqualTo(1)
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||
assertThat(room.myReactions.count()).isEqualTo(1)
|
||||
|
||||
// No crashes when sending a reaction failed
|
||||
room.givenSendReactionResult(Result.failure(IllegalStateException("Failed to send reaction")))
|
||||
initialState.eventSink.invoke(MessagesEvents.SendReaction("👍", AN_EVENT_ID))
|
||||
assertThat(room.sendReactionCount).isEqualTo(2)
|
||||
room.givenToggleReactionResult(Result.failure(IllegalStateException("Failed to send reaction")))
|
||||
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
|
||||
assertThat(room.myReactions.count()).isEqualTo(1)
|
||||
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
|
||||
fun `present - handle action forward`() = runTest {
|
||||
val navigator = FakeMessagesNavigator()
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@
|
|||
|
||||
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.TimelineItemReactions
|
||||
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.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.timeline.item.TimelineItemDebugInfo
|
||||
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_NAME
|
||||
import io.element.android.libraries.matrix.test.room.aTimelineItemDebugInfo
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal fun aMessageEvent(
|
||||
eventId: EventId? = AN_EVENT_ID,
|
||||
|
|
@ -43,11 +43,11 @@ internal fun aMessageEvent(
|
|||
eventId = eventId,
|
||||
senderId = A_USER_ID,
|
||||
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,
|
||||
sentTime = "",
|
||||
isMine = isMine,
|
||||
reactionsState = TimelineItemReactions(persistentListOf()),
|
||||
reactionsState = aTimelineItemReactions(count = 0),
|
||||
sendState = EventSendState.Sent(AN_EVENT_ID),
|
||||
inReplyTo = inReplyTo,
|
||||
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.eventformatter.api.TimelineEventFormatter
|
||||
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 kotlinx.coroutines.test.TestScope
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
|||
return TimelineItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
eventItemFactory = TimelineItemEventFactory(
|
||||
TimelineItemContentFactory(
|
||||
contentFactory = TimelineItemContentFactory(
|
||||
messageFactory = TimelineItemContentMessageFactory(FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation()),
|
||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||
stickerFactory = TimelineItemContentStickerFactory(),
|
||||
|
|
@ -54,7 +55,8 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
|||
stateFactory = TimelineItemContentStateFactory(timelineEventFormatter),
|
||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
||||
)
|
||||
),
|
||||
matrixClient = FakeMatrixClient(),
|
||||
),
|
||||
virtualItemFactory = TimelineItemVirtualFactory(
|
||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||
|
|
|
|||
|
|
@ -18,10 +18,9 @@ package io.element.android.features.messages.timeline.groups
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.model.AggregatedReaction
|
||||
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.virtual.aTimelineItemDaySeparatorModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||
|
|
@ -42,7 +41,7 @@ class TimelineItemGrouperTest {
|
|||
senderAvatar = anAvatarData(),
|
||||
senderDisplayName = "",
|
||||
content = TimelineItemStateEventContent(body = "a state event"),
|
||||
reactionsState = TimelineItemReactions(emptyList<AggregatedReaction>().toImmutableList()),
|
||||
reactionsState = aTimelineItemReactions(count = 0),
|
||||
sendState = EventSendState.Sent(AN_EVENT_ID),
|
||||
inReplyTo = null,
|
||||
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.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun ConnectivityIndicatorView(
|
||||
|
|
@ -83,7 +82,7 @@ private fun Indicator(modifier: Modifier = Modifier) {
|
|||
Row(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalColors.current.gray400)
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer)
|
||||
.statusBarsPadding()
|
||||
.padding(vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
|
|
@ -98,7 +97,7 @@ private fun Indicator(modifier: Modifier = Modifier) {
|
|||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(StringR.string.common_offline),
|
||||
text = stringResource(CommonStrings.common_offline),
|
||||
style = ElementTextStyles.Regular.bodyMD.copy(fontWeight = FontWeight.Medium),
|
||||
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