ganfra
6 months ago
9 changed files with 583 additions and 30 deletions
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
/* |
||||
* Copyright (c) 2024 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.appnav.room.join |
||||
|
||||
sealed interface JoinRoomEvents { |
||||
data object JoinRoom: JoinRoomEvents |
||||
data object AcceptInvite : JoinRoomEvents |
||||
data object DeclineInvite : JoinRoomEvents |
||||
} |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/* |
||||
* Copyright (c) 2024 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.appnav.room.join |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
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.libraries.architecture.NodeInputs |
||||
import io.element.android.libraries.architecture.inputs |
||||
import io.element.android.libraries.di.SessionScope |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
|
||||
@ContributesNode(SessionScope::class) |
||||
class JoinRoomNode @AssistedInject constructor( |
||||
@Assisted buildContext: BuildContext, |
||||
@Assisted plugins: List<Plugin>, |
||||
presenterFactory: JoinRoomPresenter.Factory, |
||||
) : Node(buildContext, plugins = plugins) { |
||||
|
||||
data class Inputs( |
||||
val roomId: RoomId, |
||||
) : NodeInputs |
||||
|
||||
private val inputs: Inputs = inputs() |
||||
private val presenter = presenterFactory.create(inputs.roomId) |
||||
|
||||
@Composable |
||||
override fun View(modifier: Modifier) { |
||||
val state = presenter.present() |
||||
JoinRoomView( |
||||
state = state, |
||||
onBackPressed = ::navigateUp, |
||||
modifier = modifier |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,104 @@
@@ -0,0 +1,104 @@
|
||||
/* |
||||
* Copyright (c) 2024 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.appnav.room.join |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.collectAsState |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.produceState |
||||
import androidx.compose.runtime.rememberCoroutineScope |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.libraries.architecture.AsyncData |
||||
import io.element.android.libraries.architecture.Presenter |
||||
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.CurrentUserMembership |
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService |
||||
import kotlinx.coroutines.launch |
||||
import java.util.Optional |
||||
import kotlin.jvm.optionals.getOrNull |
||||
|
||||
class JoinRoomPresenter @AssistedInject constructor( |
||||
@Assisted private val roomId: RoomId, |
||||
private val matrixClient: MatrixClient, |
||||
private val roomListService: RoomListService, |
||||
) : Presenter<JoinRoomState> { |
||||
|
||||
interface Factory { |
||||
fun create(roomId: RoomId): JoinRoomPresenter |
||||
} |
||||
|
||||
@Composable |
||||
override fun present(): JoinRoomState { |
||||
val userMembership by roomListService.getUserMembershipForRoom(roomId).collectAsState(initial = Optional.empty()) |
||||
val joinAuthorisationStatus = joinAuthorisationStatus(userMembership) |
||||
val roomInfo by produceState<AsyncData<RoomInfo>>(initialValue = AsyncData.Uninitialized, key1 = userMembership) { |
||||
when { |
||||
userMembership.isPresent -> { |
||||
val roomInfo = matrixClient.getRoom(roomId)?.let { |
||||
RoomInfo( |
||||
roomId = it.roomId, |
||||
roomName = it.displayName, |
||||
roomAlias = it.alias, |
||||
memberCount = it.activeMemberCount, |
||||
roomAvatarUrl = it.avatarUrl |
||||
) |
||||
} |
||||
value = roomInfo?.let { AsyncData.Success(it) } ?: AsyncData.Failure(Exception("Failed to load room info")) |
||||
} |
||||
else -> { |
||||
value = AsyncData.Uninitialized |
||||
} |
||||
} |
||||
} |
||||
|
||||
val coroutineScope = rememberCoroutineScope() |
||||
|
||||
fun handleEvents(event: JoinRoomEvents) { |
||||
when (event) { |
||||
JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> { |
||||
coroutineScope.launch { |
||||
matrixClient.joinRoom(roomId) |
||||
} |
||||
} |
||||
JoinRoomEvents.DeclineInvite -> { |
||||
coroutineScope.launch { |
||||
matrixClient.getRoom(roomId)?.use { |
||||
it.leave() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return JoinRoomState( |
||||
roomInfo = roomInfo, |
||||
joinAuthorisationStatus = joinAuthorisationStatus, |
||||
currentAction = CurrentAction.None, |
||||
eventSink = ::handleEvents |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
private fun joinAuthorisationStatus(userMembership: Optional<CurrentUserMembership>): JoinAuthorisationStatus { |
||||
return when { |
||||
userMembership.getOrNull() == CurrentUserMembership.INVITED -> return JoinAuthorisationStatus.IsInvited |
||||
else -> JoinAuthorisationStatus.Unknown |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
/* |
||||
* Copyright (c) 2024 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.appnav.room.join |
||||
|
||||
import androidx.compose.runtime.Immutable |
||||
import io.element.android.libraries.architecture.AsyncData |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
|
||||
@Immutable |
||||
data class JoinRoomState( |
||||
val roomInfo: AsyncData<RoomInfo>, |
||||
val joinAuthorisationStatus: JoinAuthorisationStatus, |
||||
val currentAction: CurrentAction, |
||||
val eventSink: (JoinRoomEvents) -> Unit |
||||
) |
||||
|
||||
data class RoomInfo( |
||||
val roomId: RoomId, |
||||
val roomName: String, |
||||
val roomAlias: String?, |
||||
val memberCount: Long?, |
||||
val roomAvatarUrl: String?, |
||||
) { |
||||
fun avatarData(size: AvatarSize): AvatarData { |
||||
return AvatarData( |
||||
id = roomId.value, |
||||
name = roomName, |
||||
url = roomAvatarUrl, |
||||
size = size, |
||||
) |
||||
} |
||||
} |
||||
|
||||
enum class JoinAuthorisationStatus { |
||||
IsInvited, |
||||
CanKnock, |
||||
CanJoin, |
||||
Unknown, |
||||
} |
||||
|
||||
sealed interface CurrentAction { |
||||
data object None : CurrentAction |
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
/* |
||||
* Copyright (c) 2024 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.appnav.room.join |
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||
import io.element.android.libraries.architecture.AsyncData |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
|
||||
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> { |
||||
override val values: Sequence<JoinRoomState> |
||||
get() = sequenceOf( |
||||
aJoinRoomState( |
||||
roomInfo = AsyncData.Uninitialized |
||||
), |
||||
aJoinRoomState( |
||||
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin |
||||
), |
||||
aJoinRoomState( |
||||
joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock |
||||
), |
||||
aJoinRoomState( |
||||
joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited |
||||
), |
||||
) |
||||
} |
||||
|
||||
fun aJoinRoomState( |
||||
roomInfo: AsyncData<RoomInfo> = AsyncData.Success( |
||||
RoomInfo( |
||||
roomId = RoomId("@exa:matrix.org"), |
||||
roomName = "Element x android", |
||||
roomAlias = "#exa:matrix.org", |
||||
memberCount = null, |
||||
roomAvatarUrl = null |
||||
) |
||||
), |
||||
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown, |
||||
currentAction: CurrentAction = CurrentAction.None, |
||||
eventSink: (JoinRoomEvents) -> Unit = {} |
||||
) = JoinRoomState( |
||||
roomInfo = roomInfo, |
||||
joinAuthorisationStatus = joinAuthorisationStatus, |
||||
currentAction = currentAction, |
||||
eventSink = eventSink |
||||
) |
||||
|
@ -0,0 +1,211 @@
@@ -0,0 +1,211 @@
|
||||
/* |
||||
* Copyright (c) 2024 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.appnav.room.join |
||||
|
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.width |
||||
import androidx.compose.material3.ExperimentalMaterial3Api |
||||
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.text.style.TextAlign |
||||
import androidx.compose.ui.text.style.TextOverflow |
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark |
||||
import androidx.compose.ui.tooling.preview.PreviewParameter |
||||
import androidx.compose.ui.unit.dp |
||||
import io.element.android.compound.theme.ElementTheme |
||||
import io.element.android.libraries.architecture.AsyncData |
||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom |
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule |
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule |
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage |
||||
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.preview.ElementPreview |
||||
import io.element.android.libraries.designsystem.theme.components.Button |
||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize |
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton |
||||
import io.element.android.libraries.designsystem.theme.components.Text |
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar |
||||
import io.element.android.libraries.ui.strings.CommonStrings |
||||
|
||||
@Composable |
||||
fun JoinRoomView( |
||||
state: JoinRoomState, |
||||
onBackPressed: () -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
HeaderFooterPage( |
||||
modifier = modifier, |
||||
topBar = { |
||||
JoinRoomTopBar(asyncRoomInfo = state.roomInfo, onBackClicked = onBackPressed) |
||||
}, |
||||
content = { |
||||
JoinRoomContent(state = state) |
||||
}, |
||||
footer = { |
||||
JoinRoomFooter( |
||||
joinAuthorisationStatus = state.joinAuthorisationStatus, |
||||
onAcceptInvite = { |
||||
state.eventSink(JoinRoomEvents.AcceptInvite) |
||||
}, |
||||
onDeclineInvite = { |
||||
state.eventSink(JoinRoomEvents.DeclineInvite) |
||||
}, |
||||
onJoinRoom = { |
||||
state.eventSink(JoinRoomEvents.JoinRoom) |
||||
}, |
||||
) |
||||
} |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
private fun JoinRoomFooter( |
||||
joinAuthorisationStatus: JoinAuthorisationStatus, |
||||
onAcceptInvite: () -> Unit, |
||||
onDeclineInvite: () -> Unit, |
||||
onJoinRoom: () -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
when (joinAuthorisationStatus) { |
||||
JoinAuthorisationStatus.IsInvited -> { |
||||
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) { |
||||
OutlinedButton( |
||||
text = stringResource(CommonStrings.action_decline), |
||||
onClick = onDeclineInvite, |
||||
modifier = Modifier.weight(1f), |
||||
size = ButtonSize.Medium, |
||||
) |
||||
Button( |
||||
text = stringResource(CommonStrings.action_accept), |
||||
onClick = onAcceptInvite, |
||||
modifier = Modifier.weight(1f), |
||||
size = ButtonSize.Medium, |
||||
) |
||||
} |
||||
} |
||||
// TODO handle all cases properly |
||||
else -> { |
||||
Button( |
||||
text = stringResource(CommonStrings.action_join), |
||||
onClick = onJoinRoom, |
||||
modifier = modifier.fillMaxWidth(), |
||||
size = ButtonSize.Medium, |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun JoinRoomContent( |
||||
state: JoinRoomState, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
Column( |
||||
modifier = modifier.padding(all = 16.dp), |
||||
horizontalAlignment = Alignment.CenterHorizontally |
||||
) { |
||||
Spacer(modifier = Modifier.height(80.dp)) |
||||
when (state.roomInfo) { |
||||
is AsyncData.Success -> { |
||||
val roomInfo = state.roomInfo.data |
||||
Avatar(avatarData = roomInfo.avatarData(AvatarSize.RoomHeader)) |
||||
} |
||||
else -> { |
||||
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) |
||||
} |
||||
} |
||||
Spacer(modifier = Modifier.height(16.dp)) |
||||
Text( |
||||
text = "Preview is not available", |
||||
style = ElementTheme.typography.fontHeadingMdBold, |
||||
textAlign = TextAlign.Center, |
||||
color = ElementTheme.colors.textPrimary, |
||||
) |
||||
Spacer(modifier = Modifier.height(8.dp)) |
||||
Text( |
||||
text = "You must be a member of this room to view the message history.", |
||||
style = ElementTheme.typography.fontBodyMdRegular, |
||||
textAlign = TextAlign.Center, |
||||
color = ElementTheme.colors.textSecondary, |
||||
) |
||||
} |
||||
} |
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class) |
||||
@Composable |
||||
private fun JoinRoomTopBar( |
||||
asyncRoomInfo: AsyncData<RoomInfo>, |
||||
onBackClicked: () -> Unit, |
||||
) { |
||||
TopAppBar( |
||||
navigationIcon = { |
||||
BackButton(onClick = onBackClicked) |
||||
}, |
||||
title = { |
||||
when (asyncRoomInfo) { |
||||
is AsyncData.Success -> { |
||||
val roomInfo = asyncRoomInfo.data |
||||
RoomAvatarAndNameRow(roomName = roomInfo.roomName, roomAvatar = roomInfo.avatarData(AvatarSize.TimelineRoom)) |
||||
} |
||||
else -> { |
||||
IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp) |
||||
} |
||||
} |
||||
}, |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
private fun RoomAvatarAndNameRow( |
||||
roomName: String, |
||||
roomAvatar: AvatarData, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
Row( |
||||
modifier = modifier, |
||||
verticalAlignment = Alignment.CenterVertically |
||||
) { |
||||
Avatar(roomAvatar) |
||||
Spacer(modifier = Modifier.width(8.dp)) |
||||
Text( |
||||
text = roomName, |
||||
style = ElementTheme.typography.fontBodyLgMedium, |
||||
maxLines = 1, |
||||
overflow = TextOverflow.Ellipsis |
||||
) |
||||
} |
||||
} |
||||
|
||||
@PreviewLightDark |
||||
@Composable |
||||
fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview { |
||||
JoinRoomView( |
||||
state = state, |
||||
onBackPressed = { } |
||||
) |
||||
} |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* Copyright (c) 2024 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.appnav.room.join.di |
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo |
||||
import dagger.Module |
||||
import dagger.Provides |
||||
import io.element.android.appnav.room.join.JoinRoomPresenter |
||||
import io.element.android.libraries.di.SessionScope |
||||
import io.element.android.libraries.matrix.api.MatrixClient |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService |
||||
|
||||
@Module |
||||
@ContributesTo(SessionScope::class) |
||||
object JoinRoomModule { |
||||
@Provides |
||||
fun providesJoinRoomPresenterFactory( |
||||
roomListService: RoomListService, |
||||
client: MatrixClient, |
||||
): JoinRoomPresenter.Factory { |
||||
return object : JoinRoomPresenter.Factory { |
||||
override fun create(roomId: RoomId): JoinRoomPresenter { |
||||
return JoinRoomPresenter( |
||||
roomId = roomId, |
||||
matrixClient = client, |
||||
roomListService = roomListService |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue