Florian Renaud
1 year ago
29 changed files with 628 additions and 14 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Implement room member details screen |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomdetails.impl.members.details |
||||
|
||||
// TODO Add your events or remove the file completely if no events |
||||
sealed interface RoomMemberDetailsEvents { |
||||
object MyEvent : RoomMemberDetailsEvents |
||||
} |
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
/* |
||||
* 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.roomdetails.impl.members.details |
||||
|
||||
import android.content.Context |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.platform.LocalContext |
||||
import com.bumble.appyx.core.modality.BuildContext |
||||
import com.bumble.appyx.core.node.Node |
||||
import com.bumble.appyx.core.plugin.Plugin |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.anvilannotations.ContributesNode |
||||
import io.element.android.features.roomdetails.impl.R |
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent |
||||
import io.element.android.libraries.architecture.NodeInputs |
||||
import io.element.android.libraries.architecture.inputs |
||||
import io.element.android.libraries.di.RoomScope |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder |
||||
import io.element.android.libraries.matrix.api.room.RoomMember |
||||
import timber.log.Timber |
||||
import io.element.android.libraries.androidutils.R as AndroidUtilsR |
||||
|
||||
@ContributesNode(RoomScope::class) |
||||
class RoomMemberDetailsNode @AssistedInject constructor( |
||||
@Assisted buildContext: BuildContext, |
||||
@Assisted plugins: List<Plugin>, |
||||
presenterFactory: RoomMemberDetailsPresenter.Factory, |
||||
) : Node(buildContext, plugins = plugins) { |
||||
|
||||
data class Inputs( |
||||
val member: RoomMember, |
||||
) : NodeInputs |
||||
|
||||
private val inputs = inputs<Inputs>() |
||||
private val presenter = presenterFactory.create(inputs.member) |
||||
|
||||
private fun onShareUser(context: Context) { |
||||
val permalinkResult = PermalinkBuilder.permalinkForUser(UserId(inputs.member.userId)) |
||||
permalinkResult.onSuccess { permalink -> |
||||
startSharePlainTextIntent( |
||||
context = context, |
||||
activityResultLauncher = null, |
||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title), |
||||
text = permalink, |
||||
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found) |
||||
) |
||||
}.onFailure { |
||||
Timber.e(it) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
override fun View(modifier: Modifier) { |
||||
val context = LocalContext.current |
||||
val state = presenter.present() |
||||
RoomMemberDetailsView( |
||||
state = state, |
||||
modifier = modifier, |
||||
goBack = { navigateUp() }, |
||||
onShareUser = { onShareUser(context) } |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
/* |
||||
* 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.roomdetails.impl.members.details |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.libraries.architecture.Presenter |
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom |
||||
import io.element.android.libraries.matrix.api.room.RoomMember |
||||
|
||||
class RoomMemberDetailsPresenter @AssistedInject constructor( |
||||
private val room: MatrixRoom, |
||||
@Assisted private val roomMember: RoomMember, |
||||
) : Presenter<RoomMemberDetailsState> { |
||||
|
||||
interface Factory { |
||||
fun create(roomMember: RoomMember): RoomMemberDetailsPresenter |
||||
} |
||||
|
||||
@Composable |
||||
override fun present(): RoomMemberDetailsState { |
||||
|
||||
// fun handleEvents(event: RoomMemberDetailsEvents) { |
||||
// when (event) { |
||||
// } |
||||
// } |
||||
|
||||
return RoomMemberDetailsState( |
||||
userId = roomMember.userId, |
||||
userName = roomMember.displayName, |
||||
avatarUrl = roomMember.avatarUrl, |
||||
isBlocked = roomMember.isIgnored, |
||||
// eventSink = ::handleEvents |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,25 @@
@@ -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.roomdetails.impl.members.details |
||||
|
||||
data class RoomMemberDetailsState( |
||||
val userId: String, |
||||
val userName: String?, |
||||
val avatarUrl: String?, |
||||
val isBlocked: Boolean, |
||||
// val eventSink: (RoomMemberDetailsEvents) -> Unit |
||||
) |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
/* |
||||
* 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.roomdetails.impl.members.details |
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||
|
||||
open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberDetailsState> { |
||||
override val values: Sequence<RoomMemberDetailsState> |
||||
get() = sequenceOf( |
||||
aRoomMemberDetailsState(), |
||||
aRoomMemberDetailsState().copy(userName = null), |
||||
aRoomMemberDetailsState().copy(isBlocked = true), |
||||
// Add other states here |
||||
) |
||||
} |
||||
|
||||
fun aRoomMemberDetailsState() = RoomMemberDetailsState( |
||||
userId = "@daniel:domain.com", |
||||
userName = "Daniel", |
||||
avatarUrl = null, |
||||
isBlocked = false, |
||||
// eventSink = {}, |
||||
) |
@ -0,0 +1,177 @@
@@ -0,0 +1,177 @@
|
||||
/* |
||||
* 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.roomdetails.impl.members.details |
||||
|
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.size |
||||
import androidx.compose.foundation.rememberScrollState |
||||
import androidx.compose.foundation.verticalScroll |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.outlined.Block |
||||
import androidx.compose.material.icons.outlined.ChatBubbleOutline |
||||
import androidx.compose.material.icons.outlined.Share |
||||
import androidx.compose.material3.ExperimentalMaterial3Api |
||||
import androidx.compose.material3.MaterialTheme |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
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.features.roomdetails.impl.R |
||||
import io.element.android.libraries.designsystem.ElementTextStyles |
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize |
||||
import io.element.android.libraries.designsystem.components.button.BackButton |
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory |
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText |
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark |
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight |
||||
import io.element.android.libraries.designsystem.theme.LocalColors |
||||
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.ui.strings.R as StringR |
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class) |
||||
@Composable |
||||
fun RoomMemberDetailsView( |
||||
state: RoomMemberDetailsState, |
||||
onShareUser: () -> Unit, |
||||
goBack: () -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
Scaffold( |
||||
modifier = modifier, |
||||
topBar = { |
||||
TopAppBar(title = { }, navigationIcon = { BackButton(onClick = goBack) }) |
||||
}, |
||||
) { padding -> |
||||
Column( |
||||
modifier = Modifier |
||||
.padding(padding) |
||||
.verticalScroll(rememberScrollState()) |
||||
) { |
||||
HeaderSection( |
||||
avatarUrl = state.avatarUrl, |
||||
userId = state.userId, |
||||
userName = state.userName, |
||||
) |
||||
|
||||
ShareSection(onShareUser = onShareUser) |
||||
|
||||
SendMessageSection(onSendMessage = { |
||||
// TODO implement send DM |
||||
}) |
||||
|
||||
BlockSection(isBlocked = state.isBlocked, onToggleBlock = { |
||||
// TODO implement block & unblock |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
internal fun HeaderSection( |
||||
avatarUrl: String?, |
||||
userId: String, |
||||
userName: String?, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { |
||||
Box(modifier = Modifier.size(70.dp)) { |
||||
Avatar( |
||||
avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.HUGE), |
||||
modifier = Modifier.fillMaxSize() |
||||
) |
||||
} |
||||
Spacer(modifier = Modifier.height(30.dp)) |
||||
if (userName != null) { |
||||
Text(userName, style = ElementTextStyles.Bold.title1) |
||||
Spacer(modifier = Modifier.height(8.dp)) |
||||
} |
||||
Text(userId, style = ElementTextStyles.Regular.body, color = MaterialTheme.colorScheme.secondary) |
||||
Spacer(Modifier.height(32.dp)) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
internal fun ShareSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) { |
||||
PreferenceCategory(modifier = modifier) { |
||||
PreferenceText( |
||||
title = stringResource(StringR.string.action_share), |
||||
icon = Icons.Outlined.Share, |
||||
onClick = onShareUser, |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
internal fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier = Modifier) { |
||||
PreferenceCategory(modifier = modifier) { |
||||
PreferenceText( |
||||
title = stringResource(StringR.string.action_send_message), |
||||
icon = Icons.Outlined.ChatBubbleOutline, |
||||
onClick = onSendMessage, |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
internal fun BlockSection(isBlocked: Boolean, onToggleBlock: () -> Unit, modifier: Modifier = Modifier) { |
||||
PreferenceCategory(showDivider = false, modifier = modifier) { |
||||
if (isBlocked) { |
||||
PreferenceText( |
||||
title = stringResource(R.string.screen_dm_details_unblock_user), |
||||
icon = Icons.Outlined.Block, |
||||
) |
||||
} else { |
||||
PreferenceText( |
||||
title = stringResource(R.string.screen_dm_details_block_user), |
||||
icon = Icons.Outlined.Block, |
||||
tintColor = LocalColors.current.textActionCritical, |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Preview |
||||
@Composable |
||||
fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) = |
||||
ElementPreviewLight { ContentToPreview(state) } |
||||
|
||||
@Preview |
||||
@Composable |
||||
fun RoomMemberDetailsViewDarkPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) = |
||||
ElementPreviewDark { ContentToPreview(state) } |
||||
|
||||
@Composable |
||||
private fun ContentToPreview(state: RoomMemberDetailsState) { |
||||
RoomMemberDetailsView( |
||||
state = state, |
||||
onShareUser = {}, |
||||
goBack = {}, |
||||
) |
||||
} |
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
/* |
||||
* 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.roomdetails.members.details |
||||
|
||||
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.aMatrixRoom |
||||
import io.element.android.features.roomdetails.aRoomMember |
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter |
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
|
||||
@ExperimentalCoroutinesApi |
||||
class RoomMemberDetailsPresenterTests { |
||||
|
||||
@Test |
||||
fun `present - returns the room member's data`() = runTest { |
||||
val room = aMatrixRoom() |
||||
val roomMember = aRoomMember(displayName = "Alice") |
||||
val presenter = RoomMemberDetailsPresenter(room, roomMember) |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
Truth.assertThat(initialState.userId).isEqualTo(roomMember.userId) |
||||
Truth.assertThat(initialState.userName).isEqualTo(roomMember.displayName) |
||||
Truth.assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) |
||||
Truth.assertThat(initialState.isBlocked).isEqualTo(roomMember.isIgnored) |
||||
} |
||||
} |
||||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue