Florian Renaud
1 year ago
29 changed files with 628 additions and 14 deletions
@ -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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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