[Message actions] Update timeline context menu (#498)
* Replace bottom sheets in timeline with M3 ones. * Add missing options to menu with the right icons * Add message summary and reaction emoji row. * Use the same icon in the message summary and file timeline item
This commit is contained in:
parent
9072305735
commit
dc4b385ada
42 changed files with 657 additions and 299 deletions
1
changelog.d/483.feature
Normal file
1
changelog.d/483.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Redesign the timeline item context menu using M3 bottom sheet
|
||||
|
|
@ -118,6 +118,8 @@ class MessagesPresenter @Inject constructor(
|
|||
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
||||
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState)
|
||||
TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState)
|
||||
TimelineItemAction.Developer -> notImplementedYet()
|
||||
TimelineItemAction.ReportContent -> notImplementedYet()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package io.element.android.features.messages.impl
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
|
|
@ -32,27 +31,22 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ListItem
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
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.PhotoCamera
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
import androidx.compose.material.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -66,9 +60,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListView
|
|||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
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
|
||||
|
|
@ -80,7 +72,6 @@ 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.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout
|
||||
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
|
||||
|
|
@ -91,7 +82,7 @@ import kotlinx.coroutines.launch
|
|||
import timber.log.Timber
|
||||
import io.element.android.libraries.ui.strings.R as StringsR
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun MessagesView(
|
||||
state: MessagesState,
|
||||
|
|
@ -103,26 +94,11 @@ fun MessagesView(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
||||
val itemActionsBottomSheetState = rememberModalBottomSheetState(
|
||||
initialValue = ModalBottomSheetValue.Hidden,
|
||||
)
|
||||
val composerState = state.composerState
|
||||
val initialBottomSheetState = if (LocalInspectionMode.current && composerState.showAttachmentSourcePicker) {
|
||||
ModalBottomSheetValue.Expanded
|
||||
} else {
|
||||
ModalBottomSheetValue.Hidden
|
||||
}
|
||||
val bottomSheetState = rememberModalBottomSheetState(initialValue = initialBottomSheetState)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var isMessageActionsBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments)
|
||||
|
||||
BackHandler(enabled = bottomSheetState.isVisible) {
|
||||
coroutineScope.launch {
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val snackbarMessageText = state.snackbarMessage?.let { stringResource(it.messageResId) }
|
||||
if (snackbarMessageText != null) {
|
||||
|
|
@ -150,78 +126,57 @@ fun MessagesView(
|
|||
Timber.v("OnMessageLongClicked= ${event.id}")
|
||||
localView.hideKeyboard()
|
||||
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event))
|
||||
coroutineScope.launch {
|
||||
itemActionsBottomSheetState.show()
|
||||
}
|
||||
isMessageActionsBottomSheetVisible = true
|
||||
}
|
||||
|
||||
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
|
||||
isMessageActionsBottomSheetVisible = false
|
||||
state.eventSink(MessagesEvents.HandleAction(action, event))
|
||||
}
|
||||
|
||||
LaunchedEffect(composerState.showAttachmentSourcePicker) {
|
||||
if (composerState.showAttachmentSourcePicker) {
|
||||
// We need to use this instead of `LocalFocusManager.clearFocus()` to hide the keyboard when focus is on an Android View
|
||||
localView.hideKeyboard()
|
||||
bottomSheetState.show()
|
||||
} else {
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
fun onDismissActionListBottomSheet() {
|
||||
isMessageActionsBottomSheetVisible = false
|
||||
}
|
||||
// Send 'DismissAttachmentMenu' event when the bottomsheet was just hidden
|
||||
LaunchedEffect(bottomSheetState.isVisible) {
|
||||
if (!bottomSheetState.isVisible) {
|
||||
composerState.eventSink(MessageComposerEvents.DismissAttachmentMenu)
|
||||
}
|
||||
}
|
||||
ModalBottomSheetLayout(
|
||||
sheetState = bottomSheetState,
|
||||
displayHandle = true,
|
||||
sheetContent = {
|
||||
AttachmentSourcePickerMenu(
|
||||
eventSink = composerState.eventSink
|
||||
)
|
||||
}
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
contentWindowInsets = WindowInsets.statusBars,
|
||||
topBar = {
|
||||
Column {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
MessagesViewTopBar(
|
||||
roomTitle = state.roomName,
|
||||
roomAvatar = state.roomAvatar,
|
||||
onBackPressed = onBackPressed,
|
||||
onRoomDetailsClicked = onRoomDetailsClicked,
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { padding ->
|
||||
MessagesViewContent(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
onMessageClicked = ::onMessageClicked,
|
||||
onMessageLongClicked = ::onMessageLongClicked,
|
||||
onUserDataClicked = onUserDataClicked,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(
|
||||
snackbarHostState,
|
||||
modifier = Modifier.navigationBarsPadding()
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
ActionListView(
|
||||
state = state.actionListState,
|
||||
modalBottomSheetState = itemActionsBottomSheetState,
|
||||
onActionSelected = ::onActionSelected
|
||||
)
|
||||
}
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
contentWindowInsets = WindowInsets.statusBars,
|
||||
topBar = {
|
||||
Column {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
MessagesViewTopBar(
|
||||
roomTitle = state.roomName,
|
||||
roomAvatar = state.roomAvatar,
|
||||
onBackPressed = onBackPressed,
|
||||
onRoomDetailsClicked = onRoomDetailsClicked,
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { padding ->
|
||||
MessagesViewContent(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
onMessageClicked = ::onMessageClicked,
|
||||
onMessageLongClicked = ::onMessageLongClicked,
|
||||
onUserDataClicked = onUserDataClicked,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(
|
||||
snackbarHostState,
|
||||
modifier = Modifier.navigationBarsPadding()
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
ActionListView(
|
||||
state = state.actionListState,
|
||||
isVisible = isMessageActionsBottomSheetVisible,
|
||||
onDismiss = ::onDismissActionListBottomSheet,
|
||||
onActionSelected = ::onActionSelected
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -312,36 +267,6 @@ fun MessagesViewTopBar(
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
internal fun AttachmentSourcePickerMenu(
|
||||
eventSink: (MessageComposerEvents) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) },
|
||||
icon = { Icon(Icons.Default.Collections, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) },
|
||||
)
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) },
|
||||
icon = { Icon(Icons.Default.AttachFile, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_source_files)) },
|
||||
)
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) },
|
||||
icon = { Icon(Icons.Default.PhotoCamera, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) },
|
||||
)
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) },
|
||||
icon = { Icon(Icons.Default.Videocam, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun MessagesViewLightPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) =
|
||||
|
|
|
|||
|
|
@ -26,12 +26,15 @@ 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 io.element.android.libraries.core.meta.BuildMeta
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ActionListPresenter @Inject constructor() : Presenter<ActionListState> {
|
||||
class ActionListPresenter @Inject constructor(
|
||||
private val buildMeta: BuildMeta,
|
||||
) : Presenter<ActionListState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): ActionListState {
|
||||
|
|
@ -60,21 +63,30 @@ class ActionListPresenter @Inject constructor() : Presenter<ActionListState> {
|
|||
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)
|
||||
buildList {
|
||||
add(TimelineItemAction.Copy)
|
||||
if (buildMeta.isDebuggable) {
|
||||
add(TimelineItemAction.Developer)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> buildList<TimelineItemAction> {
|
||||
add(TimelineItemAction.Reply)
|
||||
add(TimelineItemAction.Forward)
|
||||
if (timelineItem.isMine) {
|
||||
add(TimelineItemAction.Edit)
|
||||
}
|
||||
add(TimelineItemAction.Copy)
|
||||
if (buildMeta.isDebuggable) {
|
||||
add(TimelineItemAction.Developer)
|
||||
}
|
||||
if (!timelineItem.isMine) {
|
||||
add(TimelineItemAction.ReportContent)
|
||||
}
|
||||
if (timelineItem.isMine) {
|
||||
add(TimelineItemAction.Redact)
|
||||
}
|
||||
}
|
||||
}
|
||||
target.value = ActionListState.Target.Success(timelineItem, actions.toImmutableList())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,52 +16,81 @@
|
|||
|
||||
package io.element.android.features.messages.impl.actionlist
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AddReaction
|
||||
import androidx.compose.material.icons.outlined.Attachment
|
||||
import androidx.compose.material.icons.outlined.VideoCameraBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
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.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||
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.TimelineItemProfileChangeContent
|
||||
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
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ActionListView(
|
||||
state: ActionListState,
|
||||
modalBottomSheetState: ModalBottomSheetState,
|
||||
isVisible: Boolean,
|
||||
onActionSelected: (action: TimelineItemAction, TimelineItem.Event) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
LaunchedEffect(modalBottomSheetState) {
|
||||
snapshotFlow { modalBottomSheetState.currentValue }
|
||||
.filter { it == ModalBottomSheetValue.Hidden }
|
||||
.collect {
|
||||
state.eventSink(ActionListEvents.Clear)
|
||||
}
|
||||
LaunchedEffect(isVisible) {
|
||||
if (!isVisible) {
|
||||
state.eventSink(ActionListEvents.Clear)
|
||||
}
|
||||
}
|
||||
|
||||
fun onItemActionClicked(
|
||||
|
|
@ -69,24 +98,22 @@ fun ActionListView(
|
|||
targetItem: TimelineItem.Event
|
||||
) {
|
||||
onActionSelected(itemAction, targetItem)
|
||||
coroutineScope.launch {
|
||||
modalBottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
|
||||
ModalBottomSheetLayout(
|
||||
modifier = modifier,
|
||||
sheetState = modalBottomSheetState,
|
||||
sheetContent = {
|
||||
if (isVisible) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
SheetContent(
|
||||
state = state,
|
||||
onActionClicked = ::onItemActionClicked,
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
modifier = modifier
|
||||
.padding(bottom = 32.dp)
|
||||
// .navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
|
||||
// .imePadding()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
|
|
@ -108,6 +135,19 @@ private fun SheetContent(
|
|||
LazyColumn(
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
item {
|
||||
Column {
|
||||
MessageSummary(event = target.event, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp))
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
item {
|
||||
EmojiReactionsRow(Modifier.fillMaxWidth())
|
||||
Divider()
|
||||
}
|
||||
items(
|
||||
items = actions,
|
||||
) { action ->
|
||||
|
|
@ -135,6 +175,141 @@ private fun SheetContent(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
|
||||
val content: @Composable () -> Unit
|
||||
var icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.SMALL)) }
|
||||
val contentStyle = ElementTextStyles.Regular.bodyMD.copy(color = MaterialTheme.colorScheme.secondary)
|
||||
val imageModifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(9.dp))
|
||||
|
||||
@Composable
|
||||
fun ContentForBody(body: String) {
|
||||
Text(body, style = contentStyle, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
|
||||
when (event.content) {
|
||||
is TimelineItemTextBasedContent -> content = { ContentForBody(event.content.body) }
|
||||
is TimelineItemStateContent -> content = { ContentForBody(event.content.body) }
|
||||
is TimelineItemProfileChangeContent -> content = { ContentForBody(event.content.body) }
|
||||
is TimelineItemEncryptedContent -> content = { ContentForBody(stringResource(StringR.string.common_unable_to_decrypt)) }
|
||||
is TimelineItemRedactedContent -> content = { ContentForBody(stringResource(StringR.string.common_message_removed)) }
|
||||
is TimelineItemUnknownContent -> content = { ContentForBody(stringResource(StringR.string.common_unsupported_event)) }
|
||||
is TimelineItemImageContent -> {
|
||||
icon = {
|
||||
val mediaRequestData = MediaRequestData(
|
||||
source = event.content.mediaSource,
|
||||
kind = MediaRequestData.Kind.Thumbnail(32),
|
||||
)
|
||||
BlurHashAsyncImage(
|
||||
model = mediaRequestData,
|
||||
blurHash = event.content.blurhash,
|
||||
contentDescription = stringResource(StringR.string.common_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = imageModifier,
|
||||
)
|
||||
}
|
||||
content = { ContentForBody(event.content.body) }
|
||||
}
|
||||
is TimelineItemVideoContent -> {
|
||||
icon = {
|
||||
val thumbnailSource = event.content.thumbnailSource
|
||||
if (thumbnailSource != null) {
|
||||
val mediaRequestData = MediaRequestData(
|
||||
source = event.content.thumbnailSource,
|
||||
kind = MediaRequestData.Kind.Thumbnail(32),
|
||||
)
|
||||
BlurHashAsyncImage(
|
||||
model = mediaRequestData,
|
||||
blurHash = event.content.blurHash,
|
||||
contentDescription = stringResource(StringR.string.common_video),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = imageModifier,
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = imageModifier.background(MaterialTheme.colorScheme.surface),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.VideoCameraBack,
|
||||
contentDescription = stringResource(StringR.string.common_video),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
content = { ContentForBody(event.content.body) }
|
||||
}
|
||||
is TimelineItemFileContent -> {
|
||||
icon = {
|
||||
Box(
|
||||
modifier = imageModifier.background(MaterialTheme.colorScheme.surface),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Attachment,
|
||||
contentDescription = stringResource(StringR.string.common_file),
|
||||
modifier = Modifier.rotate(-45f)
|
||||
)
|
||||
}
|
||||
}
|
||||
content = { ContentForBody(event.content.body) }
|
||||
}
|
||||
}
|
||||
Row(modifier = modifier) {
|
||||
icon()
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column {
|
||||
Row {
|
||||
if (event.senderDisplayName != null) {
|
||||
Text(
|
||||
text = event.senderDisplayName,
|
||||
style = ElementTextStyles.Bold.caption1,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
event.sentTime,
|
||||
style = ElementTextStyles.Regular.caption2,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.End,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun EmojiReactionsRow(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = modifier.padding(horizontal = 28.dp, vertical = 16.dp)
|
||||
) {
|
||||
// TODO use real emojis, have real interaction
|
||||
Text("\uD83D\uDC4D", fontSize = 28.dpToSp())
|
||||
Text("\uD83D\uDC4E", fontSize = 28.dpToSp())
|
||||
Text("\uD83D\uDD25", fontSize = 28.dpToSp())
|
||||
Text("❤\uFE0F", fontSize = 28.dpToSp())
|
||||
Text("\uD83D\uDC4F", fontSize = 28.dpToSp())
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.AddReaction,
|
||||
contentDescription = "Emojis",
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Int.dpToSp(): TextUnit = with(LocalDensity.current) {
|
||||
return dp.toSp()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) =
|
||||
|
|
@ -145,14 +320,7 @@ fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) s
|
|||
fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun ContentToPreview(state: ActionListState) {
|
||||
ActionListView(
|
||||
state = state,
|
||||
modalBottomSheetState = ModalBottomSheetState(
|
||||
initialValue = ModalBottomSheetValue.Expanded
|
||||
),
|
||||
onActionSelected = { _, _ -> }
|
||||
)
|
||||
SheetContent(state = state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,11 @@ sealed class TimelineItemAction(
|
|||
@DrawableRes val icon: Int,
|
||||
val destructive: Boolean = false
|
||||
) {
|
||||
object Forward : TimelineItemAction("Forward", VectorIcons.ArrowForward)
|
||||
object Forward : TimelineItemAction("Forward", VectorIcons.Forward)
|
||||
object Copy : TimelineItemAction("Copy", VectorIcons.Copy)
|
||||
object Redact : TimelineItemAction("Redact", VectorIcons.Delete, destructive = true)
|
||||
object Reply : TimelineItemAction("Reply", VectorIcons.Reply)
|
||||
object Edit : TimelineItemAction("Edit", VectorIcons.Edit)
|
||||
object Developer : TimelineItemAction("Developer", VectorIcons.DeveloperMode)
|
||||
object ReportContent : TimelineItemAction("Report content", VectorIcons.ReportContent, destructive = true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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.messagecomposer
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ListItem
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AttachFile
|
||||
import androidx.compose.material.icons.filled.Collections
|
||||
import androidx.compose.material.icons.filled.PhotoCamera
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.impl.R
|
||||
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun AttachmentsBottomSheet(
|
||||
state: MessageComposerState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val localView = LocalView.current
|
||||
var isVisible by rememberSaveable { mutableStateOf(state.showAttachmentSourcePicker) }
|
||||
|
||||
BackHandler(enabled = isVisible) {
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
LaunchedEffect(state.showAttachmentSourcePicker) {
|
||||
if (state.showAttachmentSourcePicker) {
|
||||
// We need to use this instead of `LocalFocusManager.clearFocus()` to hide the keyboard when focus is on an Android View
|
||||
localView.hideKeyboard()
|
||||
isVisible = true
|
||||
} else {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
// Send 'DismissAttachmentMenu' event when the bottomsheet was just hidden
|
||||
LaunchedEffect(isVisible) {
|
||||
if (!isVisible) {
|
||||
state.eventSink(MessageComposerEvents.DismissAttachmentMenu)
|
||||
}
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
ModalBottomSheet(
|
||||
modifier = modifier,
|
||||
onDismissRequest = { isVisible = false }
|
||||
) {
|
||||
AttachmentSourcePickerMenu(eventSink = state.eventSink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
internal fun AttachmentSourcePickerMenu(
|
||||
eventSink: (MessageComposerEvents) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier.padding(bottom = 32.dp)
|
||||
// .navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) },
|
||||
icon = { Icon(Icons.Default.Collections, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) },
|
||||
)
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) },
|
||||
icon = { Icon(Icons.Default.AttachFile, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_source_files)) },
|
||||
)
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) },
|
||||
icon = { Icon(Icons.Default.PhotoCamera, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) },
|
||||
)
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) },
|
||||
icon = { Icon(Icons.Default.Videocam, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.messages.impl.messagecomposer
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -46,21 +47,25 @@ fun MessageComposerView(
|
|||
state.eventSink(MessageComposerEvents.UpdateText(text))
|
||||
}
|
||||
|
||||
TextComposer(
|
||||
onSendMessage = ::sendMessage,
|
||||
fullscreen = state.isFullScreen,
|
||||
onFullscreenToggle = ::onFullscreenToggle,
|
||||
composerMode = state.mode,
|
||||
onCloseSpecialMode = ::onCloseSpecialMode,
|
||||
onComposerTextChange = ::onComposerTextChange,
|
||||
onAddAttachment = {
|
||||
state.eventSink(MessageComposerEvents.AddAttachment)
|
||||
},
|
||||
composerCanSendMessage = state.isSendButtonVisible,
|
||||
composerText = state.text?.charSequence?.toString(),
|
||||
isInDarkMode = !ElementTheme.colors.isLight,
|
||||
modifier = modifier
|
||||
)
|
||||
Box {
|
||||
AttachmentsBottomSheet(state = state)
|
||||
|
||||
TextComposer(
|
||||
onSendMessage = ::sendMessage,
|
||||
fullscreen = state.isFullScreen,
|
||||
onFullscreenToggle = ::onFullscreenToggle,
|
||||
composerMode = state.mode,
|
||||
onCloseSpecialMode = ::onCloseSpecialMode,
|
||||
onComposerTextChange = ::onComposerTextChange,
|
||||
onAddAttachment = {
|
||||
state.eventSink(MessageComposerEvents.AddAttachment)
|
||||
},
|
||||
composerCanSendMessage = state.isSendButtonVisible,
|
||||
composerText = state.text?.charSequence?.toString(),
|
||||
isInDarkMode = !ElementTheme.colors.isLight,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
|
|||
|
|
@ -24,11 +24,13 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Attachment
|
||||
import androidx.compose.material.icons.outlined.Attachment
|
||||
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.draw.rotate
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
|
@ -57,8 +59,9 @@ fun TimelineItemFileView(
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Attachment,
|
||||
contentDescription = "OpenFile"
|
||||
imageVector = Icons.Outlined.Attachment,
|
||||
contentDescription = "OpenFile",
|
||||
modifier = Modifier.size(16.dp).rotate(-45f),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
|
|||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
|
|
@ -132,6 +134,32 @@ class MessagesPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action report content`() = runTest {
|
||||
val presenter = createMessagePresenter()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent()))
|
||||
// Still a TODO in the code
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action show developer info`() = runTest {
|
||||
val presenter = createMessagePresenter()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Developer, aMessageEvent()))
|
||||
// Still a TODO in the code
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createMessagePresenter(
|
||||
matrixRoom: MatrixRoom = FakeMatrixRoom()
|
||||
): MessagesPresenter {
|
||||
|
|
@ -148,7 +176,20 @@ class MessagesPresenterTest {
|
|||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
room = matrixRoom,
|
||||
)
|
||||
val actionListPresenter = ActionListPresenter()
|
||||
val buildMeta = BuildMeta(
|
||||
buildType = BuildType.DEBUG,
|
||||
isDebuggable = true,
|
||||
applicationId = "",
|
||||
applicationName = "",
|
||||
lowPrivacyLoggingEnabled = true,
|
||||
versionName = "",
|
||||
gitRevision = "",
|
||||
gitBranchName = "",
|
||||
gitRevisionDate = "",
|
||||
flavorDescription = "",
|
||||
flavorShortDescription = "",
|
||||
)
|
||||
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
|
||||
return MessagesPresenter(
|
||||
room = matrixRoom,
|
||||
composerPresenter = messageComposerPresenter,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemReac
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
|
|
@ -42,7 +44,7 @@ import org.junit.Test
|
|||
class ActionListPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = ActionListPresenter()
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -53,7 +55,7 @@ class ActionListPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - compute for message from me redacted`() = runTest {
|
||||
val presenter = ActionListPresenter()
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -67,6 +69,8 @@ class ActionListPresenterTest {
|
|||
ActionListState.Target.Success(
|
||||
messageEvent,
|
||||
persistentListOf(
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Developer,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -75,9 +79,10 @@ class ActionListPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `present - compute for message from others redacted`() = runTest {
|
||||
val presenter = ActionListPresenter()
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -91,6 +96,8 @@ class ActionListPresenterTest {
|
|||
ActionListState.Target.Success(
|
||||
messageEvent,
|
||||
persistentListOf(
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Developer,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -101,7 +108,7 @@ class ActionListPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - compute for others message`() = runTest {
|
||||
val presenter = ActionListPresenter()
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -121,6 +128,8 @@ class ActionListPresenterTest {
|
|||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Developer,
|
||||
TimelineItemAction.ReportContent,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -131,7 +140,7 @@ class ActionListPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - compute for my message`() = runTest {
|
||||
val presenter = ActionListPresenter()
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -150,8 +159,41 @@ class ActionListPresenterTest {
|
|||
persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Developer,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
)
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - compute message in non-debuggable build`() = runTest {
|
||||
val presenter = anActionListPresenter(isBuildDebuggable = false)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val messageEvent = aMessageEvent(
|
||||
isMine = true,
|
||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||
)
|
||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
|
||||
// val loadingState = awaitItem()
|
||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.target).isEqualTo(
|
||||
ActionListState.Target.Success(
|
||||
messageEvent,
|
||||
persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
)
|
||||
|
|
@ -162,6 +204,34 @@ class ActionListPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun aBuildMeta(
|
||||
buildType: BuildType = BuildType.DEBUG,
|
||||
isDebuggable: Boolean = true,
|
||||
applicationName: String = "",
|
||||
applicationId: String = "",
|
||||
lowPrivacyLoggingEnabled: Boolean = true,
|
||||
versionName: String = "",
|
||||
gitRevision: String = "",
|
||||
gitRevisionDate: String = "",
|
||||
gitBranchName: String = "",
|
||||
flavorDescription: String = "",
|
||||
flavorShortDescription: String = "",
|
||||
) = BuildMeta(
|
||||
buildType,
|
||||
isDebuggable,
|
||||
applicationName,
|
||||
applicationId,
|
||||
lowPrivacyLoggingEnabled,
|
||||
versionName,
|
||||
gitRevision,
|
||||
gitRevisionDate,
|
||||
gitBranchName,
|
||||
flavorDescription,
|
||||
flavorShortDescription
|
||||
)
|
||||
|
||||
private fun anActionListPresenter(isBuildDebuggable: Boolean) = ActionListPresenter(buildMeta = aBuildMeta(isDebuggable = isBuildDebuggable))
|
||||
|
||||
private fun aMessageEvent(
|
||||
isMine: Boolean,
|
||||
content: TimelineItemEventContent,
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ package io.element.android.libraries.designsystem
|
|||
|
||||
object VectorIcons {
|
||||
val Copy = R.drawable.ic_content_copy
|
||||
val ArrowForward = R.drawable.ic_content_arrow_forward
|
||||
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 Forward = R.drawable.ic_forward
|
||||
val Delete = R.drawable.ic_delete
|
||||
val Reply = R.drawable.ic_reply
|
||||
val Edit = R.drawable.ic_edit
|
||||
val DoorOpen = R.drawable.ic_door_open_24
|
||||
val DeveloperMode = R.drawable.ic_developer_mode
|
||||
val ReportContent = R.drawable.ic_report_content
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<vector android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8,9h8v10L8,19L8,9zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z" />
|
||||
</vector>
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<vector android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||
</vector>
|
||||
10
libraries/designsystem/src/main/res/drawable/ic_delete.xml
Normal file
10
libraries/designsystem/src/main/res/drawable/ic_delete.xml
Normal file
|
|
@ -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="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L160,240L160,160L360,160L360,120L600,120L600,160L800,160L800,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM360,680L440,680L440,320L360,320L360,680ZM520,680L600,680L600,320L520,320L520,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M344,664L160,480L344,296L400,354L274,480L400,606L344,664ZM200,680L280,680L280,720L680,720L680,680L760,680L760,840Q760,873 736.5,896.5Q713,920 680,920L280,920Q247,920 223.5,896.5Q200,873 200,840L200,680ZM280,280L200,280L200,120Q200,87 223.5,63.5Q247,40 280,40L680,40Q713,40 736.5,63.5Q760,87 760,120L760,280L680,280L680,240L280,240L280,280ZM280,800L280,840Q280,840 280,840Q280,840 280,840L680,840Q680,840 680,840Q680,840 680,840L680,800L280,800ZM280,160L680,160L680,120Q680,120 680,120Q680,120 680,120L280,120Q280,120 280,120Q280,120 280,120L280,160ZM616,664L560,606L686,480L560,354L616,296L800,480L616,664ZM280,160L280,120Q280,120 280,120Q280,120 280,120L280,120Q280,120 280,120Q280,120 280,120L280,160L280,160ZM280,800L280,800L280,840Q280,840 280,840Q280,840 280,840L280,840Q280,840 280,840Q280,840 280,840L280,800Z"/>
|
||||
</vector>
|
||||
10
libraries/designsystem/src/main/res/drawable/ic_edit.xml
Normal file
10
libraries/designsystem/src/main/res/drawable/ic_edit.xml
Normal file
|
|
@ -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="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M200,760L256,760L601,415L545,359L200,704L200,760ZM772,357L602,189L658,133Q681,110 714.5,110Q748,110 771,133L827,189Q850,212 851,244.5Q852,277 829,300L772,357ZM714,416L290,840L120,840L120,670L544,246L714,416ZM573,387L545,359L545,359L601,415L601,415L573,387Z"/>
|
||||
</vector>
|
||||
11
libraries/designsystem/src/main/res/drawable/ic_forward.xml
Normal file
11
libraries/designsystem/src/main/res/drawable/ic_forward.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M120,760L120,600Q120,517 178.5,458.5Q237,400 320,400L688,400L544,256L600,200L840,440L600,680L544,624L688,480L320,480Q270,480 235,515Q200,550 200,600L200,760L120,760Z"/>
|
||||
</vector>
|
||||
11
libraries/designsystem/src/main/res/drawable/ic_reply.xml
Normal file
11
libraries/designsystem/src/main/res/drawable/ic_reply.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M760,760L760,600Q760,550 725,515Q690,480 640,480L272,480L416,624L360,680L120,440L360,200L416,256L272,400L640,400Q723,400 781.5,458.5Q840,517 840,600L840,760L760,760Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,600Q497,600 508.5,588.5Q520,577 520,560Q520,543 508.5,531.5Q497,520 480,520Q463,520 451.5,531.5Q440,543 440,560Q440,577 451.5,588.5Q463,600 480,600ZM440,440L520,440L520,200L440,200L440,440ZM80,880L80,160Q80,127 103.5,103.5Q127,80 160,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L240,720L80,880ZM206,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L160,160Q160,160 160,160Q160,160 160,160L160,685L206,640ZM160,640L160,640L160,160Q160,160 160,160Q160,160 160,160L160,160Q160,160 160,160Q160,160 160,160L160,640Q160,640 160,640Q160,640 160,640L160,640Z"/>
|
||||
</vector>
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e3cb476c16c2cae9f3230cc4030b66662b9f63cef22208fc5bf577d0b16bf946
|
||||
size 4484
|
||||
oid sha256:a5948ff8260dc73ff19232679312141dd4021a00cce2871f69870b291a237aa9
|
||||
size 4478
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e3cb476c16c2cae9f3230cc4030b66662b9f63cef22208fc5bf577d0b16bf946
|
||||
size 4484
|
||||
oid sha256:a5948ff8260dc73ff19232679312141dd4021a00cce2871f69870b291a237aa9
|
||||
size 4478
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dea394d708a714603ea77543a7ab31550baaea72c75255c56ac9162589096128
|
||||
size 14453
|
||||
oid sha256:92fc84907bd6a779d10daa894f4c8ed3039ad8019c32a502f487086a8cbc535a
|
||||
size 33707
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:54b434198b8b6b534e0e82310e58eec162f18aba876f0dca9a1790c137230595
|
||||
size 4496
|
||||
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||
size 4457
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:54b434198b8b6b534e0e82310e58eec162f18aba876f0dca9a1790c137230595
|
||||
size 4496
|
||||
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||
size 4457
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f3080445c87d85fd5c51228e33ef7f91eb3a718f2f8288bdfa2a48d6769a25a1
|
||||
size 15480
|
||||
oid sha256:aaf0c08cc44a092f72e964f53982570ea277be4db8b30045b175abfe5a3eea50
|
||||
size 32930
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:82871e8bf4472e48dd134d55c4ef24a490643e022da3101342fa47ca89024adb
|
||||
size 7323
|
||||
oid sha256:0e4ddf896da22a54df8584595a02c8aea737c7c306b20d496a3cc5e7aa0bb892
|
||||
size 7630
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c3173576742c91fea1f6825a83e08c85636d14cd6ef9d84ff9c402df7d62eee9
|
||||
size 9936
|
||||
oid sha256:aa8f48f1f8167755f27a67fc634f854b13f1ee78fe7c425a8e7285df03e937d7
|
||||
size 10189
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1ed4bfb88bb5d1a037d0393caf056cb30c23df14a6258dac5b7cdfd2435e6ddf
|
||||
size 12614
|
||||
oid sha256:94300042d18138cea4e3d7462c3a1492bbc8766292bbf4bdc0b22802f199c52e
|
||||
size 12882
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:764bbe03b664b697f2e1bbb93ef879060819b41d8b9f8620ad7f64883c5892c2
|
||||
size 6890
|
||||
oid sha256:55e7f102412306ca4ff8e0519f08c3e5c7a822629bd6786304b89be82bcaffaf
|
||||
size 7132
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2dbc148e817289cc726fe4fbbba09c0a2b43e794bde2673841ad9062ebfc3ec5
|
||||
size 8979
|
||||
oid sha256:2533688d2cd343c4466d7b1024451db5936e94c34ba6a9e685be5fc27ca05f40
|
||||
size 9216
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c5207aa4f89d0613937318de8cf6e324c19ab4b769eca0519f375ab505687b4c
|
||||
size 11256
|
||||
oid sha256:0a139e4bf9c9b0bb3696e154fb0e56f67e00df6a3673b24f12f8dae6606833dc
|
||||
size 11464
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f8f54ea6a7d158e8f7da56648aabf05c8dbc761db8698891d2d3f1ff863b76fb
|
||||
size 51724
|
||||
oid sha256:c9e411482e83d74323faec8b14410ec48ad9a2ba7ec731de7192f583608cecc6
|
||||
size 53465
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4c2845cd6ef914a8062d3685d62d6b861a3963e09aad13efd5a9ac5e2de1a3ea
|
||||
size 73507
|
||||
oid sha256:e9f2ee31c8c1a8183a3314b2f9310dcb737b4f9b73ab99b1a41ddecf4f52d3b9
|
||||
size 75291
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:022ca7f2b5ce17e0079346778ee3b99b1e235439f88885a2d1bf545cc11864e6
|
||||
size 51800
|
||||
oid sha256:ea99af12122d317652b23751542d751463e555ef16418ceaf2ec83be48bcc9a0
|
||||
size 53241
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c347c51252c8e1305385c6c92936941403c481c792ba3fe10ca4949a1accd051
|
||||
size 74080
|
||||
oid sha256:6fdabd6188d749aa9fd8e1e1c54432399a8d25ae1b3d59a200d07e4370b1dbe6
|
||||
size 75518
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:74d763ff8d44e686ee3a10dcadc0e2c1f02e3332c486d62d00e2333d0c3b60cf
|
||||
size 45578
|
||||
oid sha256:996ca55692e56818c1473c991b6252f0886f1bfbb5511d9d35f5fc38a878e490
|
||||
size 45546
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:09b29e2e7bb8c38c978d3b56b04a97fc96d752154bb78fddd13a5cde8051a048
|
||||
size 45757
|
||||
oid sha256:ac450ec2a16b34217538c4304db4cd57645c676b1b44f510d4672ad47beffc57
|
||||
size 45725
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a936d98080f35c838adab660ef9b9287fc380d1358335c54e3da56230476721f
|
||||
size 43810
|
||||
oid sha256:996ca55692e56818c1473c991b6252f0886f1bfbb5511d9d35f5fc38a878e490
|
||||
size 45546
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43042ccb540b5fa3b8d5d9e273bd1155258d0dc12875b95c7a364492b3985da2
|
||||
size 44854
|
||||
oid sha256:de4068d875e93b5cc69d4c55e9dff3a32104514e7efad791f753e5ab77e96b90
|
||||
size 44665
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:16cf470ec6ff50ee469db7bdc2cbfc1e32c5bcd7835c12c7313aafa495a44825
|
||||
size 45282
|
||||
oid sha256:7744199792a34d1fe284867b9bf2878bfa81a05eb34ccd50f8bd33662408fa8b
|
||||
size 45096
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e6cc29403f3b3e146e9a637973320f5bd6bf73feb7b042f8844337817e551113
|
||||
size 42280
|
||||
oid sha256:de4068d875e93b5cc69d4c55e9dff3a32104514e7efad791f753e5ab77e96b90
|
||||
size 44665
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue