Merge branch 'develop' into feature/fga/image_loading
This commit is contained in:
commit
2c0771ecc0
165 changed files with 3727 additions and 1085 deletions
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.2
|
||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.3.3
|
||||
with:
|
||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||
app-file: app/build/outputs/apk/debug/app-universal-debug.apk
|
||||
|
|
|
|||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
|||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon --warn
|
||||
|
||||
jobs:
|
||||
check:
|
||||
|
|
|
|||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
|||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --warn
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
|
|
|||
|
|
@ -206,15 +206,14 @@ knit {
|
|||
dependencies {
|
||||
allLibrariesImpl()
|
||||
allServicesImpl()
|
||||
allFeaturesImpl(rootDir)
|
||||
allFeaturesImpl(rootDir, logger)
|
||||
implementation(projects.libraries.deeplink)
|
||||
implementation(projects.tests.uitests)
|
||||
implementation(projects.anvilannotations)
|
||||
implementation(projects.appnav)
|
||||
anvil(projects.anvilcodegen)
|
||||
|
||||
// https://developer.android.com/studio/write/java8-support#library-desugaring-versions
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
|
||||
coreLibraryDesugaring(libs.android.desugar)
|
||||
implementation(libs.appyx.core)
|
||||
implementation(libs.androidx.splash)
|
||||
implementation(libs.androidx.core)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ dependencies {
|
|||
implementation(libs.dagger)
|
||||
kapt(libs.dagger.compiler)
|
||||
|
||||
allFeaturesApi(rootDir)
|
||||
allFeaturesApi(rootDir, logger)
|
||||
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
|
|
|
|||
|
|
@ -135,7 +135,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
object RoomList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class Room(val roomId: RoomId) : NavTarget
|
||||
data class Room(
|
||||
val roomId: RoomId,
|
||||
val initialElement: RoomFlowNode.NavTarget = RoomFlowNode.NavTarget.Messages
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object Settings : NavTarget
|
||||
|
|
@ -176,6 +179,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
override fun onInvitesClicked() {
|
||||
backstack.push(NavTarget.InviteList)
|
||||
}
|
||||
|
||||
override fun onRoomSettingsClicked(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomFlowNode.NavTarget.RoomDetails))
|
||||
}
|
||||
}
|
||||
roomListEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
|
|
@ -193,7 +200,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
}
|
||||
} else {
|
||||
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
|
||||
val inputs = RoomFlowNode.Inputs(room)
|
||||
val inputs = RoomFlowNode.Inputs(room, initialElement = navTarget.initialElement)
|
||||
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs) + nodeLifecycleCallbacks)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
roomMembershipObserver: RoomMembershipObserver,
|
||||
) : BackstackNode<RoomFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Messages,
|
||||
initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialElement,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
|
|
@ -73,6 +73,7 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
|
||||
data class Inputs(
|
||||
val room: MatrixRoom,
|
||||
val initialElement: NavTarget = NavTarget.Messages,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
|
@ -98,6 +99,7 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
navigateUp()
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
inputs<Inputs>()
|
||||
}
|
||||
|
||||
private fun fetchRoomMembers() = lifecycleScope.launch {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ allprojects {
|
|||
config = files("$rootDir/tools/detekt/detekt.yml")
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.1.6")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.1.7")
|
||||
}
|
||||
|
||||
// KtLint
|
||||
|
|
|
|||
1
changelog.d/427.feature
Normal file
1
changelog.d/427.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Room list contextual menu
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_create_room_action_create_room">"Neuer Raum"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Personen einladen"</string>
|
||||
<string name="screen_create_room_add_people_title">"Personen hinzufügen"</string>
|
||||
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Raumname"</string>
|
||||
|
|
|
|||
|
|
@ -161,12 +161,12 @@ fun InviteListContent(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListViewLightPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) =
|
||||
internal fun InviteListViewLightPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListViewDarkPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) =
|
||||
internal fun InviteListViewDarkPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
31
features/leaveroom/api/build.gradle.kts
Normal file
31
features/leaveroom/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.leaveroom.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.leaveroom.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface LeaveRoomEvent {
|
||||
data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent
|
||||
object HideConfirmation : LeaveRoomEvent
|
||||
data class LeaveRoom(val roomId: RoomId) : LeaveRoomEvent
|
||||
object HideError : LeaveRoomEvent
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.leaveroom.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
||||
interface LeaveRoomPresenter : Presenter<LeaveRoomState> {
|
||||
@Composable
|
||||
override fun present(): LeaveRoomState
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.leaveroom.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class LeaveRoomState(
|
||||
val confirmation: Confirmation = Confirmation.Hidden,
|
||||
val progress: Progress = Progress.Hidden,
|
||||
val error: Error = Error.Hidden,
|
||||
val eventSink: (LeaveRoomEvent) -> Unit = {},
|
||||
) {
|
||||
sealed interface Confirmation {
|
||||
object Hidden : Confirmation
|
||||
data class Generic(val roomId: RoomId) : Confirmation
|
||||
data class PrivateRoom(val roomId: RoomId) : Confirmation
|
||||
data class LastUserInRoom(val roomId: RoomId) : Confirmation
|
||||
}
|
||||
|
||||
sealed interface Progress {
|
||||
object Hidden : Progress
|
||||
object Shown : Progress
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
object Hidden : Error
|
||||
object Shown : Error
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.leaveroom.api
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
class LeaveRoomStateProvider : PreviewParameterProvider<LeaveRoomState> {
|
||||
override val values: Sequence<LeaveRoomState>
|
||||
get() = sequenceOf(
|
||||
LeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
LeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Generic(A_ROOM_ID),
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
LeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID),
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
LeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID),
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
LeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||
progress = LeaveRoomState.Progress.Shown,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
LeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Shown,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private val A_ROOM_ID = RoomId("!aRoomId:aDomain")
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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.leaveroom.api
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun LeaveRoomView(
|
||||
state: LeaveRoomState
|
||||
) {
|
||||
LeaveRoomConfirmationDialog(state)
|
||||
LeaveRoomProgressDialog(state)
|
||||
LeaveRoomErrorDialog(state)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomConfirmationDialog(
|
||||
state: LeaveRoomState,
|
||||
) {
|
||||
when (state.confirmation) {
|
||||
is LeaveRoomState.Confirmation.Hidden -> {}
|
||||
is LeaveRoomState.Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
|
||||
text = StringR.string.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,
|
||||
roomId = state.confirmation.roomId,
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
|
||||
is LeaveRoomState.Confirmation.Generic -> LeaveRoomConfirmationDialog(
|
||||
text = StringR.string.leave_room_alert_subtitle,
|
||||
roomId = state.confirmation.roomId,
|
||||
eventSink = state.eventSink,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomConfirmationDialog(
|
||||
@StringRes text: Int,
|
||||
roomId: RoomId,
|
||||
eventSink: (LeaveRoomEvent) -> Unit,
|
||||
) {
|
||||
ConfirmationDialog(
|
||||
content = stringResource(text),
|
||||
submitText = stringResource(R.string.action_leave),
|
||||
onSubmitClicked = { eventSink(LeaveRoomEvent.LeaveRoom(roomId)) },
|
||||
onDismiss = { eventSink(LeaveRoomEvent.HideConfirmation) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomProgressDialog(
|
||||
state: LeaveRoomState,
|
||||
) {
|
||||
when (state.progress) {
|
||||
is LeaveRoomState.Progress.Hidden -> {}
|
||||
is LeaveRoomState.Progress.Shown -> ProgressDialog(
|
||||
text = stringResource(StringR.string.common_leaving_room),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomErrorDialog(
|
||||
state: LeaveRoomState,
|
||||
) {
|
||||
when (state.error) {
|
||||
is LeaveRoomState.Error.Hidden -> {}
|
||||
is LeaveRoomState.Error.Shown -> ErrorDialog(
|
||||
content = stringResource(StringR.string.error_unknown),
|
||||
onDismiss = { state.eventSink(LeaveRoomEvent.HideError) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun LeaveRoomViewLightPreview(
|
||||
@PreviewParameter(LeaveRoomStateProvider::class) state: LeaveRoomState
|
||||
) = ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun LeaveRoomViewDarkPreview(
|
||||
@PreviewParameter(LeaveRoomStateProvider::class) state: LeaveRoomState
|
||||
) = ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: LeaveRoomState) {
|
||||
Box(
|
||||
modifier = Modifier.size(300.dp, 300.dp),
|
||||
propagateMinConstraints = true,
|
||||
) {
|
||||
LeaveRoomView(state = state)
|
||||
}
|
||||
}
|
||||
44
features/leaveroom/fake/build.gradle.kts
Normal file
44
features/leaveroom/fake/build.gradle.kts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.leaveroom.fake"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.features.leaveroom.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.leaveroom.fake
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import javax.inject.Inject
|
||||
|
||||
class LeaveRoomPresenterFake @Inject constructor() : LeaveRoomPresenter {
|
||||
|
||||
val events = mutableListOf<LeaveRoomEvent>()
|
||||
|
||||
private fun handleEvent(event: LeaveRoomEvent) {
|
||||
events += event
|
||||
}
|
||||
|
||||
private var state = LeaveRoomState(eventSink = ::handleEvent)
|
||||
set(value) {
|
||||
field = value.copy(eventSink = ::handleEvent)
|
||||
}
|
||||
|
||||
fun givenState(state: LeaveRoomState) {
|
||||
this.state = state
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): LeaveRoomState = state
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.leaveroom.fake
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@Module
|
||||
@ContributesTo(SessionScope::class)
|
||||
interface LeaveRoomPresenterFakeModule {
|
||||
@Binds
|
||||
fun leaveRoomPresenter(leaveRoomPresenter: LeaveRoomPresenterFake): LeaveRoomPresenter
|
||||
}
|
||||
46
features/leaveroom/impl/build.gradle.kts
Normal file
46
features/leaveroom/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.leaveroom.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.features.leaveroom.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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.leaveroom.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Generic
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.PrivateRoom
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
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.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class LeaveRoomPresenterImpl @Inject constructor(
|
||||
private val client: MatrixClient,
|
||||
private val roomMembershipObserver: RoomMembershipObserver,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : LeaveRoomPresenter {
|
||||
@Composable
|
||||
override fun present(): LeaveRoomState {
|
||||
val scope = rememberCoroutineScope()
|
||||
val confirmation = remember { mutableStateOf<LeaveRoomState.Confirmation>(LeaveRoomState.Confirmation.Hidden) }
|
||||
val progress = remember { mutableStateOf<LeaveRoomState.Progress>(LeaveRoomState.Progress.Hidden) }
|
||||
val error = remember { mutableStateOf<LeaveRoomState.Error>(LeaveRoomState.Error.Hidden) }
|
||||
|
||||
return LeaveRoomState(
|
||||
confirmation = confirmation.value,
|
||||
progress = progress.value,
|
||||
error = error.value,
|
||||
) { event ->
|
||||
when (event) {
|
||||
is LeaveRoomEvent.ShowConfirmation -> scope.launch(dispatchers.io) {
|
||||
showLeaveRoomAlert(
|
||||
matrixClient = client,
|
||||
roomId = event.roomId,
|
||||
confirmation = confirmation,
|
||||
)
|
||||
}
|
||||
|
||||
is LeaveRoomEvent.HideConfirmation -> confirmation.value = LeaveRoomState.Confirmation.Hidden
|
||||
is LeaveRoomEvent.LeaveRoom -> scope.launch(dispatchers.io) {
|
||||
client.leaveRoom(
|
||||
roomId = event.roomId,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
confirmation = confirmation,
|
||||
progress = progress,
|
||||
error = error,
|
||||
)
|
||||
}
|
||||
|
||||
is LeaveRoomEvent.HideError -> error.value = LeaveRoomState.Error.Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showLeaveRoomAlert(
|
||||
matrixClient: MatrixClient,
|
||||
roomId: RoomId,
|
||||
confirmation: MutableState<LeaveRoomState.Confirmation>,
|
||||
) {
|
||||
matrixClient.getRoom(roomId)?.use { room ->
|
||||
confirmation.value = when {
|
||||
!room.isPublic -> PrivateRoom(roomId)
|
||||
(room.memberCount() as? Async.Success<Int>)?.state == 1 -> LastUserInRoom(roomId)
|
||||
else -> Generic(roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MatrixClient.leaveRoom(
|
||||
roomId: RoomId,
|
||||
roomMembershipObserver: RoomMembershipObserver,
|
||||
confirmation: MutableState<LeaveRoomState.Confirmation>,
|
||||
progress: MutableState<LeaveRoomState.Progress>,
|
||||
error: MutableState<LeaveRoomState.Error>,
|
||||
) {
|
||||
confirmation.value = LeaveRoomState.Confirmation.Hidden
|
||||
progress.value = LeaveRoomState.Progress.Shown
|
||||
getRoom(roomId)?.use { room ->
|
||||
room.leave().onSuccess {
|
||||
roomMembershipObserver.notifyUserLeftRoom(room.roomId)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Error while leaving room ${room.name} - ${room.roomId}")
|
||||
error.value = LeaveRoomState.Error.Shown
|
||||
}
|
||||
}
|
||||
progress.value = LeaveRoomState.Progress.Hidden
|
||||
}
|
||||
|
||||
private suspend fun MatrixRoom.memberCount(): Async<Int> = membersStateFlow.first().let { membersState ->
|
||||
when (membersState) {
|
||||
MatrixRoomMembersState.Unknown -> Async.Uninitialized
|
||||
is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size)
|
||||
is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size)
|
||||
is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.count { it.membership == RoomMembershipState.JOIN })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.leaveroom.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@Module
|
||||
@ContributesTo(SessionScope::class)
|
||||
interface LeaveRoomPresenterImplModule {
|
||||
@Binds
|
||||
fun leaveRoomPresenter(leaveRoomPresenter: LeaveRoomPresenterImpl): LeaveRoomPresenter
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* 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.leaveroom.impl
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class LeaveRoomPresenterImplTest {
|
||||
|
||||
@Test
|
||||
fun `present - initial state hides all dialogs`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Hidden)
|
||||
assertThat(initialState.progress).isEqualTo(LeaveRoomState.Progress.Hidden)
|
||||
assertThat(initialState.error).isEqualTo(LeaveRoomState.Error.Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show generic confirmation`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom()
|
||||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Generic(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show private room confirmation`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom(isPublic = false),
|
||||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show last user in room confirmation`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(
|
||||
MatrixRoomMembersState.Ready(
|
||||
listOf(
|
||||
RoomMember(
|
||||
userId = UserId(value = "@aUserId:aDomain"),
|
||||
displayName = null,
|
||||
avatarUrl = null,
|
||||
membership = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous = false,
|
||||
powerLevel = 0,
|
||||
normalizedPowerLevel = 0,
|
||||
isIgnored = false
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - leaving a room leaves the room`() = runTest {
|
||||
val roomMembershipObserver = RoomMembershipObserver()
|
||||
val presenter = createPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom(),
|
||||
)
|
||||
},
|
||||
roomMembershipObserver = roomMembershipObserver
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
// Membership observer should receive a 'left room' change
|
||||
assertThat(roomMembershipObserver.updates.first().change).isEqualTo(MembershipChange.LEFT)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show error if leave room fails`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom().apply {
|
||||
givenLeaveRoomError(RuntimeException("Blimey!"))
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
skipItems(1) // Skip show progress state
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show progress indicator while leaving a room`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom(),
|
||||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
val progressState = awaitItem()
|
||||
assertThat(progressState.progress).isEqualTo(LeaveRoomState.Progress.Shown)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.progress).isEqualTo(LeaveRoomState.Progress.Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - hide error hides the error`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom().apply {
|
||||
givenLeaveRoomError(RuntimeException("Blimey!"))
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
skipItems(1) // Skip show progress state
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown)
|
||||
skipItems(1) // Skip hide progress state
|
||||
errorState.eventSink(LeaveRoomEvent.HideError)
|
||||
val hiddenErrorState = awaitItem()
|
||||
assertThat(hiddenErrorState.error).isEqualTo(LeaveRoomState.Error.Hidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createPresenter(
|
||||
client: MatrixClient = FakeMatrixClient(),
|
||||
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||
): LeaveRoomPresenter = LeaveRoomPresenterImpl(
|
||||
client = client,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
dispatchers = testCoroutineDispatchers(testScheduler, false),
|
||||
)
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfen Sie, ob Sie die Homeserver-URL korrekt eingegeben haben. Wenn die URL korrekt ist, wenden Sie sich an Ihren Homeserver-Administrator, um weitere Hilfe zu erhalten."</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"Dieser Server unterstützt derzeit kein Sliding Sync."</string>
|
||||
<string name="screen_change_server_form_header">"Homeserver-URL"</string>
|
||||
<string name="screen_change_server_form_notice">"Sie können nur eine Verbindung zu einem vorhandenen Server herstellen, der Sliding Sync unterstützt. Ihr Homeserver-Administrator muss dies konfigurieren. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Wie lautet die Adresse deines Servers?"</string>
|
||||
<string name="screen_login_title">"Willkommen zurück!"</string>
|
||||
<string name="screen_login_password_hint">"Passwort"</string>
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation(projects.libraries.textcomposer)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.dateformatter.api)
|
||||
implementation(projects.libraries.eventformatter.api)
|
||||
implementation(projects.libraries.mediapickers.api)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.mediaupload.api)
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ package io.element.android.features.messages.impl
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemContent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||
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.matrix.api.core.RoomId
|
||||
|
|
@ -46,7 +46,7 @@ fun aMessagesState() = MessagesState(
|
|||
mode = MessageComposerMode.Normal("Hello"),
|
||||
),
|
||||
timelineState = aTimelineState().copy(
|
||||
timelineItems = aTimelineItemList(aTimelineItemContent()),
|
||||
timelineItems = aTimelineItemList(aTimelineItemTextContent()),
|
||||
),
|
||||
actionListState = anActionListState(),
|
||||
hasNetworkConnection = true,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AttachFile
|
||||
import androidx.compose.material.icons.filled.Collections
|
||||
import androidx.compose.material.icons.filled.LocalLibrary
|
||||
import androidx.compose.material.icons.filled.PhotoCamera
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
|
|
@ -69,6 +68,7 @@ import io.element.android.features.messages.impl.attachments.Attachment
|
|||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.TimelineView
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
|
|
@ -88,7 +88,6 @@ import io.element.android.libraries.designsystem.utils.LogCompositions
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
import io.element.android.libraries.ui.strings.R as StringsR
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
|
||||
|
|
@ -154,6 +153,11 @@ fun MessagesView(
|
|||
}
|
||||
}
|
||||
|
||||
fun onExpandGroupClick(event: TimelineItem.GroupedEvents) {
|
||||
Timber.v("onExpandGroupClick= ${event.id}")
|
||||
state.timelineState.eventSink(TimelineEvents.ToggleExpandGroup(event))
|
||||
}
|
||||
|
||||
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
|
||||
state.eventSink(MessagesEvents.HandleAction(action, event))
|
||||
}
|
||||
|
|
@ -203,7 +207,8 @@ fun MessagesView(
|
|||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
onMessageClicked = ::onMessageClicked,
|
||||
onMessageLongClicked = ::onMessageLongClicked
|
||||
onMessageLongClicked = ::onMessageLongClicked,
|
||||
onExpandGroupClick = ::onExpandGroupClick,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
|
|
@ -242,6 +247,7 @@ fun MessagesViewContent(
|
|||
modifier: Modifier = Modifier,
|
||||
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -255,7 +261,8 @@ fun MessagesViewContent(
|
|||
state = state.timelineState,
|
||||
modifier = Modifier.weight(1f),
|
||||
onMessageClicked = onMessageClicked,
|
||||
onMessageLongClicked = onMessageLongClicked
|
||||
onMessageLongClicked = onMessageLongClicked,
|
||||
onExpandGroupClick = onExpandGroupClick,
|
||||
)
|
||||
}
|
||||
MessageComposerView(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
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.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -53,20 +54,25 @@ class ActionListPresenter @Inject constructor() : Presenter<ActionListState> {
|
|||
)
|
||||
}
|
||||
|
||||
fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.Event, target: MutableState<ActionListState.Target>) = launch {
|
||||
private fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.Event, target: MutableState<ActionListState.Target>) = launch {
|
||||
target.value = ActionListState.Target.Loading(timelineItem)
|
||||
val actions =
|
||||
if (timelineItem.content is TimelineItemRedactedContent) {
|
||||
emptyList()
|
||||
} else {
|
||||
mutableListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
).also {
|
||||
if (timelineItem.isMine) {
|
||||
it.add(TimelineItemAction.Edit)
|
||||
it.add(TimelineItemAction.Redact)
|
||||
when (timelineItem.content) {
|
||||
is TimelineItemRedactedContent,
|
||||
is TimelineItemStateContent -> {
|
||||
// TODO Add Share action (also) here, and developer options
|
||||
emptyList()
|
||||
}
|
||||
else -> {
|
||||
mutableListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
).also {
|
||||
if (timelineItem.isMine) {
|
||||
it.add(TimelineItemAction.Edit)
|
||||
it.add(TimelineItemAction.Redact)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
sealed interface TimelineEvents {
|
||||
object LoadMore : TimelineEvents
|
||||
data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents
|
||||
data class ToggleExpandGroup(val event: TimelineItem.GroupedEvents) : TimelineEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,15 +17,18 @@
|
|||
package io.element.android.features.messages.impl.timeline
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
|
|
@ -42,6 +45,7 @@ private const val backPaginationPageSize = 50
|
|||
|
||||
class TimelinePresenter @Inject constructor(
|
||||
private val timelineItemsFactory: TimelineItemsFactory,
|
||||
private val timelineItemGrouper: TimelineItemGrouper,
|
||||
room: MatrixRoom,
|
||||
) : Presenter<TimelineState> {
|
||||
|
||||
|
|
@ -53,6 +57,8 @@ class TimelinePresenter @Inject constructor(
|
|||
val highlightedEventId: MutableState<EventId?> = rememberSaveable {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
val expandedGroups = remember { mutableStateMapOf<String, Boolean>() }
|
||||
|
||||
val timelineItems = timelineItemsFactory
|
||||
.flow()
|
||||
.collectAsState()
|
||||
|
|
@ -65,6 +71,9 @@ class TimelinePresenter @Inject constructor(
|
|||
when (event) {
|
||||
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState.value)
|
||||
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
|
||||
is TimelineEvents.ToggleExpandGroup -> {
|
||||
expandedGroups[event.event.identifier()] = expandedGroups[event.event.identifier()].orFalse().not()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +92,7 @@ class TimelinePresenter @Inject constructor(
|
|||
return TimelineState(
|
||||
highlightedEventId = highlightedEventId.value,
|
||||
paginationState = paginationState.value,
|
||||
timelineItems = timelineItems.value.toImmutableList(),
|
||||
timelineItems = timelineItemGrouper.group(timelineItems.value, expandedGroups).toImmutableList(),
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ 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.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.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -55,6 +56,12 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
|||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.First
|
||||
),
|
||||
// A state event on top of it
|
||||
aTimelineItemEvent(
|
||||
isMine = false,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
groupPosition = TimelineItemGroupPosition.None
|
||||
),
|
||||
// 3 items (First Middle Last) with isMine = true
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
|
|
@ -71,12 +78,18 @@ 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
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineItemEvent(
|
||||
isMine: Boolean = false,
|
||||
content: TimelineItemEventContent = aTimelineItemContent(),
|
||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First
|
||||
): TimelineItem.Event {
|
||||
val randomId = "\$" + Random.nextInt().toString()
|
||||
|
|
@ -96,10 +109,3 @@ internal fun aTimelineItemEvent(
|
|||
groupPosition = groupPosition,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineItemContent(): TimelineItemEventContent {
|
||||
return TimelineItemTextContent(
|
||||
body = "Text",
|
||||
htmlDocument = null
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -49,19 +49,24 @@ import androidx.compose.runtime.snapshotFlow
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.LastBaseline
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
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 io.element.android.features.messages.impl.R
|
||||
import io.element.android.features.messages.impl.timeline.components.MessageEventBubble
|
||||
import io.element.android.features.messages.impl.timeline.components.MessageStateEventContainer
|
||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemReactionsView
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView
|
||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
|
||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
||||
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.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
|
|
@ -81,6 +86,7 @@ fun TimelineView(
|
|||
modifier: Modifier = Modifier,
|
||||
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
|
||||
) {
|
||||
|
||||
fun onReachedLoadMore() {
|
||||
|
|
@ -92,8 +98,6 @@ fun TimelineView(
|
|||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = lazyListState,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
reverseLayout = true
|
||||
) {
|
||||
itemsIndexed(
|
||||
|
|
@ -103,9 +107,10 @@ fun TimelineView(
|
|||
) { index, timelineItem ->
|
||||
TimelineItemRow(
|
||||
timelineItem = timelineItem,
|
||||
isHighlighted = timelineItem.identifier() == state.highlightedEventId?.value,
|
||||
highlightedItem = state.highlightedEventId?.value,
|
||||
onClick = onMessageClicked,
|
||||
onLongClick = onMessageLongClicked
|
||||
onLongClick = onMessageLongClicked,
|
||||
onExpandGroupClick = onExpandGroupClick,
|
||||
)
|
||||
if (index == state.timelineItems.lastIndex) {
|
||||
onReachedLoadMore()
|
||||
|
|
@ -124,16 +129,20 @@ fun TimelineView(
|
|||
@Composable
|
||||
fun TimelineItemRow(
|
||||
timelineItem: TimelineItem,
|
||||
isHighlighted: Boolean,
|
||||
highlightedItem: String?,
|
||||
onClick: (TimelineItem.Event) -> Unit,
|
||||
onLongClick: (TimelineItem.Event) -> Unit,
|
||||
onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
when (timelineItem) {
|
||||
is TimelineItem.Virtual -> TimelineItemVirtualRow(
|
||||
virtual = timelineItem
|
||||
)
|
||||
is TimelineItem.Virtual -> {
|
||||
TimelineItemVirtualRow(
|
||||
virtual = timelineItem,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
is TimelineItem.Event -> {
|
||||
|
||||
fun onClick() {
|
||||
onClick(timelineItem)
|
||||
}
|
||||
|
|
@ -142,12 +151,54 @@ fun TimelineItemRow(
|
|||
onLongClick(timelineItem)
|
||||
}
|
||||
|
||||
TimelineItemEventRow(
|
||||
event = timelineItem,
|
||||
isHighlighted = isHighlighted,
|
||||
onClick = ::onClick,
|
||||
onLongClick = ::onLongClick
|
||||
)
|
||||
if (timelineItem.content is TimelineItemStateContent) {
|
||||
TimelineItemStateEventRow(
|
||||
event = timelineItem,
|
||||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||
onClick = ::onClick,
|
||||
onLongClick = ::onLongClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
TimelineItemEventRow(
|
||||
event = timelineItem,
|
||||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||
onClick = ::onClick,
|
||||
onLongClick = ::onLongClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
is TimelineItem.GroupedEvents -> {
|
||||
fun onExpandGroupClick() {
|
||||
onExpandGroupClick(timelineItem)
|
||||
}
|
||||
|
||||
Column(modifier = modifier.animateContentSize()) {
|
||||
GroupHeaderView(
|
||||
text = pluralStringResource(
|
||||
id = R.plurals.room_timeline_state_changes,
|
||||
count = timelineItem.events.size,
|
||||
timelineItem.events.size
|
||||
),
|
||||
isExpanded = timelineItem.expanded,
|
||||
isHighlighted = !timelineItem.expanded && timelineItem.events.any { it.identifier() == highlightedItem },
|
||||
onClick = ::onExpandGroupClick,
|
||||
)
|
||||
if (timelineItem.expanded) {
|
||||
Column {
|
||||
timelineItem.events.forEach { subGroupEvent ->
|
||||
TimelineItemRow(
|
||||
timelineItem = subGroupEvent,
|
||||
highlightedItem = highlightedItem,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
onExpandGroupClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -236,6 +287,42 @@ fun TimelineItemEventRow(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TimelineItemStateEventRow(
|
||||
event: TimelineItem.Event,
|
||||
isHighlighted: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MessageStateEventContainer(
|
||||
isHighlighted = isHighlighted,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = Modifier
|
||||
.zIndex(-1f)
|
||||
.widthIn(max = 320.dp)
|
||||
) {
|
||||
val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = contentModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageSenderInformation(
|
||||
sender: String,
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ fun MessageEventBubble(
|
|||
|
||||
fun Modifier.offsetForItem(): Modifier {
|
||||
return if (state.isMine) {
|
||||
// FIXME setting y offset to -12.dp can overlap a state event displayed above.
|
||||
offset(y = -(12.dp))
|
||||
} else {
|
||||
offset(x = 20.dp, y = -(12.dp))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.designsystem.theme.components.Surface
|
||||
|
||||
private val CORNER_RADIUS = 8.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MessageStateEventContainer(
|
||||
isHighlighted: Boolean,
|
||||
interactionSource: MutableInteractionSource,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
val backgroundColor = if (isHighlighted) {
|
||||
ElementTheme.colors.messageHighlightedBackground
|
||||
} else {
|
||||
Color.Companion.Transparent
|
||||
}
|
||||
val shape = RoundedCornerShape(CORNER_RADIUS)
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.widthIn(min = 80.dp)
|
||||
.clip(shape)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
indication = rememberRipple(),
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
color = backgroundColor,
|
||||
shape = shape,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun MessageStateEventContainerLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun MessageStateEventContainerDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column {
|
||||
MessageStateEventContainer(
|
||||
isHighlighted = false,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
) {
|
||||
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
|
||||
}
|
||||
MessageStateEventContainer(
|
||||
isHighlighted = true,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
) {
|
||||
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
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.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
|
|
@ -74,5 +75,9 @@ fun TimelineItemEventContentView(
|
|||
content = content,
|
||||
modifier = modifier.defaultContentPadding()
|
||||
)
|
||||
is TimelineItemStateContent -> TimelineItemStateView(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun TimelineItemStateView(
|
||||
content: TimelineItemStateContent,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 13.sp,
|
||||
text = content.body,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemStateViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemStateViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
TimelineItemStateView(
|
||||
content = aTimelineItemStateEventContent(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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.group
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
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.tooling.preview.Preview
|
||||
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.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
private val CORNER_RADIUS = 8.dp
|
||||
|
||||
@Composable
|
||||
fun GroupHeaderView(
|
||||
text: String,
|
||||
isExpanded: Boolean,
|
||||
isHighlighted: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val backgroundColor = if (isHighlighted) {
|
||||
ElementTheme.colors.messageHighlightedBackground
|
||||
} else {
|
||||
Color.Companion.Transparent
|
||||
}
|
||||
val shape = RoundedCornerShape(CORNER_RADIUS)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.clip(shape)
|
||||
.clickable(onClick = onClick),
|
||||
color = backgroundColor,
|
||||
shape = shape,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
val icon = if (isExpanded) {
|
||||
Icons.Default.ExpandLess
|
||||
} else {
|
||||
Icons.Default.ExpandMore
|
||||
}
|
||||
Icon(icon, "", tint = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun GroupHeaderViewLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun GroupHeaderViewDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
GroupHeaderView(
|
||||
text = "8 room changes (expanded)",
|
||||
isExpanded = true,
|
||||
isHighlighted = false,
|
||||
onClick = {}
|
||||
)
|
||||
GroupHeaderView(
|
||||
text = "8 room changes (not expanded)",
|
||||
isExpanded = false,
|
||||
isHighlighted = false,
|
||||
onClick = {}
|
||||
)
|
||||
GroupHeaderView(
|
||||
text = "8 room changes (expanded/h)",
|
||||
isExpanded = true,
|
||||
isHighlighted = true,
|
||||
onClick = {}
|
||||
)
|
||||
GroupHeaderView(
|
||||
text = "8 room changes (not expanded/h)",
|
||||
isExpanded = false,
|
||||
isHighlighted = true,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.factories.event
|
|||
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
|
|
@ -26,7 +27,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RedactedConte
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import javax.inject.Inject
|
||||
|
|
@ -43,15 +43,15 @@ class TimelineItemContentFactory @Inject constructor(
|
|||
private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory
|
||||
) {
|
||||
|
||||
fun create(itemContent: EventContent): TimelineItemEventContent {
|
||||
return when (itemContent) {
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
return when (val itemContent = eventTimelineItem.content) {
|
||||
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
|
||||
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
|
||||
is MessageContent -> messageFactory.create(itemContent)
|
||||
is ProfileChangeContent -> profileChangeFactory.create(itemContent)
|
||||
is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem)
|
||||
is RedactedContent -> redactedMessageFactory.create(itemContent)
|
||||
is RoomMembershipContent -> roomMembershipFactory.create(itemContent)
|
||||
is StateContent -> stateFactory.create(itemContent)
|
||||
is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem)
|
||||
is StateContent -> stateFactory.create(eventTimelineItem)
|
||||
is StickerContent -> stickerFactory.create(itemContent)
|
||||
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
||||
is UnknownContent -> TimelineItemUnknownContent
|
||||
|
|
|
|||
|
|
@ -17,13 +17,18 @@
|
|||
package io.element.android.features.messages.impl.timeline.factories.event
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentProfileChangeFactory @Inject constructor() {
|
||||
class TimelineItemContentProfileChangeFactory @Inject constructor(
|
||||
private val timelineEventFormatter: TimelineEventFormatter,
|
||||
) {
|
||||
|
||||
fun create(content: ProfileChangeContent): TimelineItemEventContent {
|
||||
return TimelineItemUnknownContent
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(eventTimelineItem)
|
||||
return TimelineItemProfileChangeContent(text.orEmpty().toString())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,18 @@
|
|||
package io.element.android.features.messages.impl.timeline.factories.event
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentRoomMembershipFactory @Inject constructor() {
|
||||
class TimelineItemContentRoomMembershipFactory @Inject constructor(
|
||||
private val timelineEventFormatter: TimelineEventFormatter,
|
||||
) {
|
||||
|
||||
fun create(content: RoomMembershipContent): TimelineItemEventContent {
|
||||
return TimelineItemUnknownContent
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(eventTimelineItem)
|
||||
return TimelineItemRoomMembershipContent(text.orEmpty().toString())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,18 @@
|
|||
package io.element.android.features.messages.impl.timeline.factories.event
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentStateFactory @Inject constructor() {
|
||||
class TimelineItemContentStateFactory @Inject constructor(
|
||||
private val timelineEventFormatter: TimelineEventFormatter,
|
||||
) {
|
||||
|
||||
fun create(content: StateContent): TimelineItemEventContent {
|
||||
return TimelineItemUnknownContent
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(eventTimelineItem)
|
||||
return TimelineItemStateEventContent(text.orEmpty().toString())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class TimelineItemEventFactory @Inject constructor(
|
|||
senderId = currentSender,
|
||||
senderDisplayName = senderDisplayName,
|
||||
senderAvatar = senderAvatarData,
|
||||
content = contentFactory.create(currentTimelineItem.event.content),
|
||||
content = contentFactory.create(currentTimelineItem.event),
|
||||
isMine = currentTimelineItem.event.isOwn,
|
||||
groupPosition = groupPosition,
|
||||
reactionsState = currentTimelineItem.computeReactionsState()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.groups
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent
|
||||
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.TimelineItemRoomMembershipContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemGrouper @Inject constructor() {
|
||||
/**
|
||||
* Create a new list of [TimelineItem] by grouping some of them into [TimelineItem.GroupedEvents].
|
||||
*/
|
||||
fun group(from: List<TimelineItem>, expandedGroups: Map<String, Boolean>): List<TimelineItem> {
|
||||
val result = mutableListOf<TimelineItem>()
|
||||
val currentGroup = mutableListOf<TimelineItem.Event>()
|
||||
from.forEach { timelineItem ->
|
||||
if (timelineItem is TimelineItem.Event && timelineItem.canBeGrouped()) {
|
||||
currentGroup.add(0, timelineItem)
|
||||
} else {
|
||||
// timelineItem cannot be grouped
|
||||
if (currentGroup.isNotEmpty()) {
|
||||
// There is a pending group, create a TimelineItem.GroupedEvents if there is more than 1 Event in the pending group.
|
||||
result.addGroup(currentGroup, expandedGroups)
|
||||
currentGroup.clear()
|
||||
}
|
||||
result.add(timelineItem)
|
||||
}
|
||||
}
|
||||
if (currentGroup.isNotEmpty()) {
|
||||
result.addGroup(currentGroup, expandedGroups)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun TimelineItem.Event.canBeGrouped(): Boolean {
|
||||
return when (content) {
|
||||
is TimelineItemEncryptedContent,
|
||||
is TimelineItemImageContent,
|
||||
TimelineItemRedactedContent,
|
||||
is TimelineItemEmoteContent,
|
||||
is TimelineItemNoticeContent,
|
||||
is TimelineItemTextContent,
|
||||
TimelineItemUnknownContent -> false
|
||||
is TimelineItemProfileChangeContent,
|
||||
is TimelineItemRoomMembershipContent,
|
||||
is TimelineItemStateEventContent -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will add a group if there is more than 1 item, else add the item to the list.
|
||||
*/
|
||||
private fun MutableList<TimelineItem>.addGroup(
|
||||
group: MutableList<TimelineItem.Event>,
|
||||
expandedGroups: Map<String, Boolean>,
|
||||
) {
|
||||
if (group.size == 1) {
|
||||
// Do not create a group with just 1 item, just add the item to the result
|
||||
add(group.first())
|
||||
} else {
|
||||
add(
|
||||
TimelineItem.GroupedEvents(
|
||||
expanded = expandedGroups[group.first().id + "_group"].orFalse(),
|
||||
events = group.toImmutableList()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
sealed interface TimelineItem {
|
||||
|
|
@ -29,11 +30,13 @@ sealed interface TimelineItem {
|
|||
fun identifier(): String = when (this) {
|
||||
is Event -> id
|
||||
is Virtual -> id
|
||||
is GroupedEvents -> id
|
||||
}
|
||||
|
||||
fun contentType(): String = when (this) {
|
||||
is Event -> content.type
|
||||
is Virtual -> model.type
|
||||
is GroupedEvents -> "groupedEvent"
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
@ -60,4 +63,13 @@ sealed interface TimelineItem {
|
|||
|
||||
val safeSenderName: String = senderDisplayName ?: senderId.value
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class GroupedEvents(
|
||||
val expanded: Boolean,
|
||||
val events: ImmutableList<Event>,
|
||||
) : TimelineItem {
|
||||
// use first id with a suffix
|
||||
val id = events.first().id + "_group"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,3 +68,7 @@ fun aTimelineItemTextContent() = TimelineItemTextContent(
|
|||
)
|
||||
|
||||
fun aTimelineItemUnknownContent() = TimelineItemUnknownContent
|
||||
|
||||
fun aTimelineItemStateEventContent() = TimelineItemStateEventContent(
|
||||
body = "A state event",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
data class TimelineItemProfileChangeContent(
|
||||
override val body: String,
|
||||
) : TimelineItemStateContent {
|
||||
override val type: String = "TimelineItemProfileChangeContent"
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
data class TimelineItemRoomMembershipContent(
|
||||
override val body: String,
|
||||
) : TimelineItemStateContent {
|
||||
override val type: String = "TimelineItemRoomMembershipContent"
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.model.event
|
||||
|
||||
sealed interface TimelineItemStateContent : TimelineItemEventContent {
|
||||
val body: String
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
data class TimelineItemStateEventContent(
|
||||
override val body: String,
|
||||
) : TimelineItemStateContent {
|
||||
override val type: String = "TimelineItemStateEventContent"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d cambio en la sala"</item>
|
||||
<item quantity="other">"%1$d cambios en la sala"</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d modifica alla stanza"</item>
|
||||
<item quantity="other">"%1$d modifiche alla stanza"</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d schimbare a camerii"</item>
|
||||
<item quantity="few">"%1$d schimbări ale camerei"</item>
|
||||
<item quantity="other">"%1$d schimbări ale camerei"</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d room change"</item>
|
||||
<item quantity="other">"%1$d room changes"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_attachment_source_camera">"Camera"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Take photo"</string>
|
||||
<string name="screen_room_attachment_source_camera_video">"Record a video"</string>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
|
|||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
|
||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
|
|
@ -141,6 +142,7 @@ class MessagesPresenterTest {
|
|||
)
|
||||
val timelinePresenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
timelineItemGrouper = TimelineItemGrouper(),
|
||||
room = matrixRoom,
|
||||
)
|
||||
val actionListPresenter = ActionListPresenter()
|
||||
|
|
|
|||
|
|
@ -31,26 +31,39 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli
|
|||
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
|
||||
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.tests.testutils.testCoroutineDispatchers
|
||||
|
||||
internal fun aTimelineItemsFactory() = TimelineItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
eventItemFactory = TimelineItemEventFactory(
|
||||
TimelineItemContentFactory(
|
||||
messageFactory = TimelineItemContentMessageFactory(),
|
||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||
stickerFactory = TimelineItemContentStickerFactory(),
|
||||
utdFactory = TimelineItemContentUTDFactory(),
|
||||
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(),
|
||||
profileChangeFactory = TimelineItemContentProfileChangeFactory(),
|
||||
stateFactory = TimelineItemContentStateFactory(),
|
||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
||||
)
|
||||
),
|
||||
virtualItemFactory = TimelineItemVirtualFactory(
|
||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||
FakeDaySeparatorFormatter()
|
||||
internal fun aTimelineItemsFactory(): TimelineItemsFactory {
|
||||
val timelineEventFormatter = aTimelineEventFormatter()
|
||||
return TimelineItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
eventItemFactory = TimelineItemEventFactory(
|
||||
TimelineItemContentFactory(
|
||||
messageFactory = TimelineItemContentMessageFactory(),
|
||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||
stickerFactory = TimelineItemContentStickerFactory(),
|
||||
utdFactory = TimelineItemContentUTDFactory(),
|
||||
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter),
|
||||
profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter),
|
||||
stateFactory = TimelineItemContentStateFactory(timelineEventFormatter),
|
||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
||||
)
|
||||
),
|
||||
virtualItemFactory = TimelineItemVirtualFactory(
|
||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||
FakeDaySeparatorFormatter()
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineEventFormatter(): TimelineEventFormatter {
|
||||
return object : TimelineEventFormatter {
|
||||
override fun format(event: EventTimelineItem): CharSequence {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.messages.textcomposer
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
|
|
@ -21,11 +23,11 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
|
|
@ -52,6 +54,7 @@ import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
|||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
|
|
|||
|
|
@ -23,8 +23,13 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -33,6 +38,7 @@ class TimelinePresenterTest {
|
|||
fun `present - initial state`() = runTest {
|
||||
val presenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
timelineItemGrouper = TimelineItemGrouper(),
|
||||
room = FakeMatrixRoom(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
@ -49,6 +55,7 @@ class TimelinePresenterTest {
|
|||
fun `present - load more`() = runTest {
|
||||
val presenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
timelineItemGrouper = TimelineItemGrouper(),
|
||||
room = FakeMatrixRoom(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
@ -71,6 +78,7 @@ class TimelinePresenterTest {
|
|||
fun `present - set highlighted event`() = runTest {
|
||||
val presenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
timelineItemGrouper = TimelineItemGrouper(),
|
||||
room = FakeMatrixRoom(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
@ -87,4 +95,37 @@ class TimelinePresenterTest {
|
|||
assertThat(withoutHighlightedState.highlightedEventId).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - expand and collapse grouped events`() = runTest {
|
||||
val fakeTimeline = FakeMatrixTimeline(
|
||||
initialTimelineItems = listOf(
|
||||
MatrixTimelineItem.Event(anEventTimelineItem() /* This is a groupable event */),
|
||||
MatrixTimelineItem.Event(anEventTimelineItem() /* This is a groupable event */),
|
||||
)
|
||||
)
|
||||
val fakeRoom = FakeMatrixRoom(matrixTimeline = fakeTimeline)
|
||||
val presenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
timelineItemGrouper = TimelineItemGrouper(),
|
||||
room = fakeRoom,
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
fakeTimeline.updateTimelineItems { it }
|
||||
val loadedState = awaitItem()
|
||||
val group1 = loadedState.timelineItems.first() as TimelineItem.GroupedEvents
|
||||
assertThat(group1.expanded).isFalse()
|
||||
loadedState.eventSink.invoke(TimelineEvents.ToggleExpandGroup(group1))
|
||||
val withExpandedGroup = awaitItem()
|
||||
val group2 = withExpandedGroup.timelineItems.first() as TimelineItem.GroupedEvents
|
||||
assertThat(group2.expanded).isTrue()
|
||||
withExpandedGroup.eventSink.invoke(TimelineEvents.ToggleExpandGroup(group2))
|
||||
val withCollapsedGroup = awaitItem()
|
||||
val group3 = withCollapsedGroup.timelineItems.first() as TimelineItem.GroupedEvents
|
||||
assertThat(group3.expanded).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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.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.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
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.Test
|
||||
|
||||
class TimelineItemGrouperTest {
|
||||
private val sut = TimelineItemGrouper()
|
||||
|
||||
private val aGroupableItem = TimelineItem.Event(
|
||||
id = AN_EVENT_ID.value,
|
||||
senderId = A_USER_ID,
|
||||
senderAvatar = anAvatarData(),
|
||||
senderDisplayName = "",
|
||||
content = TimelineItemStateEventContent(body = "a state event"),
|
||||
reactionsState = TimelineItemReactions(emptyList<AggregatedReaction>().toImmutableList())
|
||||
)
|
||||
private val aNonGroupableItem = aMessageEvent()
|
||||
private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today"))
|
||||
|
||||
@Test
|
||||
fun `test empty`() {
|
||||
val result = sut.group(emptyList(), emptyMap())
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test non groupables`() {
|
||||
val result = sut.group(
|
||||
listOf(
|
||||
aNonGroupableItem,
|
||||
aNonGroupableItem,
|
||||
),
|
||||
emptyMap()
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
aNonGroupableItem,
|
||||
aNonGroupableItem,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test groupables and ensure reordering`() {
|
||||
val result = sut.group(
|
||||
listOf(
|
||||
aGroupableItem.copy(id = AN_EVENT_ID_2.value),
|
||||
aGroupableItem,
|
||||
),
|
||||
emptyMap()
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
TimelineItem.GroupedEvents(
|
||||
expanded = false,
|
||||
events = listOf(
|
||||
aGroupableItem,
|
||||
aGroupableItem.copy(id = AN_EVENT_ID_2.value),
|
||||
).toImmutableList()
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test groupables expanded`() {
|
||||
val result = sut.group(
|
||||
listOf(
|
||||
aGroupableItem,
|
||||
aGroupableItem.copy(id = AN_EVENT_ID_2.value),
|
||||
),
|
||||
mapOf("${AN_EVENT_ID_2.value}_group" to true)
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
TimelineItem.GroupedEvents(
|
||||
expanded = true,
|
||||
events = listOf(
|
||||
aGroupableItem.copy(id = AN_EVENT_ID_2.value),
|
||||
aGroupableItem,
|
||||
).toImmutableList()
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 1 groupable, not group must be created`() {
|
||||
val listsToTest = listOf(
|
||||
listOf(aGroupableItem),
|
||||
listOf(aGroupableItem, aNonGroupableItem),
|
||||
listOf(aGroupableItem, aNonGroupableItemNoEvent),
|
||||
listOf(aNonGroupableItem, aGroupableItem),
|
||||
listOf(aNonGroupableItemNoEvent, aGroupableItem),
|
||||
listOf(aNonGroupableItem, aGroupableItem, aNonGroupableItem),
|
||||
listOf(aNonGroupableItemNoEvent, aGroupableItem, aNonGroupableItemNoEvent),
|
||||
listOf(aGroupableItem, aNonGroupableItem, aGroupableItem),
|
||||
listOf(aGroupableItem, aNonGroupableItemNoEvent, aGroupableItem),
|
||||
listOf(aNonGroupableItem),
|
||||
listOf(aNonGroupableItemNoEvent),
|
||||
)
|
||||
listsToTest.forEach { listToTest ->
|
||||
val result = sut.group(listToTest, emptyMap())
|
||||
assertThat(result).isEqualTo(listToTest)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 3 blocks`() {
|
||||
val result = sut.group(
|
||||
listOf(
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
aNonGroupableItem,
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
),
|
||||
emptyMap()
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
TimelineItem.GroupedEvents(
|
||||
expanded = false,
|
||||
events = listOf(
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
).toImmutableList()
|
||||
),
|
||||
aNonGroupableItem,
|
||||
TimelineItem.GroupedEvents(
|
||||
expanded = false,
|
||||
events = listOf(
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
).toImmutableList()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -31,10 +31,21 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
|||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class RageshakeDetectionPresenterTest {
|
||||
|
||||
companion object {
|
||||
private lateinit var aBitmap: Bitmap
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun initBitmap() {
|
||||
aBitmap = mockk()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val screenshotHolder = FakeScreenshotHolder(screenshotUri = null)
|
||||
|
|
@ -85,7 +96,7 @@ class RageshakeDetectionPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - screenshot with success then dismiss`() = runTest(timeout = 30.seconds) {
|
||||
fun `present - screenshot with success then dismiss`() = runTest {
|
||||
val screenshotHolder = FakeScreenshotHolder(screenshotUri = null)
|
||||
val rageshake = FakeRageShake(isAvailableValue = true)
|
||||
val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true)
|
||||
|
|
@ -108,7 +119,7 @@ class RageshakeDetectionPresenterTest {
|
|||
rageshake.triggerPhoneRageshake()
|
||||
assertThat(awaitItem().takeScreenshot).isTrue()
|
||||
initialState.eventSink.invoke(
|
||||
RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Success(aBitmap()))
|
||||
RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Success(aBitmap))
|
||||
)
|
||||
assertThat(awaitItem().showDialog).isTrue()
|
||||
initialState.eventSink.invoke(RageshakeDetectionEvents.Dismiss)
|
||||
|
|
@ -153,7 +164,7 @@ class RageshakeDetectionPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - screenshot then disable`() = runTest(timeout = 1.seconds) {
|
||||
fun `present - screenshot then disable`() = runTest {
|
||||
val screenshotHolder = FakeScreenshotHolder(screenshotUri = null)
|
||||
val rageshake = FakeRageShake(isAvailableValue = true)
|
||||
val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true)
|
||||
|
|
@ -176,7 +187,7 @@ class RageshakeDetectionPresenterTest {
|
|||
rageshake.triggerPhoneRageshake()
|
||||
assertThat(awaitItem().takeScreenshot).isTrue()
|
||||
initialState.eventSink.invoke(
|
||||
RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Success(aBitmap()))
|
||||
RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Success(aBitmap))
|
||||
)
|
||||
assertThat(awaitItem().showDialog).isTrue()
|
||||
initialState.eventSink.invoke(RageshakeDetectionEvents.Disable)
|
||||
|
|
@ -187,5 +198,3 @@ class RageshakeDetectionPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun aBitmap(): Bitmap = mockk()
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ dependencies {
|
|||
api(projects.libraries.usersearch.api)
|
||||
api(projects.services.apperror.api)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(projects.features.leaveroom.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
@ -54,6 +55,7 @@ dependencies {
|
|||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.usersearch.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.features.leaveroom.fake)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,5 @@
|
|||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
sealed interface RoomDetailsEvent {
|
||||
data class LeaveRoom(val needsConfirmation: Boolean) : RoomDetailsEvent
|
||||
object ClearLeaveRoomWarning : RoomDetailsEvent
|
||||
object ClearError : RoomDetailsEvent
|
||||
object LeaveRoom : RoomDetailsEvent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,44 +18,33 @@ package io.element.android.features.roomdetails.impl
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomDetailsPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val roomMembershipObserver: RoomMembershipObserver,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory,
|
||||
private val leaveRoomPresenter: LeaveRoomPresenter,
|
||||
) : Presenter<RoomDetailsState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomDetailsState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val leaveRoomWarning = remember {
|
||||
mutableStateOf<LeaveRoomWarning?>(null)
|
||||
}
|
||||
val error = remember {
|
||||
mutableStateOf<RoomDetailsError?>(null)
|
||||
}
|
||||
val leaveRoomState = leaveRoomPresenter.present()
|
||||
LaunchedEffect(Unit) {
|
||||
room.updateMembers()
|
||||
}
|
||||
|
|
@ -69,16 +58,8 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
|
||||
fun handleEvents(event: RoomDetailsEvent) {
|
||||
when (event) {
|
||||
is RoomDetailsEvent.LeaveRoom -> {
|
||||
coroutineScope.leaveRoom(
|
||||
needsConfirmation = event.needsConfirmation,
|
||||
memberCount = memberCount,
|
||||
leaveRoomWarning = leaveRoomWarning,
|
||||
error = error,
|
||||
)
|
||||
}
|
||||
RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning.value = null
|
||||
RoomDetailsEvent.ClearError -> error.value = null
|
||||
is RoomDetailsEvent.LeaveRoom ->
|
||||
leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(room.roomId))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,10 +74,9 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
memberCount = memberCount,
|
||||
isEncrypted = room.isEncrypted,
|
||||
canInvite = canInvite,
|
||||
displayLeaveRoomWarning = leaveRoomWarning.value,
|
||||
error = error.value,
|
||||
roomType = roomType.value,
|
||||
roomMemberDetailsState = roomMemberDetailsState,
|
||||
leaveRoomState = leaveRoomState,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
|
@ -141,27 +121,4 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.leaveRoom(
|
||||
needsConfirmation: Boolean,
|
||||
memberCount: Async<Int>,
|
||||
leaveRoomWarning: MutableState<LeaveRoomWarning?>,
|
||||
error: MutableState<RoomDetailsError?>,
|
||||
) = launch(coroutineDispatchers.io) {
|
||||
if (needsConfirmation) {
|
||||
leaveRoomWarning.value = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount)
|
||||
} else {
|
||||
room.leave()
|
||||
.onSuccess {
|
||||
roomMembershipObserver.notifyUserLeftRoom(room.roomId)
|
||||
}.onFailure {
|
||||
error.value = RoomDetailsError.AlertGeneric
|
||||
}
|
||||
leaveRoomWarning.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
|
@ -28,11 +29,10 @@ data class RoomDetailsState(
|
|||
val roomTopic: String?,
|
||||
val memberCount: Async<Int>,
|
||||
val isEncrypted: Boolean,
|
||||
val displayLeaveRoomWarning: LeaveRoomWarning?,
|
||||
val error: RoomDetailsError?,
|
||||
val roomType: RoomDetailsType,
|
||||
val roomMemberDetailsState: RoomMemberDetailsState?,
|
||||
val canInvite: Boolean,
|
||||
val leaveRoomState: LeaveRoomState,
|
||||
val eventSink: (RoomDetailsEvent) -> Unit
|
||||
)
|
||||
|
||||
|
|
@ -40,23 +40,3 @@ sealed interface RoomDetailsType {
|
|||
object Room : RoomDetailsType
|
||||
data class Dm(val roomMember: RoomMember) : RoomDetailsType
|
||||
}
|
||||
|
||||
sealed class LeaveRoomWarning {
|
||||
object Generic : LeaveRoomWarning()
|
||||
object PrivateRoom : LeaveRoomWarning()
|
||||
object LastUserInRoom : LeaveRoomWarning()
|
||||
|
||||
companion object {
|
||||
fun computeLeaveRoomWarning(isPublic: Boolean, memberCount: Async<Int>): LeaveRoomWarning {
|
||||
return when {
|
||||
!isPublic -> PrivateRoom
|
||||
(memberCount as? Async.Success<Int>)?.state == 1 -> LastUserInRoom
|
||||
else -> Generic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface RoomDetailsError {
|
||||
object AlertGeneric : RoomDetailsError
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -70,11 +71,10 @@ fun aRoomDetailsState() = RoomDetailsState(
|
|||
"|| MAI iki/Marketing...",
|
||||
memberCount = Async.Success(32),
|
||||
isEncrypted = true,
|
||||
displayLeaveRoomWarning = null,
|
||||
error = null,
|
||||
canInvite = false,
|
||||
roomType = RoomDetailsType.Room,
|
||||
roomMemberDetailsState = null,
|
||||
leaveRoomState = LeaveRoomState(),
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection
|
||||
import io.element.android.libraries.architecture.isLoading
|
||||
|
|
@ -56,8 +57,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.button.MainActionButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
|
|
@ -68,7 +67,6 @@ 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.room.RoomMember
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
|
@ -97,6 +95,8 @@ fun RoomDetailsView(
|
|||
.consumeWindowInsets(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
LeaveRoomView(state = state.leaveRoomState)
|
||||
|
||||
when (state.roomType) {
|
||||
RoomDetailsType.Room -> {
|
||||
RoomHeaderSection(
|
||||
|
|
@ -145,23 +145,8 @@ fun RoomDetailsView(
|
|||
}
|
||||
|
||||
OtherActionsSection(onLeaveRoom = {
|
||||
state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
|
||||
state.eventSink(RoomDetailsEvent.LeaveRoom)
|
||||
})
|
||||
|
||||
if (state.displayLeaveRoomWarning != null) {
|
||||
ConfirmLeaveRoomDialog(
|
||||
leaveRoomWarning = state.displayLeaveRoomWarning,
|
||||
onConfirmLeave = { state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) },
|
||||
onDismiss = { state.eventSink(RoomDetailsEvent.ClearLeaveRoomWarning) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.error != null) {
|
||||
ErrorDialog(
|
||||
content = stringResource(StringR.string.error_unknown),
|
||||
onDismiss = { state.eventSink(RoomDetailsEvent.ClearError) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -260,27 +245,6 @@ internal fun OtherActionsSection(onLeaveRoom: () -> Unit, modifier: Modifier = M
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ConfirmLeaveRoomDialog(
|
||||
leaveRoomWarning: LeaveRoomWarning,
|
||||
onConfirmLeave: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val content = stringResource(
|
||||
when (leaveRoomWarning) {
|
||||
LeaveRoomWarning.PrivateRoom -> StringR.string.leave_room_alert_private_subtitle
|
||||
LeaveRoomWarning.LastUserInRoom -> StringR.string.leave_room_alert_empty_subtitle
|
||||
LeaveRoomWarning.Generic -> StringR.string.leave_room_alert_subtitle
|
||||
}
|
||||
)
|
||||
ConfirmationDialog(
|
||||
content = content,
|
||||
submitText = stringResource(StringR.string.action_leave),
|
||||
onSubmitClicked = onConfirmLeave,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@LargeHeightPreview
|
||||
@Composable
|
||||
fun RoomDetailsLightPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) =
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@
|
|||
<item quantity="one">"1 Person"</item>
|
||||
<item quantity="other">"%1$d Personen"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_already_invited">"Bereits eingeladen"</string>
|
||||
<string name="screen_room_details_share_room_title">"Raum teilen"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Blockieren"</string>
|
||||
<string name="screen_dm_details_block_user">"Nutzer blockieren"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Blockierung aufheben"</string>
|
||||
<string name="screen_dm_details_unblock_user">"Nutzer entblockieren"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Personen einladen"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Raum verlassen"</string>
|
||||
<string name="screen_room_details_security_title">"Sicherheit"</string>
|
||||
<string name="screen_room_details_topic_title">"Thema"</string>
|
||||
|
|
|
|||
|
|
@ -20,8 +20,7 @@ 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.roomdetails.impl.LeaveRoomWarning
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
||||
import io.element.android.features.leaveroom.fake.LeaveRoomPresenterFake
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsType
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
|
|
@ -34,7 +33,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
|
|
@ -44,9 +42,6 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -62,7 +57,7 @@ class RoomDetailsPresenterTests {
|
|||
return RoomMemberDetailsPresenter(aMatrixClient(), room, roomMemberId)
|
||||
}
|
||||
}
|
||||
return RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers, roomMemberDetailsPresenterFactory)
|
||||
return RoomDetailsPresenter(room, roomMemberDetailsPresenterFactory, LeaveRoomPresenterFake())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -155,103 +150,6 @@ class RoomDetailsPresenterTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Leave with confirmation on private room shows a specific warning`() = runTest {
|
||||
val room = aMatrixRoom(isPublic = false).apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
|
||||
}
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
|
||||
val confirmationState = awaitItem()
|
||||
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest {
|
||||
val room = aMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(aRoomMember())))
|
||||
}
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
|
||||
val confirmationState = awaitItem()
|
||||
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Leave with confirmation shows a generic warning`() = runTest {
|
||||
val room = aMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
|
||||
}
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
|
||||
val confirmationState = awaitItem()
|
||||
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Leave without confirmation leaves the room`() = runTest {
|
||||
val room = aMatrixRoom().apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
|
||||
}
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false))
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
|
||||
// Membership observer should receive a 'left room' change
|
||||
roomMembershipObserver.updates.take(1)
|
||||
.onEach { update -> Truth.assertThat(update.change).isEqualTo(MembershipChange.LEFT) }
|
||||
.collect()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ClearError removes any error present`() = runTest {
|
||||
val room = aMatrixRoom().apply {
|
||||
givenLeaveRoomError(Throwable())
|
||||
}
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false))
|
||||
val errorState = awaitItem()
|
||||
Truth.assertThat(errorState.error).isNotNull()
|
||||
errorState.eventSink(RoomDetailsEvent.ClearError)
|
||||
Truth.assertThat(awaitItem().error).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state when user can invite others to room`() = runTest {
|
||||
val room = aMatrixRoom().apply {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ interface RoomListEntryPoint : FeatureEntryPoint {
|
|||
fun onSettingsClicked()
|
||||
fun onSessionVerificationClicked()
|
||||
fun onInvitesClicked()
|
||||
fun onRoomSettingsClicked(roomId: RoomId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,8 +48,10 @@ dependencies {
|
|||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.dateformatter.api)
|
||||
implementation(projects.libraries.eventformatter.api)
|
||||
implementation(projects.features.invitelist.api)
|
||||
implementation(projects.features.networkmonitor.api)
|
||||
implementation(projects.features.leaveroom.api)
|
||||
implementation(libs.accompanist.placeholder)
|
||||
api(projects.features.roomlist.api)
|
||||
ksp(libs.showkase.processor)
|
||||
|
|
@ -62,10 +64,12 @@ dependencies {
|
|||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.libraries.eventformatter.test)
|
||||
testImplementation(projects.libraries.permissions.noop)
|
||||
testImplementation(projects.features.invitelist.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.features.leaveroom.fake)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,330 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||
// TODO replace with StringProvider
|
||||
@ApplicationContext private val context: Context,
|
||||
private val matrixClient: MatrixClient,
|
||||
) : RoomLastMessageFormatter {
|
||||
|
||||
override fun processMessageItem(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? {
|
||||
val isOutgoing = event.sender == matrixClient.sessionId
|
||||
val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value
|
||||
return when (val content = event.content) {
|
||||
is MessageContent -> processMessageContents(content, senderDisplayName, isDmRoom)
|
||||
RedactedContent -> {
|
||||
val message = context.getString(StringR.string.common_message_removed)
|
||||
if (!isDmRoom) {
|
||||
prefix(message, senderDisplayName)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
is StickerContent -> {
|
||||
content.body
|
||||
}
|
||||
is UnableToDecryptContent -> {
|
||||
val message = context.getString(StringR.string.common_decryption_error)
|
||||
if (!isDmRoom) {
|
||||
prefix(message, senderDisplayName)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
is RoomMembershipContent -> {
|
||||
processRoomMembershipChange(content, senderDisplayName, isOutgoing)
|
||||
}
|
||||
is ProfileChangeContent -> {
|
||||
processProfileChangeContent(content, senderDisplayName, isOutgoing)
|
||||
}
|
||||
is StateContent -> {
|
||||
processRoomStateChange(content, senderDisplayName, isOutgoing)
|
||||
}
|
||||
is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> {
|
||||
prefixIfNeeded(context.getString(StringR.string.common_unsupported_event), senderDisplayName, isDmRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processMessageContents(messageContent: MessageContent, senderDisplayName: String, isDmRoom: Boolean): CharSequence? {
|
||||
val messageType: MessageType = messageContent.type ?: return null
|
||||
|
||||
val internalMessage = when (messageType) {
|
||||
// Doesn't need a prefix
|
||||
is EmoteMessageType -> {
|
||||
return "- $senderDisplayName ${messageType.body}"
|
||||
}
|
||||
is TextMessageType -> {
|
||||
messageType.body
|
||||
}
|
||||
is VideoMessageType -> {
|
||||
context.getString(StringR.string.common_video)
|
||||
}
|
||||
is ImageMessageType -> {
|
||||
context.getString(StringR.string.common_image)
|
||||
}
|
||||
is FileMessageType -> {
|
||||
context.getString(StringR.string.common_file)
|
||||
}
|
||||
is AudioMessageType -> {
|
||||
context.getString(StringR.string.common_audio)
|
||||
}
|
||||
UnknownMessageType -> {
|
||||
context.getString(StringR.string.common_unsupported_event)
|
||||
}
|
||||
is NoticeMessageType -> {
|
||||
messageType.body
|
||||
}
|
||||
}
|
||||
return prefixIfNeeded(internalMessage, senderDisplayName, isDmRoom)
|
||||
}
|
||||
|
||||
private fun processRoomMembershipChange(membershipContent: RoomMembershipContent, senderDisplayName: String, senderIsYou: Boolean): CharSequence? {
|
||||
val userId = membershipContent.userId
|
||||
val memberIsYou = userId == matrixClient.sessionId
|
||||
return when (val change = membershipContent.change) {
|
||||
MembershipChange.JOINED -> if (memberIsYou) {
|
||||
context.getString(R.string.state_event_room_join_by_you)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_join, userId.value)
|
||||
}
|
||||
MembershipChange.LEFT -> if (memberIsYou) {
|
||||
context.getString(R.string.state_event_room_leave_by_you)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_leave, userId.value)
|
||||
}
|
||||
MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) {
|
||||
context.getString(R.string.state_event_room_ban_by_you, userId.value)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_ban, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.UNBANNED -> if (senderIsYou) {
|
||||
context.getString(R.string.state_event_room_unban_by_you, userId.value)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_unban, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.KICKED -> if (senderIsYou) {
|
||||
context.getString(R.string.state_event_room_remove_by_you, userId.value)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_remove, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.INVITED -> if (senderIsYou) {
|
||||
context.getString(R.string.state_event_room_invite_by_you, userId.value)
|
||||
} else if (memberIsYou) {
|
||||
context.getString(R.string.state_event_room_invite_you, senderDisplayName)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_invite, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.INVITATION_ACCEPTED -> if (memberIsYou) {
|
||||
context.getString(R.string.state_event_room_invite_accepted_by_you)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_invite_accepted, userId.value)
|
||||
}
|
||||
MembershipChange.INVITATION_REJECTED -> if (memberIsYou) {
|
||||
context.getString(R.string.state_event_room_reject_by_you)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_reject, userId.value)
|
||||
}
|
||||
MembershipChange.INVITATION_REVOKED -> if (senderIsYou) {
|
||||
context.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_third_party_revoked_invite, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.KNOCKED -> if (memberIsYou) {
|
||||
context.getString(R.string.state_event_room_knock_by_you)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_knock, userId.value)
|
||||
}
|
||||
MembershipChange.KNOCK_ACCEPTED -> if (senderIsYou) {
|
||||
context.getString(R.string.state_event_room_knock_accepted_by_you, userId.value)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_knock_accepted, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.KNOCK_RETRACTED -> if (memberIsYou) {
|
||||
context.getString(R.string.state_event_room_knock_retracted_by_you)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_knock_retracted, userId.value)
|
||||
}
|
||||
MembershipChange.KNOCK_DENIED -> if (senderIsYou) {
|
||||
context.getString(R.string.state_event_room_knock_denied_by_you, userId.value)
|
||||
} else if (memberIsYou) {
|
||||
context.getString(R.string.state_event_room_knock_denied_you, senderDisplayName)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_knock_denied, senderDisplayName, userId.value)
|
||||
}
|
||||
else -> {
|
||||
Timber.v("Filtering timeline item for room membership: $membershipContent")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processRoomStateChange(stateContent: StateContent, senderDisplayName: String, senderIsYou: Boolean): CharSequence? {
|
||||
return when (val content = stateContent.content) {
|
||||
is OtherState.RoomAvatar -> {
|
||||
val hasAvatarUrl = content.url != null
|
||||
when {
|
||||
senderIsYou && hasAvatarUrl -> context.getString(R.string.state_event_room_avatar_changed_by_you)
|
||||
senderIsYou && !hasAvatarUrl -> context.getString(R.string.state_event_room_avatar_removed_by_you)
|
||||
!senderIsYou && hasAvatarUrl -> context.getString(R.string.state_event_room_avatar_changed, senderDisplayName)
|
||||
else -> context.getString(R.string.state_event_room_avatar_removed, senderDisplayName)
|
||||
}
|
||||
}
|
||||
is OtherState.RoomCreate -> {
|
||||
if (senderIsYou) {
|
||||
context.getString(R.string.state_event_room_created_by_you)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_created, senderDisplayName)
|
||||
}
|
||||
}
|
||||
is OtherState.RoomEncryption -> context.getString(StringR.string.common_encryption_enabled)
|
||||
is OtherState.RoomName -> {
|
||||
val hasRoomName = content.name != null
|
||||
when {
|
||||
senderIsYou && hasRoomName -> context.getString(R.string.state_event_room_name_changed_by_you, content.name)
|
||||
senderIsYou && !hasRoomName -> context.getString(R.string.state_event_room_name_removed_by_you)
|
||||
!senderIsYou && hasRoomName -> context.getString(R.string.state_event_room_name_changed, senderDisplayName, content.name)
|
||||
else -> context.getString(R.string.state_event_room_name_removed, senderDisplayName)
|
||||
}
|
||||
}
|
||||
is OtherState.RoomThirdPartyInvite -> {
|
||||
if (content.displayName == null) {
|
||||
Timber.e("RoomThirdPartyInvite undisplayable due to missing name")
|
||||
return null
|
||||
}
|
||||
if (senderIsYou) {
|
||||
context.getString(R.string.state_event_room_third_party_invite_by_you, content.displayName)
|
||||
} else {
|
||||
context.getString(R.string.state_event_room_third_party_invite, senderDisplayName, content.displayName)
|
||||
}
|
||||
}
|
||||
is OtherState.RoomTopic -> {
|
||||
val hasRoomTopic = content.topic != null
|
||||
when {
|
||||
senderIsYou && hasRoomTopic -> context.getString(R.string.state_event_room_topic_changed_by_you, content.topic)
|
||||
senderIsYou && !hasRoomTopic -> context.getString(R.string.state_event_room_topic_removed_by_you)
|
||||
!senderIsYou && hasRoomTopic -> context.getString(R.string.state_event_room_topic_changed, senderDisplayName, content.topic)
|
||||
else -> context.getString(R.string.state_event_room_topic_removed, senderDisplayName)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.v("Filtering timeline item for room state change: $content")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processProfileChangeContent(
|
||||
profileChangeContent: ProfileChangeContent,
|
||||
senderDisplayName: String,
|
||||
senderIsYou: Boolean
|
||||
): String? = profileChangeContent.run {
|
||||
val displayNameChanged = displayName != prevDisplayName
|
||||
val avatarChanged = avatarUrl != prevAvatarUrl
|
||||
return when {
|
||||
avatarChanged && displayNameChanged -> {
|
||||
val message = processProfileChangeContent(profileChangeContent.copy(avatarUrl = null, prevAvatarUrl = null), senderDisplayName, senderIsYou)
|
||||
val avatarChangedToo = context.getString(R.string.state_event_avatar_changed_too)
|
||||
"$message\n$avatarChangedToo"
|
||||
}
|
||||
displayNameChanged -> {
|
||||
if (displayName != null && prevDisplayName != null) {
|
||||
if (senderIsYou) {
|
||||
context.getString(R.string.state_event_display_name_changed_from_by_you, prevDisplayName, displayName)
|
||||
} else {
|
||||
context.getString(R.string.state_event_display_name_changed_from, senderDisplayName, prevDisplayName, displayName)
|
||||
}
|
||||
} else if (displayName != null) {
|
||||
if (senderIsYou) {
|
||||
context.getString(R.string.state_event_display_name_set_by_you, displayName)
|
||||
} else {
|
||||
context.getString(R.string.state_event_display_name_set, senderDisplayName, displayName)
|
||||
}
|
||||
} else {
|
||||
if (senderIsYou) {
|
||||
context.getString(R.string.state_event_display_name_removed_by_you, prevDisplayName)
|
||||
} else {
|
||||
context.getString(R.string.state_event_display_name_removed, senderDisplayName, prevDisplayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
avatarChanged -> {
|
||||
if (senderIsYou) {
|
||||
context.getString(R.string.state_event_avatar_url_changed_by_you)
|
||||
} else {
|
||||
context.getString(R.string.state_event_avatar_url_changed, senderDisplayName)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun prefixIfNeeded(message: String, senderDisplayName: String, isDmRoom: Boolean): CharSequence = if (isDmRoom) {
|
||||
message
|
||||
} else {
|
||||
prefix(message, senderDisplayName)
|
||||
}
|
||||
|
||||
private fun prefix(message: String, senderDisplayName: String): AnnotatedString {
|
||||
return buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(senderDisplayName)
|
||||
}
|
||||
append(": ")
|
||||
append(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.VectorIcons
|
||||
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.ModalBottomSheet
|
||||
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.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomListContextMenu(
|
||||
contextMenu: RoomListState.ContextMenu.Shown,
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { eventSink(RoomListEvents.HideContextMenu) },
|
||||
) {
|
||||
RoomListModalBottomSheetContent(
|
||||
contextMenu = contextMenu,
|
||||
onRoomSettingsClicked = {
|
||||
eventSink(RoomListEvents.HideContextMenu)
|
||||
onRoomSettingsClicked(it)
|
||||
},
|
||||
onLeaveRoomClicked = {
|
||||
eventSink(RoomListEvents.HideContextMenu)
|
||||
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomListModalBottomSheetContent(
|
||||
contextMenu: RoomListState.ContextMenu.Shown,
|
||||
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
||||
onLeaveRoomClicked: (roomId: RoomId) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = contextMenu.roomName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = stringResource(id = StringR.string.common_settings))
|
||||
},
|
||||
modifier = Modifier.clickable { onRoomSettingsClicked(contextMenu.roomId) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(id = StringR.string.common_settings),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(id = StringR.string.action_leave_room),
|
||||
color = ElementTheme.colors.textActionCritical,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable { onLeaveRoomClicked(contextMenu.roomId) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
resourceId = VectorIcons.DoorOpen,
|
||||
contentDescription = stringResource(id = StringR.string.action_leave_room),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = ElementTheme.colors.textActionCritical,
|
||||
)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up.
|
||||
// see: https://issuetracker.google.com/issues/283843380
|
||||
// Remove this preview when the issue is fixed.
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListModalBottomSheetContentLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up.
|
||||
// see: https://issuetracker.google.com/issues/283843380
|
||||
// Remove this preview when the issue is fixed.
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListModalBottomSheetContentDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
RoomListModalBottomSheetContent(
|
||||
contextMenu = RoomListState.ContextMenu.Shown(
|
||||
roomId = RoomId(value = "!aRoom:aDomain"),
|
||||
roomName = "aRoom"
|
||||
),
|
||||
onRoomSettingsClicked = {},
|
||||
onLeaveRoomClicked = {}
|
||||
)
|
||||
}
|
||||
|
|
@ -16,9 +16,15 @@
|
|||
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface RoomListEvents {
|
||||
data class UpdateFilter(val newFilter: String) : RoomListEvents
|
||||
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
|
||||
object DismissRequestVerificationPrompt : RoomListEvents
|
||||
object ToggleSearchResults : RoomListEvents
|
||||
data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
|
||||
object HideContextMenu : RoomListEvents
|
||||
data class LeaveRoom(val roomId: RoomId) : RoomListEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,17 +56,22 @@ class RoomListNode @AssistedInject constructor(
|
|||
plugins<RoomListEntryPoint.Callback>().forEach { it.onInvitesClicked() }
|
||||
}
|
||||
|
||||
private fun onRoomSettingsClicked(roomId: RoomId) {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onRoomSettingsClicked(roomId) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RoomListView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onRoomClicked = this::onRoomClicked,
|
||||
onOpenSettings = this::onOpenSettings,
|
||||
onSettingsClicked = this::onOpenSettings,
|
||||
onCreateRoomClicked = this::onCreateRoomClicked,
|
||||
onVerifyClicked = this::onSessionVerificationClicked,
|
||||
onInvitesClicked = this::onInvitesClicked,
|
||||
onRoomSettingsClicked = this::onRoomSettingsClicked,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -37,6 +39,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
|
||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -62,10 +65,12 @@ class RoomListPresenter @Inject constructor(
|
|||
private val networkMonitor: NetworkMonitor,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val inviteStateDataSource: InviteStateDataSource,
|
||||
private val leaveRoomPresenter: LeaveRoomPresenter,
|
||||
) : Presenter<RoomListState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomListState {
|
||||
val leaveRoomState = leaveRoomPresenter.present()
|
||||
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
|
|
@ -97,6 +102,8 @@ class RoomListPresenter @Inject constructor(
|
|||
|
||||
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var contextMenu by remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
|
||||
|
||||
fun handleEvents(event: RoomListEvents) {
|
||||
when (event) {
|
||||
is RoomListEvents.UpdateFilter -> filter = event.newFilter
|
||||
|
|
@ -108,6 +115,14 @@ class RoomListPresenter @Inject constructor(
|
|||
}
|
||||
displaySearchResults = !displaySearchResults
|
||||
}
|
||||
is RoomListEvents.ShowContextMenu -> {
|
||||
contextMenu = RoomListState.ContextMenu.Shown(
|
||||
roomId = event.roomListRoomSummary.roomId,
|
||||
roomName = event.roomListRoomSummary.name
|
||||
)
|
||||
}
|
||||
is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden
|
||||
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +147,8 @@ class RoomListPresenter @Inject constructor(
|
|||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||
invitesState = inviteStateDataSource.inviteState(),
|
||||
displaySearchResults = displaySearchResults,
|
||||
contextMenu = contextMenu,
|
||||
leaveRoomState = leaveRoomState,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
|
@ -183,7 +200,7 @@ class RoomListPresenter @Inject constructor(
|
|||
hasUnread = roomSummary.details.unreadNotificationCount > 0,
|
||||
timestamp = lastMessageTimestampFormatter.format(roomSummary.details.lastMessageTimestamp),
|
||||
lastMessage = roomSummary.details.lastMessage?.let { message ->
|
||||
roomLastMessageFormatter.processMessageItem(message.event, roomSummary.details.isDirect)
|
||||
roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect)
|
||||
}.orEmpty(),
|
||||
avatarData = avatarData,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@
|
|||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -33,8 +35,18 @@ data class RoomListState(
|
|||
val snackbarMessage: SnackbarMessage?,
|
||||
val invitesState: InvitesState,
|
||||
val displaySearchResults: Boolean,
|
||||
val eventSink: (RoomListEvents) -> Unit
|
||||
)
|
||||
val contextMenu: ContextMenu,
|
||||
val leaveRoomState: LeaveRoomState,
|
||||
val eventSink: (RoomListEvents) -> Unit,
|
||||
) {
|
||||
sealed interface ContextMenu {
|
||||
object Hidden : ContextMenu
|
||||
data class Shown(
|
||||
val roomId: RoomId,
|
||||
val roomName: String,
|
||||
) : ContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
enum class InvitesState {
|
||||
NoInvites,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -39,6 +40,9 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
|||
aRoomListState().copy(invitesState = InvitesState.NewInvites),
|
||||
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
|
||||
aRoomListState().copy(displaySearchResults = true),
|
||||
aRoomListState().copy(contextMenu = RoomListState.ContextMenu.Shown(
|
||||
roomId = RoomId("!aRoom:aDomain"), roomName = "A nice room name"
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +56,8 @@ internal fun aRoomListState() = RoomListState(
|
|||
displayVerificationPrompt = false,
|
||||
invitesState = InvitesState.NoInvites,
|
||||
displaySearchResults = false,
|
||||
contextMenu = RoomListState.ContextMenu.Hidden,
|
||||
leaveRoomState = LeaveRoomState(),
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -62,6 +61,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
import io.element.android.features.roomlist.impl.components.RoomListTopBar
|
||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
|
|
@ -88,20 +88,38 @@ import io.element.android.libraries.ui.strings.R as StringR
|
|||
@Composable
|
||||
fun RoomListView(
|
||||
state: RoomListState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onSettingsClicked: () -> Unit,
|
||||
onVerifyClicked: () -> Unit,
|
||||
onCreateRoomClicked: () -> Unit,
|
||||
onInvitesClicked: () -> Unit,
|
||||
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onRoomClicked: (RoomId) -> Unit = {},
|
||||
onOpenSettings: () -> Unit = {},
|
||||
onVerifyClicked: () -> Unit = {},
|
||||
onCreateRoomClicked: () -> Unit = {},
|
||||
onInvitesClicked: () -> Unit = {},
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
Box {
|
||||
fun onRoomLongClicked(
|
||||
roomListRoomSummary: RoomListRoomSummary
|
||||
) {
|
||||
state.eventSink(RoomListEvents.ShowContextMenu(roomListRoomSummary))
|
||||
}
|
||||
|
||||
if (state.contextMenu is RoomListState.ContextMenu.Shown) {
|
||||
RoomListContextMenu(
|
||||
contextMenu = state.contextMenu,
|
||||
eventSink = state.eventSink,
|
||||
onRoomSettingsClicked = onRoomSettingsClicked,
|
||||
)
|
||||
}
|
||||
|
||||
LeaveRoomView(state = state.leaveRoomState)
|
||||
|
||||
RoomListContent(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||
onOpenSettings = onSettingsClicked,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
|
|
@ -110,6 +128,7 @@ fun RoomListView(
|
|||
RoomListSearchResultView(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
|
|
@ -125,12 +144,12 @@ fun RoomListContent(
|
|||
modifier: Modifier = Modifier,
|
||||
onVerifyClicked: () -> Unit = {},
|
||||
onRoomClicked: (RoomId) -> Unit = {},
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit = {},
|
||||
onOpenSettings: () -> Unit = {},
|
||||
onCreateRoomClicked: () -> Unit = {},
|
||||
onInvitesClicked: () -> Unit = {},
|
||||
) {
|
||||
fun onRoomClicked(room: RoomListRoomSummary) {
|
||||
if (room.roomId == null) return
|
||||
onRoomClicked(room.roomId)
|
||||
}
|
||||
|
||||
|
|
@ -237,7 +256,11 @@ fun RoomListContent(
|
|||
items = state.roomList,
|
||||
contentType = { room -> room.contentType() },
|
||||
) { room ->
|
||||
RoomSummaryRow(room = room, onClick = ::onRoomClicked)
|
||||
RoomSummaryRow(
|
||||
room = room,
|
||||
onClick = ::onRoomClicked,
|
||||
onLongClick = onRoomLongClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -339,13 +362,25 @@ internal fun RoomListViewDarkPreview(@PreviewParameter(RoomListStateProvider::cl
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: RoomListState) {
|
||||
RoomListView(state)
|
||||
RoomListView(
|
||||
state = state,
|
||||
onRoomClicked = {},
|
||||
onSettingsClicked = {},
|
||||
onVerifyClicked = {},
|
||||
onCreateRoomClicked = {},
|
||||
onInvitesClicked = {},
|
||||
onRoomSettingsClicked = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListSearchResultContentPreview() {
|
||||
ElementPreviewLight {
|
||||
RoomListSearchResultContent(state = aRoomListState(), onRoomClicked = {})
|
||||
RoomListSearchResultContent(
|
||||
state = aRoomListState(),
|
||||
onRoomClicked = {},
|
||||
onRoomLongClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@
|
|||
|
||||
package io.element.android.features.roomlist.impl.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -70,17 +71,20 @@ import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
|
|||
|
||||
private val minHeight = 72.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun RoomSummaryRow(
|
||||
room: RoomListRoomSummary,
|
||||
onClick: (RoomListRoomSummary) -> Unit,
|
||||
onLongClick: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (RoomListRoomSummary) -> Unit = {},
|
||||
) {
|
||||
val clickModifier = if (room.isPlaceholder) {
|
||||
modifier
|
||||
} else {
|
||||
modifier.clickable(
|
||||
modifier.combinedClickable(
|
||||
onClick = { onClick(room) },
|
||||
onLongClick = { onLongClick(room) },
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
|
|
@ -214,5 +218,9 @@ internal fun RoomSummaryRowDarkPreview(@PreviewParameter(RoomListRoomSummaryProv
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview(data: RoomListRoomSummary) {
|
||||
RoomSummaryRow(data)
|
||||
RoomSummaryRow(
|
||||
room = data,
|
||||
onClick = {},
|
||||
onLongClick = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
@Immutable
|
||||
data class RoomListRoomSummary(
|
||||
val id: String,
|
||||
val roomId: RoomId?,
|
||||
val roomId: RoomId,
|
||||
val name: String = "",
|
||||
val hasUnread: Boolean = false,
|
||||
val timestamp: String? = null,
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@
|
|||
package io.element.android.features.roomlist.impl.model
|
||||
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
object RoomListRoomSummaryPlaceholders {
|
||||
|
||||
fun create(id: String): RoomListRoomSummary {
|
||||
return RoomListRoomSummary(
|
||||
id = id,
|
||||
roomId = null,
|
||||
roomId = RoomId("!aRoom:domain"),
|
||||
isPlaceholder = true,
|
||||
name = "Short name",
|
||||
timestamp = "hh:mm",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ import io.element.android.libraries.ui.strings.R
|
|||
internal fun RoomListSearchResultView(
|
||||
state: RoomListState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
|
|
@ -85,7 +86,11 @@ internal fun RoomListSearchResultView(
|
|||
})
|
||||
) {
|
||||
if (state.displaySearchResults) {
|
||||
RoomListSearchResultContent(state = state, onRoomClicked = onRoomClicked)
|
||||
RoomListSearchResultContent(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = onRoomLongClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -96,6 +101,7 @@ internal fun RoomListSearchResultView(
|
|||
internal fun RoomListSearchResultContent(
|
||||
state: RoomListState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val borderColor = MaterialTheme.colorScheme.tertiary
|
||||
|
|
@ -104,7 +110,6 @@ internal fun RoomListSearchResultContent(
|
|||
state.eventSink(RoomListEvents.ToggleSearchResults)
|
||||
}
|
||||
fun onRoomClicked(room: RoomListRoomSummary) {
|
||||
if (room.roomId == null) return
|
||||
onRoomClicked(room.roomId)
|
||||
}
|
||||
Scaffold(
|
||||
|
|
@ -197,7 +202,11 @@ internal fun RoomListSearchResultContent(
|
|||
items = state.filteredRoomList,
|
||||
contentType = { room -> room.contentType() },
|
||||
) { room ->
|
||||
RoomSummaryRow(room = room, onClick = ::onRoomClicked)
|
||||
RoomSummaryRow(
|
||||
room = room,
|
||||
onClick = ::onRoomClicked,
|
||||
onLongClick = onRoomLongClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_roomlist_main_space_title">"Alle Chats"</string>
|
||||
<string name="state_event_avatar_changed_too">"(Avatar wurde ebenfalls geändert)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s hat seinen Avatar geändert"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Du hast deinen Avatar geändert"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s hat den Anzeigenamen von %2$s in %3$s geändert"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Du hast deinen Anzeigenamen von %1$s in %2$s geändert"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s hat den Anzeigenamen entfernt (war %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Du hast deinen Anzeigenamen entfernt (war %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s hat den Anzeigenamen auf %2$s gesetzt"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Du hast deinen Anzeigenamen auf %1$s gesetzt"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s hat den Raum-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Du hast den Raum-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s hat den Raum-Avatar entfernt"</string>
|
||||
<string name="state_event_room_created">"%1$s hat den Raum erstellt"</string>
|
||||
<string name="state_event_room_created_by_you">"Du hast den Raum erstellt"</string>
|
||||
<string name="state_event_room_invite">"%1$s hat %2$s eingeladen"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s hat die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Du hast die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_by_you">"Du hast %1$s eingeladen"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s hat dich eingeladen"</string>
|
||||
<string name="state_event_room_join">"%1$s ist dem Raum beigetreten"</string>
|
||||
<string name="state_event_room_join_by_you">"Du bist dem Raum beigetreten"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s hat deine Beitrittsanfrage abgelehnt"</string>
|
||||
<string name="state_event_room_leave">"%1$s hat den Raum verlassen"</string>
|
||||
<string name="state_event_room_leave_by_you">"Du hast den Raum verlassen"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s hat den Raumnamen geändert in: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Sie haben den Raumnamen geändert in: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s hat den Raumnamen entfernt"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Du hast den Raumnamen entfernt"</string>
|
||||
<string name="state_event_room_reject">"%1$s hat die Einladung abgelehnt"</string>
|
||||
<string name="state_event_room_reject_by_you">"Du hast die Einladung abgelehnt"</string>
|
||||
<string name="state_event_room_remove">"%1$s hat %2$s entfernt"</string>
|
||||
<string name="state_event_room_remove_by_you">"Du hast %1$s entfernt"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s hat das Thema geändert zu: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Sie haben das Thema geändert zu: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s hat das Raumthema entfernt"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Du hast das Raumthema entfernt"</string>
|
||||
</resources>
|
||||
|
|
@ -4,58 +4,4 @@
|
|||
<string name="screen_roomlist_main_space_title">"Todos los chats"</string>
|
||||
<string name="session_verification_banner_message">"Parece que estás usando un nuevo dispositivo. Verifica que eres tú para acceder a tus mensajes cifrados."</string>
|
||||
<string name="session_verification_banner_title">"Accede a tu historial de mensajes"</string>
|
||||
<string name="state_event_avatar_changed_too">"(el avatar también cambió)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s cambió su avatar"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Cambiaste tu avatar"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s cambió su nombre de %2$s a %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Cambiaste tu nombre de %1$s a %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s eliminó su nombre (era %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Eliminaste tu nombre (era %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s cambió su nombre a %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Cambiaste tu nombre a %1$s"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s cambió el avatar de la sala"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Cambiaste el avatar de la sala"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s eliminó el avatar de la sala"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"Eliminaste el avatar de la sala"</string>
|
||||
<string name="state_event_room_ban">"%1$s expulsó permanentemente a %2$s"</string>
|
||||
<string name="state_event_room_ban_by_you">"Expulsaste permanentemente a %1$s"</string>
|
||||
<string name="state_event_room_created">"%1$s creó la sala"</string>
|
||||
<string name="state_event_room_created_by_you">"Tú creaste la sala"</string>
|
||||
<string name="state_event_room_invite">"%1$s invitó a %2$s"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s aceptó la invitación"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Aceptaste la invitación"</string>
|
||||
<string name="state_event_room_invite_by_you">"Invitaste a %1$s"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s te invitó."</string>
|
||||
<string name="state_event_room_join">"%1$s se unió a la sala"</string>
|
||||
<string name="state_event_room_join_by_you">"Te uniste a la sala"</string>
|
||||
<string name="state_event_room_knock">"%1$s solicitó unirse"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s permitió que %2$s se uniera"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"%1$s te permitió unirte"</string>
|
||||
<string name="state_event_room_knock_by_you">"Solicitaste unirte"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s rechazó la solicitud de %2$s para unirse"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Rechazaste la solicitud de %1$s para unirte"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s rechazó su solicitud para unirte"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s ya no está interesado en unirse"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Cancelaste tu solicitud de unirte"</string>
|
||||
<string name="state_event_room_leave">"%1$s salió de la sala"</string>
|
||||
<string name="state_event_room_leave_by_you">"Saliste de la sala"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s cambió el nombre de la sala a: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Cambiaste el nombre de la sala a: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s eliminó el nombre de la sala"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Eliminaste el nombre de la sala"</string>
|
||||
<string name="state_event_room_reject">"%1$s rechazó la invitación"</string>
|
||||
<string name="state_event_room_reject_by_you">"Rechazaste la invitación"</string>
|
||||
<string name="state_event_room_remove">"%1$s echó a %2$s"</string>
|
||||
<string name="state_event_room_remove_by_you">"Echaste a %1$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s envió una invitación a %2$s para unirse a la sala"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Enviaste una invitación a %1$s para unirse a la sala"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s revocó la invitación a %2$s para unirse a la sala"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Revocaste la invitación de %1$s para unirse a la sala"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s cambió el tema a: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Cambiaste el tema a: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s eliminó el tema de la sala"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Eliminaste el tema de la sala"</string>
|
||||
<string name="state_event_room_unban">"%1$s readmitió a %2$s"</string>
|
||||
<string name="state_event_room_unban_by_you">"Readmitiste a %1$s"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s realizó un cambio desconocido en su membresía"</string>
|
||||
</resources>
|
||||
|
|
@ -4,58 +4,4 @@
|
|||
<string name="screen_roomlist_main_space_title">"Tutte le conversazioni"</string>
|
||||
<string name="session_verification_banner_message">"Sembra che tu stia utilizzando un nuovo dispositivo. Verifica di essere tu per accedere ai tuoi messaggi crittografati."</string>
|
||||
<string name="session_verification_banner_title">"Accedi alla cronologia dei messaggi"</string>
|
||||
<string name="state_event_avatar_changed_too">"(anche l\'avatar è stato cambiato)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s ha cambiato il proprio avatar"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Hai cambiato il tuo avatar"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s ha cambiato il proprio nome visualizzato da %2$s a %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Hai cambiato il tuo nome visualizzato da %1$s a %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s ha rimosso il proprio nome visualizzato (era %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Hai rimosso il tuo nome visualizzato (era %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s ha impostato il proprio nome visualizzato su %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Hai impostato il tuo nome visualizzato su %1$s"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s ha cambiato l\'avatar della stanza"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Hai cambiato l\'avatar della stanza"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s ha rimosso l\'avatar della stanza"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"Hai rimosso l\'avatar della stanza"</string>
|
||||
<string name="state_event_room_ban">"%1$s ha rimosso %2$s"</string>
|
||||
<string name="state_event_room_ban_by_you">"Hai rimosso %1$s"</string>
|
||||
<string name="state_event_room_created">"%1$s ha creato la stanza"</string>
|
||||
<string name="state_event_room_created_by_you">"Hai creato la stanza"</string>
|
||||
<string name="state_event_room_invite">"%1$s ha invitato %2$s"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s ha accettato l\'invito"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Hai accettato l\'invito"</string>
|
||||
<string name="state_event_room_invite_by_you">"Hai invitato %1$s"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s ti ha invitato"</string>
|
||||
<string name="state_event_room_join">"%1$s si è unito alla stanza"</string>
|
||||
<string name="state_event_room_join_by_you">"Ti sei unito alla stanza"</string>
|
||||
<string name="state_event_room_knock">"%1$s ha chiesto di unirsi"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s ha permesso a %2$s di unirsi"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"%1$s ti ha permesso di unirti"</string>
|
||||
<string name="state_event_room_knock_by_you">"Hai richiesto di unirti"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s ha rifiutato la richiesta di unirsi di %2$s"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Hai rifiutato la richiesta di unirsi di %1$s"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s ha rifiutato la tua richiesta di unirti"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s non è più interessato a partecipare"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Hai annullato la tua richiesta di unirti"</string>
|
||||
<string name="state_event_room_leave">"%1$s ha lasciato la stanza"</string>
|
||||
<string name="state_event_room_leave_by_you">"Hai lasciato la stanza"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s ha cambiato il nome della stanza in: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Hai cambiato il nome della stanza in: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s ha rimosso il nome della stanza"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Hai rimosso il nome della stanza"</string>
|
||||
<string name="state_event_room_reject">"%1$s ha rifiutato l\'invito"</string>
|
||||
<string name="state_event_room_reject_by_you">"Hai rifiutato l\'invito"</string>
|
||||
<string name="state_event_room_remove">"%1$s ha rimosso %2$s"</string>
|
||||
<string name="state_event_room_remove_by_you">"Hai rimosso %1$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s ha inviato un invito a %2$s per unirsi alla stanza"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Hai inviato un invito a %1$s per unirsi alla stanza"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s ha revocato l\'invito di %2$s ad unirsi alla stanza."</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Hai revocato l\'invito a %1$s a universi alla stanza"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s ha cambiato l\'oggetto in: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Hai cambiato l\'oggetto in: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s ha rimosso l\'oggetto della stanza"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Hai rimosso l\'oggetto della stanza"</string>
|
||||
<string name="state_event_room_unban">"%1$s ha sbloccato %2$s"</string>
|
||||
<string name="state_event_room_unban_by_you">"Hai sbloccato %1$s"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s ha apportato una modifica sconosciuta alla propria iscrizione"</string>
|
||||
</resources>
|
||||
|
|
@ -4,58 +4,4 @@
|
|||
<string name="screen_roomlist_main_space_title">"Toate conversatiile"</string>
|
||||
<string name="session_verification_banner_message">"Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea pentru acces la mesajele dumneavoastră criptate."</string>
|
||||
<string name="session_verification_banner_title">"Accesați istoricul mesajelor"</string>
|
||||
<string name="state_event_avatar_changed_too">"(s-a schimbat si avatarul)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s și-a schimbat avatarul"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"V-ați schimbat avatarul"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s și-a schimbat numele din %2$s în %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"V-ați schimbat numele din %1$s în %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s și-a sters numele (era %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"V-ați sters numele (era %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s și-a schimbat numele %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"V-ați schimbat numele în %1$s"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s a schimbat avatarul camerei"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Ați schimbat avatarul camerei"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s a șters avatarul camerei"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"Ați șters avatarul camerei"</string>
|
||||
<string name="state_event_room_ban">"%1$s a adăugat o interdicție pentru %2$s"</string>
|
||||
<string name="state_event_room_ban_by_you">"Ați adăugat o interdicție pentru %1$s"</string>
|
||||
<string name="state_event_room_created">"%1$s a creat camera"</string>
|
||||
<string name="state_event_room_created_by_you">"Ați creat camera"</string>
|
||||
<string name="state_event_room_invite">"%1$s l-a invitat pe %2$s"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s a acceptat invitația"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Ați acceptat invitația"</string>
|
||||
<string name="state_event_room_invite_by_you">"L-ați invitat pe %1$s"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s v-a invitat"</string>
|
||||
<string name="state_event_room_join">"%1$s a intrat în cameră"</string>
|
||||
<string name="state_event_room_join_by_you">"Ați intrat în cameră"</string>
|
||||
<string name="state_event_room_knock">"%1$s a solicitat să se alăture camerei"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s i-a permis lui %2$s să se alăture camerei"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"%1$s v-a permis să vă alăturați camerei"</string>
|
||||
<string name="state_event_room_knock_by_you">"Ați solicitat să vă alăturați camerei"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s a respins solicitarea de alăturare a lui %2$s"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Ați respins solicitarea de alăturare a lui %1$s"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s a respins cererea dumneavoastră de alăturare"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s nu mai este interesat să se alăture camerei"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Ați anulat cererea de alăturare"</string>
|
||||
<string name="state_event_room_leave">"%1$s a părăsit camera"</string>
|
||||
<string name="state_event_room_leave_by_you">"Ați părăsit camera"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s a schimbat numele camerei în: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Ați schimbat numele camerei în: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s a sters numele camerei"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Ați șters numele camerei"</string>
|
||||
<string name="state_event_room_reject">"%1$s a respins invitația"</string>
|
||||
<string name="state_event_room_reject_by_you">"Ați respins invitația"</string>
|
||||
<string name="state_event_room_remove">"%1$s l-a îndepărtat pe %2$s"</string>
|
||||
<string name="state_event_room_remove_by_you">"L-ați îndepărtat pe %1$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s a trimis o invitație către %2$s pentru a se alătura camerei"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Ați trimis o invitație către %1$s pentru a se alătura camerei"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s a revocat invitația pentru %2$s de a se alătura camerei"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Ați revocat invitația pentru %1$s de a se alătura camerei"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s a schimbat subiectul în: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Ați schimbat subiectul în: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s a șters subiectul camerei"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Ați șters subiectul camerei"</string>
|
||||
<string name="state_event_room_unban">"%1$s a anulat interdicția pentru %2$s"</string>
|
||||
<string name="state_event_room_unban_by_you">"Ați anulat interdicția pentru %1$s"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s a făcut o modificare necunoscută asupra calității sale de membru"</string>
|
||||
</resources>
|
||||
|
|
@ -4,58 +4,4 @@
|
|||
<string name="screen_roomlist_main_space_title">"All Chats"</string>
|
||||
<string name="session_verification_banner_message">"Looks like you’re using a new device. Verify it’s you to access your encrypted messages."</string>
|
||||
<string name="session_verification_banner_title">"Access your message history"</string>
|
||||
<string name="state_event_avatar_changed_too">"(avatar was changed too)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s changed their avatar"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"You changed your avatar"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s changed their display name from %2$s to %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"You changed your display name from %1$s to %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s removed their display name (it was %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"You removed your display name (it was %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s set their display name to %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"You set your display name to %1$s"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s changed the room avatar"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"You changed the room avatar"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s removed the room avatar"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"You removed the room avatar"</string>
|
||||
<string name="state_event_room_ban">"%1$s banned %2$s"</string>
|
||||
<string name="state_event_room_ban_by_you">"You banned %1$s"</string>
|
||||
<string name="state_event_room_created">"%1$s created the room"</string>
|
||||
<string name="state_event_room_created_by_you">"You created the room"</string>
|
||||
<string name="state_event_room_invite">"%1$s invited %2$s"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s accepted the invite"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"You accepted the invite"</string>
|
||||
<string name="state_event_room_invite_by_you">"You invited %1$s"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s invited you"</string>
|
||||
<string name="state_event_room_join">"%1$s joined the room"</string>
|
||||
<string name="state_event_room_join_by_you">"You joined the room"</string>
|
||||
<string name="state_event_room_knock">"%1$s requested to join"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s allowed %2$s to join"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"%1$s allowed you to join"</string>
|
||||
<string name="state_event_room_knock_by_you">"You requested to join"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s rejected %2$s\'s request to join"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"You rejected %1$s\'s request to join"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s rejected your request to join"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s is no longer interested in joining"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"You cancelled your request to join"</string>
|
||||
<string name="state_event_room_leave">"%1$s left the room"</string>
|
||||
<string name="state_event_room_leave_by_you">"You left the room"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s changed the room name to: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"You changed the room name to: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s removed the room name"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"You removed the room name"</string>
|
||||
<string name="state_event_room_reject">"%1$s rejected the invitation"</string>
|
||||
<string name="state_event_room_reject_by_you">"You rejected the invitation"</string>
|
||||
<string name="state_event_room_remove">"%1$s removed %2$s"</string>
|
||||
<string name="state_event_room_remove_by_you">"You removed %1$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s sent an invitation to %2$s to join the room"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"You sent an invitation to %1$s to join the room"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s revoked the invitation for %2$s to join the room"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"You revoked the invitation for %1$s to join the room"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s changed the topic to: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"You changed the topic to: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s removed the room topic"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"You removed the room topic"</string>
|
||||
<string name="state_event_room_unban">"%1$s unbanned %2$s"</string>
|
||||
<string name="state_event_room_unban_by_you">"You unbanned %1$s"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s made an unknown change to their membership"</string>
|
||||
</resources>
|
||||
|
|
@ -20,12 +20,16 @@ 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.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.fake.LeaveRoomPresenterFake
|
||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
|
|
@ -54,6 +58,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -82,6 +87,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -103,6 +109,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -132,6 +139,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -164,6 +172,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -202,6 +211,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -253,6 +263,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -278,6 +289,7 @@ class RoomListPresenterTests {
|
|||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(inviteStateFlow),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -297,6 +309,89 @@ class RoomListPresenterTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show context menu`() = runTest {
|
||||
val presenter = RoomListPresenter(
|
||||
FakeMatrixClient(A_SESSION_ID),
|
||||
createDateFormatter(),
|
||||
FakeRoomLastMessageFormatter(),
|
||||
FakeSessionVerificationService(),
|
||||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
|
||||
val initialState = awaitItem()
|
||||
val summary = aRoomListRoomSummary()
|
||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
||||
|
||||
val shownState = awaitItem()
|
||||
Truth.assertThat(shownState.contextMenu)
|
||||
.isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - hide context menu`() = runTest {
|
||||
val presenter = RoomListPresenter(
|
||||
FakeMatrixClient(A_SESSION_ID),
|
||||
createDateFormatter(),
|
||||
FakeRoomLastMessageFormatter(),
|
||||
FakeSessionVerificationService(),
|
||||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
LeaveRoomPresenterFake(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
|
||||
val initialState = awaitItem()
|
||||
val summary = aRoomListRoomSummary()
|
||||
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
|
||||
|
||||
val shownState = awaitItem()
|
||||
Truth.assertThat(shownState.contextMenu)
|
||||
.isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name))
|
||||
shownState.eventSink(RoomListEvents.HideContextMenu)
|
||||
|
||||
val hiddenState = awaitItem()
|
||||
Truth.assertThat(hiddenState.contextMenu).isEqualTo(RoomListState.ContextMenu.Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - leave room calls into leave room presenter`() = runTest {
|
||||
val leaveRoomPresenter = LeaveRoomPresenterFake()
|
||||
val presenter = RoomListPresenter(
|
||||
FakeMatrixClient(A_SESSION_ID),
|
||||
createDateFormatter(),
|
||||
FakeRoomLastMessageFormatter(),
|
||||
FakeSessionVerificationService(),
|
||||
FakeNetworkMonitor(),
|
||||
SnackbarDispatcher(),
|
||||
FakeInviteDataSource(),
|
||||
leaveRoomPresenter,
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID))
|
||||
|
||||
Truth.assertThat(leaveRoomPresenter.events).containsExactly(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDateFormatter(): LastMessageTimestampFormatter {
|
||||
return FakeLastMessageTimestampFormatter().apply {
|
||||
givenFormat(A_FORMATTED_DATE)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ datastore = "1.0.0"
|
|||
constraintlayout = "2.1.4"
|
||||
recyclerview = "1.3.0"
|
||||
lifecycle = "2.6.1"
|
||||
activity = "1.7.1"
|
||||
activity = "1.7.2"
|
||||
startup = "1.1.1"
|
||||
media3 = "1.0.2"
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ telephoto = "0.3.0"
|
|||
|
||||
# DI
|
||||
dagger = "2.46.1"
|
||||
anvil = "2.4.5"
|
||||
anvil = "2.4.6"
|
||||
|
||||
# quality
|
||||
detekt = "1.22.0"
|
||||
|
|
@ -55,6 +55,8 @@ dependencygraph = "0.10"
|
|||
[libraries]
|
||||
# Project
|
||||
android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" }
|
||||
# https://developer.android.com/studio/write/java8-support#library-desugaring-versions
|
||||
android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3"
|
||||
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
# https://firebase.google.com/docs/android/setup#available-libraries
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:32.0.0"
|
||||
|
|
@ -153,7 +155,7 @@ anvil_compiler_api = { module = "com.squareup.anvil:compiler-api", version.ref =
|
|||
anvil_compiler_utils = { module = "com.squareup.anvil:compiler-utils", version.ref = "anvil" }
|
||||
|
||||
# Composer
|
||||
wysiwyg = "io.element.android:wysiwyg:2.1.0"
|
||||
wysiwyg = "io.element.android:wysiwyg:2.2.1"
|
||||
|
||||
# Miscellaneous
|
||||
# Add unused dependency to androidx.compose.compiler:compiler to let Renovate create PR to change the
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ object VectorIcons {
|
|||
val Delete = R.drawable.ic_baseline_delete_outline_24
|
||||
val Reply = R.drawable.ic_baseline_reply_24
|
||||
val Edit = R.drawable.ic_baseline_edit_24
|
||||
val DoorOpen = R.drawable.ic_door_open_24
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ModalBottomSheet(
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||
shape: Shape = BottomSheetDefaults.ExpandedShape,
|
||||
containerColor: Color = BottomSheetDefaults.ContainerColor,
|
||||
contentColor: Color = contentColorFor(containerColor),
|
||||
tonalElevation: Dp = BottomSheetDefaults.Elevation,
|
||||
scrimColor: Color = BottomSheetDefaults.ScrimColor,
|
||||
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
androidx.compose.material3.ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier,
|
||||
sheetState = sheetState,
|
||||
shape = shape,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
tonalElevation = tonalElevation,
|
||||
scrimColor = scrimColor,
|
||||
dragHandle = dragHandle,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380
|
||||
@Preview(group = PreviewGroup.BottomSheets)
|
||||
@Composable
|
||||
internal fun ModalBottomSheetLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380
|
||||
@Preview(group = PreviewGroup.BottomSheets)
|
||||
@Composable
|
||||
internal fun ModalBottomSheetDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {},
|
||||
sheetState = SheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
initialValue = SheetValue.Expanded,
|
||||
skipHiddenState = true,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = "Sheet Content",
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 20.dp)
|
||||
.background(color = Color.Green)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="#000000">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M440,520Q457,520 468.5,508.5Q480,497 480,480Q480,463 468.5,451.5Q457,440 440,440Q423,440 411.5,451.5Q400,463 400,480Q400,497 411.5,508.5Q423,520 440,520ZM280,840L280,760L520,720L520,275Q520,260 511,248Q502,236 488,234L280,200L280,120L500,156Q544,164 572,197Q600,230 600,274L600,786L280,840ZM120,840L120,760L200,760L200,200Q200,166 223.5,143Q247,120 280,120L680,120Q714,120 737,143Q760,166 760,200L760,760L840,760L840,840L120,840ZM280,760L680,760L680,200Q680,200 680,200Q680,200 680,200L280,200Q280,200 280,200Q280,200 280,200L280,760Z"/>
|
||||
</vector>
|
||||
27
libraries/eventformatter/api/build.gradle.kts
Normal file
27
libraries/eventformatter/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.eventformatter.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
@ -14,10 +14,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl
|
||||
package io.element.android.libraries.eventformatter.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
|
||||
interface RoomLastMessageFormatter {
|
||||
fun processMessageItem(event: EventTimelineItem, isDmRoom: Boolean): CharSequence?
|
||||
fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence?
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.libraries.eventformatter.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
|
||||
interface TimelineEventFormatter {
|
||||
fun format(event: EventTimelineItem): CharSequence?
|
||||
}
|
||||
52
libraries/eventformatter/impl/build.gradle.kts
Normal file
52
libraries/eventformatter/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.eventformatter.impl"
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.anvilannotations)
|
||||
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.services.toolbox.api)
|
||||
api(projects.libraries.eventformatter.api)
|
||||
|
||||
testImplementation(projects.services.toolbox.impl)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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.libraries.eventformatter.impl
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||
private val sp: StringProvider,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val roomMembershipContentFormatter: RoomMembershipContentFormatter,
|
||||
private val profileChangeContentFormatter: ProfileChangeContentFormatter,
|
||||
private val stateContentFormatter: StateContentFormatter,
|
||||
) : RoomLastMessageFormatter {
|
||||
|
||||
override fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? {
|
||||
val isOutgoing = event.sender == matrixClient.sessionId
|
||||
val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value
|
||||
return when (val content = event.content) {
|
||||
is MessageContent -> processMessageContents(content, senderDisplayName, isDmRoom)
|
||||
RedactedContent -> {
|
||||
val message = sp.getString(StringR.string.common_message_removed)
|
||||
if (!isDmRoom) {
|
||||
prefix(message, senderDisplayName)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
is StickerContent -> {
|
||||
content.body
|
||||
}
|
||||
is UnableToDecryptContent -> {
|
||||
val message = sp.getString(StringR.string.common_decryption_error)
|
||||
if (!isDmRoom) {
|
||||
prefix(message, senderDisplayName)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
is RoomMembershipContent -> {
|
||||
roomMembershipContentFormatter.format(content, senderDisplayName, isOutgoing)
|
||||
}
|
||||
is ProfileChangeContent -> {
|
||||
profileChangeContentFormatter.format(content, senderDisplayName, isOutgoing)
|
||||
}
|
||||
is StateContent -> {
|
||||
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList)
|
||||
}
|
||||
is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> {
|
||||
prefixIfNeeded(sp.getString(StringR.string.common_unsupported_event), senderDisplayName, isDmRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processMessageContents(messageContent: MessageContent, senderDisplayName: String, isDmRoom: Boolean): CharSequence? {
|
||||
val messageType: MessageType = messageContent.type ?: return null
|
||||
|
||||
val internalMessage = when (messageType) {
|
||||
// Doesn't need a prefix
|
||||
is EmoteMessageType -> {
|
||||
return "- $senderDisplayName ${messageType.body}"
|
||||
}
|
||||
is TextMessageType -> {
|
||||
messageType.body
|
||||
}
|
||||
is VideoMessageType -> {
|
||||
sp.getString(StringR.string.common_video)
|
||||
}
|
||||
is ImageMessageType -> {
|
||||
sp.getString(StringR.string.common_image)
|
||||
}
|
||||
is FileMessageType -> {
|
||||
sp.getString(StringR.string.common_file)
|
||||
}
|
||||
is AudioMessageType -> {
|
||||
sp.getString(StringR.string.common_audio)
|
||||
}
|
||||
UnknownMessageType -> {
|
||||
sp.getString(StringR.string.common_unsupported_event)
|
||||
}
|
||||
is NoticeMessageType -> {
|
||||
messageType.body
|
||||
}
|
||||
}
|
||||
return prefixIfNeeded(internalMessage, senderDisplayName, isDmRoom)
|
||||
}
|
||||
|
||||
private fun prefixIfNeeded(message: String, senderDisplayName: String, isDmRoom: Boolean): CharSequence = if (isDmRoom) {
|
||||
message
|
||||
} else {
|
||||
prefix(message, senderDisplayName)
|
||||
}
|
||||
|
||||
private fun prefix(message: String, senderDisplayName: String): AnnotatedString {
|
||||
return buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(senderDisplayName)
|
||||
}
|
||||
append(": ")
|
||||
append(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.libraries.eventformatter.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultTimelineEventFormatter @Inject constructor(
|
||||
private val sp: StringProvider,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val roomMembershipContentFormatter: RoomMembershipContentFormatter,
|
||||
private val profileChangeContentFormatter: ProfileChangeContentFormatter,
|
||||
private val stateContentFormatter: StateContentFormatter,
|
||||
) : TimelineEventFormatter {
|
||||
|
||||
override fun format(event: EventTimelineItem): CharSequence? {
|
||||
val isOutgoing = event.sender == matrixClient.sessionId
|
||||
val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value
|
||||
return when (val content = event.content) {
|
||||
is RoomMembershipContent -> {
|
||||
roomMembershipContentFormatter.format(content, senderDisplayName, isOutgoing)
|
||||
}
|
||||
is ProfileChangeContent -> {
|
||||
profileChangeContentFormatter.format(content, senderDisplayName, isOutgoing)
|
||||
}
|
||||
is StateContent -> {
|
||||
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.Timeline)
|
||||
}
|
||||
RedactedContent,
|
||||
is StickerContent,
|
||||
is UnableToDecryptContent,
|
||||
is MessageContent,
|
||||
is FailedToParseMessageLikeContent,
|
||||
is FailedToParseStateContent,
|
||||
is UnknownContent -> {
|
||||
if (buildMeta.isDebuggable) {
|
||||
error("You should not use this formatter for this event: $event")
|
||||
}
|
||||
sp.getString(R.string.common_unsupported_event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.libraries.eventformatter.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class ProfileChangeContentFormatter @Inject constructor(
|
||||
private val sp: StringProvider,
|
||||
) {
|
||||
fun format(
|
||||
profileChangeContent: ProfileChangeContent,
|
||||
senderDisplayName: String,
|
||||
senderIsYou: Boolean,
|
||||
): String? = profileChangeContent.run {
|
||||
val displayNameChanged = displayName != prevDisplayName
|
||||
val avatarChanged = avatarUrl != prevAvatarUrl
|
||||
return when {
|
||||
avatarChanged && displayNameChanged -> {
|
||||
val message = format(profileChangeContent.copy(avatarUrl = null, prevAvatarUrl = null), senderDisplayName, senderIsYou)
|
||||
val avatarChangedToo = sp.getString(R.string.state_event_avatar_changed_too)
|
||||
"$message\n$avatarChangedToo"
|
||||
}
|
||||
displayNameChanged -> {
|
||||
if (displayName != null && prevDisplayName != null) {
|
||||
if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_display_name_changed_from_by_you, prevDisplayName, displayName)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_display_name_changed_from, senderDisplayName, prevDisplayName, displayName)
|
||||
}
|
||||
} else if (displayName != null) {
|
||||
if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_display_name_set_by_you, displayName)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_display_name_set, senderDisplayName, displayName)
|
||||
}
|
||||
} else {
|
||||
if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_display_name_removed_by_you, prevDisplayName)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_display_name_removed, senderDisplayName, prevDisplayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
avatarChanged -> {
|
||||
if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_avatar_url_changed_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_avatar_url_changed, senderDisplayName)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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.libraries.eventformatter.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomMembershipContentFormatter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val sp: StringProvider,
|
||||
) {
|
||||
fun format(
|
||||
membershipContent: RoomMembershipContent,
|
||||
senderDisplayName: String,
|
||||
senderIsYou: Boolean,
|
||||
): CharSequence? {
|
||||
val userId = membershipContent.userId
|
||||
val memberIsYou = userId == matrixClient.sessionId
|
||||
return when (val change = membershipContent.change) {
|
||||
MembershipChange.JOINED -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_join_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_join, userId.value)
|
||||
}
|
||||
MembershipChange.LEFT -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_leave_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_leave, userId.value)
|
||||
}
|
||||
MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_ban_by_you, userId.value)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_ban, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.UNBANNED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_unban_by_you, userId.value)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_unban, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.KICKED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_remove_by_you, userId.value)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_remove, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.INVITED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_invite_by_you, userId.value)
|
||||
} else if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_invite_you, senderDisplayName)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_invite, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.INVITATION_ACCEPTED -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_invite_accepted_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_invite_accepted, userId.value)
|
||||
}
|
||||
MembershipChange.INVITATION_REJECTED -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_reject_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_reject, userId.value)
|
||||
}
|
||||
MembershipChange.INVITATION_REVOKED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.KNOCKED -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_knock_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_knock, userId.value)
|
||||
}
|
||||
MembershipChange.KNOCK_ACCEPTED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_knock_accepted_by_you, userId.value)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_knock_accepted, senderDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.KNOCK_RETRACTED -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_knock_retracted_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_knock_retracted, userId.value)
|
||||
}
|
||||
MembershipChange.KNOCK_DENIED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_knock_denied_by_you, userId.value)
|
||||
} else if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_knock_denied_you, senderDisplayName)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_knock_denied, senderDisplayName, userId.value)
|
||||
}
|
||||
else -> {
|
||||
Timber.v("Filtering timeline item for room membership: $membershipContent")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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