WIP pick avatar image
This commit is contained in:
parent
4e88700f19
commit
6641afdd17
13 changed files with 323 additions and 9 deletions
|
|
@ -48,6 +48,7 @@ dependencies {
|
||||||
implementation(projects.features.userlist.api)
|
implementation(projects.features.userlist.api)
|
||||||
api(projects.features.createroom.api)
|
api(projects.features.createroom.api)
|
||||||
implementation(libs.coil.compose) // FIXME temp
|
implementation(libs.coil.compose) // FIXME temp
|
||||||
|
implementation(projects.libraries.mediapickers)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,12 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
sealed interface ConfigureRoomEvents {
|
sealed interface ConfigureRoomEvents {
|
||||||
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
||||||
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
||||||
data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents
|
|
||||||
data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents
|
data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents
|
||||||
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
||||||
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
|
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListEvents
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.execute
|
import io.element.android.libraries.architecture.execute
|
||||||
|
|
@ -34,6 +37,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
||||||
import io.element.android.libraries.matrix.api.createroom.RoomPreset
|
import io.element.android.libraries.matrix.api.createroom.RoomPreset
|
||||||
import io.element.android.libraries.matrix.api.createroom.RoomVisibility
|
import io.element.android.libraries.matrix.api.createroom.RoomVisibility
|
||||||
|
import io.element.android.libraries.mediapickers.PickerProvider
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -41,6 +46,7 @@ import javax.inject.Inject
|
||||||
class ConfigureRoomPresenter @Inject constructor(
|
class ConfigureRoomPresenter @Inject constructor(
|
||||||
private val dataStore: CreateRoomDataStore,
|
private val dataStore: CreateRoomDataStore,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
|
private val mediaPickerProvider: PickerProvider,
|
||||||
) : Presenter<ConfigureRoomState> {
|
) : Presenter<ConfigureRoomState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -52,6 +58,13 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { uri ->
|
||||||
|
if (uri != null) dataStore.setAvatarUrl(uri.toString())
|
||||||
|
})
|
||||||
|
val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(onResult = { uri ->
|
||||||
|
if (uri != null) dataStore.setAvatarUrl(uri.toString())
|
||||||
|
})
|
||||||
|
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val createRoomAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
val createRoomAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||||
|
|
||||||
|
|
@ -62,7 +75,6 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
|
|
||||||
fun handleEvents(event: ConfigureRoomEvents) {
|
fun handleEvents(event: ConfigureRoomEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setAvatarUrl(event.uri?.toString())
|
|
||||||
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
||||||
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
||||||
is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy)
|
is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy)
|
||||||
|
|
@ -72,9 +84,27 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleAvatarEvents(event: AvatarActionListEvents) {
|
||||||
|
when (event) {
|
||||||
|
is AvatarActionListEvents.HandleAction -> when (event.action) {
|
||||||
|
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
||||||
|
AvatarAction.TakePhoto -> cameraPhotoPicker.launch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val avatarActionListState = AvatarActionListState(
|
||||||
|
actions = persistentListOf(
|
||||||
|
AvatarAction.ChoosePhoto,
|
||||||
|
AvatarAction.TakePhoto,
|
||||||
|
),
|
||||||
|
eventSink = ::handleAvatarEvents,
|
||||||
|
)
|
||||||
|
|
||||||
return ConfigureRoomState(
|
return ConfigureRoomState(
|
||||||
config = createRoomConfig.value,
|
config = createRoomConfig.value,
|
||||||
isCreateButtonEnabled = isCreateButtonEnabled,
|
isCreateButtonEnabled = isCreateButtonEnabled,
|
||||||
|
avatarActionListState = avatarActionListState,
|
||||||
createRoomAction = createRoomAction.value,
|
createRoomAction = createRoomAction.value,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,14 @@
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
data class ConfigureRoomState(
|
data class ConfigureRoomState(
|
||||||
val config: CreateRoomConfig,
|
val config: CreateRoomConfig,
|
||||||
val isCreateButtonEnabled: Boolean,
|
val isCreateButtonEnabled: Boolean,
|
||||||
|
val avatarActionListState: AvatarActionListState,
|
||||||
val createRoomAction: Async<RoomId>,
|
val createRoomAction: Async<RoomId>,
|
||||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,11 @@ package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState
|
||||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
||||||
override val values: Sequence<ConfigureRoomState>
|
override val values: Sequence<ConfigureRoomState>
|
||||||
|
|
@ -40,6 +43,10 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
fun aConfigureRoomState() = ConfigureRoomState(
|
fun aConfigureRoomState() = ConfigureRoomState(
|
||||||
config = CreateRoomConfig(),
|
config = CreateRoomConfig(),
|
||||||
isCreateButtonEnabled = false,
|
isCreateButtonEnabled = false,
|
||||||
|
avatarActionListState = AvatarActionListState(
|
||||||
|
actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto),
|
||||||
|
eventSink = {},
|
||||||
|
),
|
||||||
createRoomAction = Async.Uninitialized,
|
createRoomAction = Async.Uninitialized,
|
||||||
eventSink = {}
|
eventSink = { },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
|
@ -27,12 +26,15 @@ import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.ModalBottomSheetValue
|
||||||
|
import androidx.compose.material.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -44,6 +46,8 @@ import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.createroom.impl.components.Avatar
|
import io.element.android.features.createroom.impl.components.Avatar
|
||||||
import io.element.android.features.createroom.impl.components.LabelledTextField
|
import io.element.android.features.createroom.impl.components.LabelledTextField
|
||||||
import io.element.android.features.createroom.impl.components.RoomPrivacyOption
|
import io.element.android.features.createroom.impl.components.RoomPrivacyOption
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListEvents
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListView
|
||||||
import io.element.android.features.userlist.api.components.SelectedUsersList
|
import io.element.android.features.userlist.api.components.SelectedUsersList
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
|
|
@ -56,6 +60,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -65,13 +70,23 @@ fun ConfigureRoomView(
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
onRoomCreated: (RoomId) -> Unit = {},
|
onRoomCreated: (RoomId) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val itemActionsBottomSheetState = rememberModalBottomSheetState(
|
||||||
|
initialValue = ModalBottomSheetValue.Hidden,
|
||||||
|
)
|
||||||
|
|
||||||
if (state.createRoomAction is Async.Success) {
|
if (state.createRoomAction is Async.Success) {
|
||||||
LaunchedEffect(state.createRoomAction) {
|
LaunchedEffect(state.createRoomAction) {
|
||||||
onRoomCreated(state.createRoomAction.state)
|
onRoomCreated(state.createRoomAction.state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
fun onAvatarClicked() {
|
||||||
|
coroutineScope.launch {
|
||||||
|
itemActionsBottomSheetState.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|
@ -92,7 +107,7 @@ fun ConfigureRoomView(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
avatarUri = state.config.avatarUrl?.toUri(),
|
avatarUri = state.config.avatarUrl?.toUri(),
|
||||||
roomName = state.config.roomName.orEmpty(),
|
roomName = state.config.roomName.orEmpty(),
|
||||||
onAvatarClick = { Toast.makeText(context, "not implemented yet", Toast.LENGTH_SHORT).show() },
|
onAvatarClick = ::onAvatarClicked,
|
||||||
onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
|
onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
|
||||||
)
|
)
|
||||||
RoomTopic(
|
RoomTopic(
|
||||||
|
|
@ -114,10 +129,17 @@ fun ConfigureRoomView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AvatarActionListView(
|
||||||
|
state = state.avatarActionListState,
|
||||||
|
modalBottomSheetState = itemActionsBottomSheetState,
|
||||||
|
onActionSelected = { state.avatarActionListState.eventSink(AvatarActionListEvents.HandleAction(it)) }
|
||||||
|
)
|
||||||
|
|
||||||
when (state.createRoomAction) {
|
when (state.createRoomAction) {
|
||||||
is Async.Loading -> {
|
is Async.Loading -> {
|
||||||
ProgressDialog(text = stringResource(StringR.string.common_creating_room))
|
ProgressDialog(text = stringResource(StringR.string.common_creating_room))
|
||||||
}
|
}
|
||||||
|
|
||||||
is Async.Failure -> {
|
is Async.Failure -> {
|
||||||
RetryDialog(
|
RetryDialog(
|
||||||
content = stringResource(R.string.screen_create_room_error_creating_room),
|
content = stringResource(R.string.screen_create_room_error_creating_room),
|
||||||
|
|
@ -125,6 +147,7 @@ fun ConfigureRoomView(
|
||||||
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) },
|
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl.configureroom.avatar
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PhotoCamera
|
||||||
|
import androidx.compose.material.icons.filled.PhotoLibrary
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import io.element.android.libraries.ui.strings.R
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed class AvatarAction(
|
||||||
|
@StringRes val titleResId: Int,
|
||||||
|
val icon: ImageVector,
|
||||||
|
) {
|
||||||
|
object TakePhoto : AvatarAction(titleResId = R.string.action_take_photo, icon = Icons.Default.PhotoCamera)
|
||||||
|
object ChoosePhoto : AvatarAction(titleResId = R.string.action_choose_photo, icon = Icons.Default.PhotoLibrary)
|
||||||
|
}
|
||||||
|
|
@ -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.createroom.impl.configureroom.avatar
|
||||||
|
|
||||||
|
sealed interface AvatarActionListEvents {
|
||||||
|
data class HandleAction(val action: AvatarAction) : AvatarActionListEvents
|
||||||
|
}
|
||||||
|
|
@ -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.createroom.impl.configureroom.avatar
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class AvatarActionListState(
|
||||||
|
val actions: ImmutableList<AvatarAction>,
|
||||||
|
val eventSink: (AvatarActionListEvents) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -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.createroom.impl.configureroom.avatar
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
open class ActionListStateProvider : PreviewParameterProvider<AvatarActionListState> {
|
||||||
|
override val values: Sequence<AvatarActionListState>
|
||||||
|
get() = sequenceOf(anActionListState())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun anActionListState() = AvatarActionListState(
|
||||||
|
actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto),
|
||||||
|
eventSink = {}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialApi::class)
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.configureroom.avatar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.ListItem
|
||||||
|
import androidx.compose.material.ModalBottomSheetState
|
||||||
|
import androidx.compose.material.ModalBottomSheetValue
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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 io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AvatarActionListView(
|
||||||
|
state: AvatarActionListState,
|
||||||
|
modalBottomSheetState: ModalBottomSheetState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onActionSelected: (action: AvatarAction) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
fun onItemActionClicked(itemAction: AvatarAction) {
|
||||||
|
onActionSelected(itemAction)
|
||||||
|
coroutineScope.launch {
|
||||||
|
modalBottomSheetState.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ModalBottomSheetLayout(
|
||||||
|
modifier = modifier,
|
||||||
|
sheetState = modalBottomSheetState,
|
||||||
|
sheetContent = {
|
||||||
|
SheetContent(
|
||||||
|
state = state,
|
||||||
|
onActionClicked = ::onItemActionClicked,
|
||||||
|
modifier = Modifier
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.imePadding()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SheetContent(
|
||||||
|
state: AvatarActionListState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onActionClicked: (AvatarAction) -> Unit = { },
|
||||||
|
) {
|
||||||
|
val actions = state.actions
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = actions,
|
||||||
|
) { action ->
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable { onActionClicked(action) },
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(action.titleResId))
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = action.icon,
|
||||||
|
contentDescription = stringResource(action.titleResId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) state: AvatarActionListState) =
|
||||||
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) state: AvatarActionListState) =
|
||||||
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview(state: AvatarActionListState) {
|
||||||
|
AvatarActionListView(
|
||||||
|
state = state,
|
||||||
|
modalBottomSheetState = ModalBottomSheetState(
|
||||||
|
initialValue = ModalBottomSheetValue.Expanded
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,13 @@ sealed interface PickerType<Input, Output> {
|
||||||
fun getContract(): ActivityResultContract<Input, Output>
|
fun getContract(): ActivityResultContract<Input, Output>
|
||||||
fun getDefaultRequest(): Input
|
fun getDefaultRequest(): Input
|
||||||
|
|
||||||
|
object Image : PickerType<PickVisualMediaRequest, Uri?> {
|
||||||
|
override fun getContract() = ActivityResultContracts.PickVisualMedia()
|
||||||
|
override fun getDefaultRequest(): PickVisualMediaRequest {
|
||||||
|
return PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object ImageAndVideo : PickerType<PickVisualMediaRequest, Uri?> {
|
object ImageAndVideo : PickerType<PickVisualMediaRequest, Uri?> {
|
||||||
override fun getContract() = ActivityResultContracts.PickVisualMedia()
|
override fun getContract() = ActivityResultContracts.PickVisualMedia()
|
||||||
override fun getDefaultRequest(): PickVisualMediaRequest {
|
override fun getDefaultRequest(): PickVisualMediaRequest {
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,20 @@ class PickerProviderImpl constructor(private val isInTest: Boolean) : PickerProv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers and returns a [PickerLauncher] for a gallery picture.
|
||||||
|
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun registerGalleryImagePicker(onResult: (Uri?) -> Unit): PickerLauncher<PickVisualMediaRequest, Uri?> {
|
||||||
|
// Tests and UI preview can't handle Contexts, so we might as well disable the whole picker
|
||||||
|
return if (LocalInspectionMode.current || isInTest) {
|
||||||
|
NoOpPickerLauncher { onResult(null) }
|
||||||
|
} else {
|
||||||
|
rememberPickerLauncher(type = PickerType.Image) { uri -> onResult(uri) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remembers and returns a [PickerLauncher] for a gallery item, either a picture or a video.
|
* Remembers and returns a [PickerLauncher] for a gallery item, either a picture or a video.
|
||||||
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
|
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue