Merge branch 'develop' of ssh://github.com/vector-im/element-x-android-poc into develop
This commit is contained in:
commit
1435428c7f
17 changed files with 212 additions and 39 deletions
|
|
@ -20,20 +20,27 @@ import org.matrix.rustcomponents.sdk.MessageFormat
|
||||||
import org.matrix.rustcomponents.sdk.MessageType
|
import org.matrix.rustcomponents.sdk.MessageType
|
||||||
import org.matrix.rustcomponents.sdk.TimelineKey
|
import org.matrix.rustcomponents.sdk.TimelineKey
|
||||||
|
|
||||||
class MessageTimelineItemStateMapper(
|
class MessageTimelineItemStateFactory(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val dispatcher: CoroutineDispatcher,
|
private val dispatcher: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
|
suspend fun create(
|
||||||
suspend fun map(timelineItems: List<MatrixTimelineItem>): List<MessagesTimelineItemState> =
|
timelineItems: List<MatrixTimelineItem>,
|
||||||
|
highlightedEventId: String? = null,
|
||||||
|
): List<MessagesTimelineItemState> =
|
||||||
withContext(dispatcher) {
|
withContext(dispatcher) {
|
||||||
val messagesTimelineItemState = ArrayList<MessagesTimelineItemState>()
|
val messagesTimelineItemState = ArrayList<MessagesTimelineItemState>()
|
||||||
for (index in timelineItems.indices.reversed()) {
|
for (index in timelineItems.indices.reversed()) {
|
||||||
val currentTimelineItem = timelineItems[index]
|
val currentTimelineItem = timelineItems[index]
|
||||||
val timelineItemState = when (currentTimelineItem) {
|
val timelineItemState = when (currentTimelineItem) {
|
||||||
is MatrixTimelineItem.Event -> {
|
is MatrixTimelineItem.Event -> {
|
||||||
buildMessageEvent(currentTimelineItem, index, timelineItems)
|
buildMessageEvent(
|
||||||
|
currentTimelineItem,
|
||||||
|
index,
|
||||||
|
timelineItems,
|
||||||
|
highlightedEventId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual(
|
is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual(
|
||||||
"virtual_item_$index"
|
"virtual_item_$index"
|
||||||
|
|
@ -48,7 +55,8 @@ class MessageTimelineItemStateMapper(
|
||||||
private suspend fun buildMessageEvent(
|
private suspend fun buildMessageEvent(
|
||||||
currentTimelineItem: MatrixTimelineItem.Event,
|
currentTimelineItem: MatrixTimelineItem.Event,
|
||||||
index: Int,
|
index: Int,
|
||||||
timelineItems: List<MatrixTimelineItem>
|
timelineItems: List<MatrixTimelineItem>,
|
||||||
|
highlightedEventId: String?,
|
||||||
): MessagesTimelineItemState.MessageEvent {
|
): MessagesTimelineItemState.MessageEvent {
|
||||||
val currentSender = currentTimelineItem.event.sender()
|
val currentSender = currentTimelineItem.event.sender()
|
||||||
val groupPosition =
|
val groupPosition =
|
||||||
|
|
@ -68,6 +76,7 @@ class MessageTimelineItemStateMapper(
|
||||||
senderAvatar = senderAvatarData,
|
senderAvatar = senderAvatarData,
|
||||||
content = currentTimelineItem.computeContent(),
|
content = currentTimelineItem.computeContent(),
|
||||||
isMine = currentTimelineItem.event.isOwn(),
|
isMine = currentTimelineItem.event.isOwn(),
|
||||||
|
isHighlighted = currentTimelineItem.event.eventId().orEmpty() == highlightedEventId,
|
||||||
groupPosition = groupPosition,
|
groupPosition = groupPosition,
|
||||||
reactionsState = currentTimelineItem.computeReactionsState()
|
reactionsState = currentTimelineItem.computeReactionsState()
|
||||||
)
|
)
|
||||||
|
|
@ -41,6 +41,7 @@ import io.element.android.x.features.messages.model.*
|
||||||
import io.element.android.x.features.messages.model.content.*
|
import io.element.android.x.features.messages.model.content.*
|
||||||
import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel
|
import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel
|
||||||
import io.element.android.x.features.messages.textcomposer.MessageComposerViewState
|
import io.element.android.x.features.messages.textcomposer.MessageComposerViewState
|
||||||
|
import io.element.android.x.textcomposer.MessageComposerMode
|
||||||
import io.element.android.x.textcomposer.TextComposer
|
import io.element.android.x.textcomposer.TextComposer
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -72,6 +73,7 @@ fun MessagesScreen(
|
||||||
val timelineItems by viewModel.collectAsState(MessagesViewState::timelineItems)
|
val timelineItems by viewModel.collectAsState(MessagesViewState::timelineItems)
|
||||||
val hasMoreToLoad by viewModel.collectAsState(MessagesViewState::hasMoreToLoad)
|
val hasMoreToLoad by viewModel.collectAsState(MessagesViewState::hasMoreToLoad)
|
||||||
val snackBarContent by viewModel.collectAsState(MessagesViewState::snackbarContent)
|
val snackBarContent by viewModel.collectAsState(MessagesViewState::snackbarContent)
|
||||||
|
val composerMode by viewModel.collectAsState(MessagesViewState::composerMode)
|
||||||
val composerFullScreen by composerViewModel.collectAsState(MessageComposerViewState::isFullScreen)
|
val composerFullScreen by composerViewModel.collectAsState(MessageComposerViewState::isFullScreen)
|
||||||
val composerCanSendMessage by composerViewModel.collectAsState(MessageComposerViewState::isSendButtonVisible)
|
val composerCanSendMessage by composerViewModel.collectAsState(MessageComposerViewState::isSendButtonVisible)
|
||||||
val composerText by composerViewModel.collectAsState(MessageComposerViewState::text)
|
val composerText by composerViewModel.collectAsState(MessageComposerViewState::text)
|
||||||
|
|
@ -87,6 +89,8 @@ fun MessagesScreen(
|
||||||
composerFullScreen = composerFullScreen,
|
composerFullScreen = composerFullScreen,
|
||||||
onComposerFullScreenChange = composerViewModel::onComposerFullScreenChange,
|
onComposerFullScreenChange = composerViewModel::onComposerFullScreenChange,
|
||||||
onComposerTextChange = composerViewModel::updateText,
|
onComposerTextChange = composerViewModel::updateText,
|
||||||
|
composerMode = composerMode,
|
||||||
|
onCloseSpecialMode = viewModel::setNormalMode,
|
||||||
composerCanSendMessage = composerCanSendMessage,
|
composerCanSendMessage = composerCanSendMessage,
|
||||||
composerText = composerText,
|
composerText = composerText,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
@ -107,6 +111,16 @@ fun MessagesScreen(
|
||||||
onActionClicked = {
|
onActionClicked = {
|
||||||
viewModel.handleItemAction(it)
|
viewModel.handleItemAction(it)
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
val targetEvent = viewModel.getTargetEvent()
|
||||||
|
when (it) {
|
||||||
|
is MessagesItemAction.Edit -> {
|
||||||
|
// Entering Edit mode, update the text in the composer.
|
||||||
|
val newComposerText =
|
||||||
|
(targetEvent?.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty()
|
||||||
|
composerViewModel.updateText(newComposerText)
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
actionsSheetState.hide()
|
actionsSheetState.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +147,8 @@ fun MessagesScreenContent(
|
||||||
composerFullScreen: Boolean,
|
composerFullScreen: Boolean,
|
||||||
onComposerFullScreenChange: () -> Unit,
|
onComposerFullScreenChange: () -> Unit,
|
||||||
onComposerTextChange: (CharSequence) -> Unit,
|
onComposerTextChange: (CharSequence) -> Unit,
|
||||||
|
composerMode: MessageComposerMode,
|
||||||
|
onCloseSpecialMode: () -> Unit,
|
||||||
composerCanSendMessage: Boolean,
|
composerCanSendMessage: Boolean,
|
||||||
composerText: StableCharSequence?,
|
composerText: StableCharSequence?,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
|
|
@ -155,6 +171,8 @@ fun MessagesScreenContent(
|
||||||
onSendMessage = onSendMessage,
|
onSendMessage = onSendMessage,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
composerMode = composerMode,
|
||||||
|
onCloseSpecialMode = onCloseSpecialMode,
|
||||||
composerFullScreen = composerFullScreen,
|
composerFullScreen = composerFullScreen,
|
||||||
onComposerFullScreenChange = onComposerFullScreenChange,
|
onComposerFullScreenChange = onComposerFullScreenChange,
|
||||||
onComposerTextChange = onComposerTextChange,
|
onComposerTextChange = onComposerTextChange,
|
||||||
|
|
@ -174,6 +192,8 @@ fun MessagesContent(
|
||||||
onSendMessage: (String) -> Unit,
|
onSendMessage: (String) -> Unit,
|
||||||
onClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
|
onClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
|
||||||
onLongClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
|
onLongClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
|
||||||
|
composerMode: MessageComposerMode,
|
||||||
|
onCloseSpecialMode: () -> Unit,
|
||||||
composerFullScreen: Boolean,
|
composerFullScreen: Boolean,
|
||||||
onComposerFullScreenChange: () -> Unit,
|
onComposerFullScreenChange: () -> Unit,
|
||||||
onComposerTextChange: (CharSequence) -> Unit,
|
onComposerTextChange: (CharSequence) -> Unit,
|
||||||
|
|
@ -202,6 +222,8 @@ fun MessagesContent(
|
||||||
onSendMessage = onSendMessage,
|
onSendMessage = onSendMessage,
|
||||||
fullscreen = composerFullScreen,
|
fullscreen = composerFullScreen,
|
||||||
onFullscreenToggle = onComposerFullScreenChange,
|
onFullscreenToggle = onComposerFullScreenChange,
|
||||||
|
composerMode = composerMode,
|
||||||
|
onCloseSpecialMode = onCloseSpecialMode,
|
||||||
onComposerTextChange = onComposerTextChange,
|
onComposerTextChange = onComposerTextChange,
|
||||||
composerCanSendMessage = composerCanSendMessage,
|
composerCanSendMessage = composerCanSendMessage,
|
||||||
composerText = composerText?.charSequence?.toString(),
|
composerText = composerText?.charSequence?.toString(),
|
||||||
|
|
@ -363,6 +385,7 @@ fun MessageEventRow(
|
||||||
groupPosition = messageEvent.groupPosition,
|
groupPosition = messageEvent.groupPosition,
|
||||||
isMine = messageEvent.isMine,
|
isMine = messageEvent.isMine,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
isHighlighted = messageEvent.isHighlighted,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,15 @@ import io.element.android.x.features.messages.model.MessagesItemActionsSheetStat
|
||||||
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
||||||
import io.element.android.x.features.messages.model.MessagesViewState
|
import io.element.android.x.features.messages.model.MessagesViewState
|
||||||
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
|
||||||
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
|
||||||
import io.element.android.x.matrix.MatrixClient
|
import io.element.android.x.matrix.MatrixClient
|
||||||
import io.element.android.x.matrix.MatrixInstance
|
import io.element.android.x.matrix.MatrixInstance
|
||||||
import io.element.android.x.matrix.media.MediaResolver
|
import io.element.android.x.matrix.media.MediaResolver
|
||||||
import io.element.android.x.matrix.room.MatrixRoom
|
import io.element.android.x.matrix.room.MatrixRoom
|
||||||
import io.element.android.x.matrix.timeline.MatrixTimeline
|
import io.element.android.x.matrix.timeline.MatrixTimeline
|
||||||
|
import io.element.android.x.textcomposer.MessageComposerMode
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ class MessagesViewModel(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val timeline: MatrixTimeline,
|
private val timeline: MatrixTimeline,
|
||||||
private val messageTimelineItemStateMapper: MessageTimelineItemStateMapper,
|
private val messageTimelineItemStateFactory: MessageTimelineItemStateFactory,
|
||||||
private val initialState: MessagesViewState
|
private val initialState: MessagesViewState
|
||||||
) :
|
) :
|
||||||
MavericksViewModel<MessagesViewState>(initialState) {
|
MavericksViewModel<MessagesViewState>(initialState) {
|
||||||
|
|
@ -42,13 +42,13 @@ class MessagesViewModel(
|
||||||
val matrix = MatrixInstance.getInstance()
|
val matrix = MatrixInstance.getInstance()
|
||||||
val client = matrix.activeClient()
|
val client = matrix.activeClient()
|
||||||
val room = client.getRoom(state.roomId) ?: return null
|
val room = client.getRoom(state.roomId) ?: return null
|
||||||
val messageTimelineItemStateMapper =
|
val messageTimelineItemStateFactory =
|
||||||
MessageTimelineItemStateMapper(client, room, Dispatchers.Default)
|
MessageTimelineItemStateFactory(client, room, Dispatchers.Default)
|
||||||
return MessagesViewModel(
|
return MessagesViewModel(
|
||||||
client,
|
client,
|
||||||
room,
|
room,
|
||||||
room.timeline(),
|
room.timeline(),
|
||||||
messageTimelineItemStateMapper,
|
messageTimelineItemStateFactory,
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -66,25 +66,72 @@ class MessagesViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendMessage(text: String) {
|
fun sendMessage(text: String) {
|
||||||
viewModelScope.launch {
|
withState { state ->
|
||||||
timeline.sendMessage(text)
|
viewModelScope.launch {
|
||||||
|
when (state.composerMode) {
|
||||||
|
is MessageComposerMode.Normal -> timeline.sendMessage(text)
|
||||||
|
is MessageComposerMode.Edit -> timeline.editMessage(
|
||||||
|
state.composerMode.eventId,
|
||||||
|
text
|
||||||
|
)
|
||||||
|
is MessageComposerMode.Quote -> TODO()
|
||||||
|
is MessageComposerMode.Reply -> timeline.replyMessage(
|
||||||
|
state.composerMode.eventId,
|
||||||
|
text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Reset composer
|
||||||
|
setNormalMode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getTargetEvent(): MessagesTimelineItemState.MessageEvent? {
|
||||||
|
val currentState = awaitState()
|
||||||
|
return currentState.itemActionsSheetState.invoke()?.targetItem
|
||||||
|
}
|
||||||
|
|
||||||
fun handleItemAction(action: MessagesItemAction) {
|
fun handleItemAction(action: MessagesItemAction) {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
val currentState = awaitState()
|
val currentState = awaitState()
|
||||||
Timber.v("Handle $action for ${currentState.itemActionsSheetState}")
|
Timber.v("Handle $action for ${currentState.itemActionsSheetState}")
|
||||||
val targetEvent =
|
val targetEvent = getTargetEvent()
|
||||||
currentState.itemActionsSheetState.invoke()?.targetItem ?: return@launch
|
?: return@launch
|
||||||
when (action) {
|
when (action) {
|
||||||
MessagesItemAction.Copy -> notImplementedYet()
|
MessagesItemAction.Copy -> notImplementedYet()
|
||||||
MessagesItemAction.Forward -> notImplementedYet()
|
MessagesItemAction.Forward -> notImplementedYet()
|
||||||
MessagesItemAction.Redact -> handleActionRedact(targetEvent)
|
MessagesItemAction.Redact -> handleActionRedact(targetEvent)
|
||||||
|
MessagesItemAction.Edit -> handleActionEdit(targetEvent)
|
||||||
|
MessagesItemAction.Reply -> handleActionReply(targetEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setNormalMode() {
|
||||||
|
setComposerMode(MessageComposerMode.Normal(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleActionEdit(targetEvent: MessagesTimelineItemState.MessageEvent) {
|
||||||
|
setComposerMode(
|
||||||
|
MessageComposerMode.Edit(
|
||||||
|
targetEvent.id,
|
||||||
|
(targetEvent.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleActionReply(targetEvent: MessagesTimelineItemState.MessageEvent) {
|
||||||
|
setComposerMode(MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setComposerMode(mode: MessageComposerMode) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
composerMode = mode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun notImplementedYet() {
|
private fun notImplementedYet() {
|
||||||
setSnackbarContent("Not implemented yet!")
|
setSnackbarContent("Not implemented yet!")
|
||||||
}
|
}
|
||||||
|
|
@ -110,10 +157,12 @@ class MessagesViewModel(
|
||||||
emptyList()
|
emptyList()
|
||||||
} else {
|
} else {
|
||||||
mutableListOf(
|
mutableListOf(
|
||||||
|
MessagesItemAction.Reply,
|
||||||
MessagesItemAction.Forward,
|
MessagesItemAction.Forward,
|
||||||
MessagesItemAction.Copy,
|
MessagesItemAction.Copy,
|
||||||
).also {
|
).also {
|
||||||
if (messagesTimelineItemState.isMine) {
|
if (messagesTimelineItemState.isMine) {
|
||||||
|
it.add(MessagesItemAction.Edit)
|
||||||
it.add(MessagesItemAction.Redact)
|
it.add(MessagesItemAction.Redact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,8 +189,20 @@ class MessagesViewModel(
|
||||||
}
|
}
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
timeline.timelineItems()
|
combine(
|
||||||
.map(messageTimelineItemStateMapper::map)
|
timeline.timelineItems(),
|
||||||
|
stateFlow.map {
|
||||||
|
when (it.composerMode) {
|
||||||
|
is MessageComposerMode.Normal -> null
|
||||||
|
is MessageComposerMode.Edit -> it.composerMode.eventId
|
||||||
|
is MessageComposerMode.Quote -> null
|
||||||
|
is MessageComposerMode.Reply -> it.composerMode.eventId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
) { timelineItems, highlightedEventId ->
|
||||||
|
messageTimelineItemStateFactory.create(timelineItems, highlightedEventId)
|
||||||
|
}
|
||||||
.execute {
|
.execute {
|
||||||
copy(timelineItems = it)
|
copy(timelineItems = it)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
|
@ -25,6 +24,7 @@ fun MessageEventBubble(
|
||||||
groupPosition: MessagesItemGroupPosition,
|
groupPosition: MessagesItemGroupPosition,
|
||||||
isMine: Boolean,
|
isMine: Boolean,
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
|
isHighlighted: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
|
|
@ -65,17 +65,25 @@ fun MessageEventBubble(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val backgroundBubbleColor = if (isMine) {
|
val backgroundBubbleColor = if (isHighlighted) {
|
||||||
if (LocalIsDarkTheme.current) {
|
if (LocalIsDarkTheme.current) {
|
||||||
SystemGrey5Dark
|
MessageHighlightDark
|
||||||
} else {
|
} else {
|
||||||
SystemGrey5Light
|
MessageHighlightLight
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (LocalIsDarkTheme.current) {
|
if (isMine) {
|
||||||
SystemGrey6Dark
|
if (LocalIsDarkTheme.current) {
|
||||||
|
SystemGrey5Dark
|
||||||
|
} else {
|
||||||
|
SystemGrey5Light
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
SystemGrey6Light
|
if (LocalIsDarkTheme.current) {
|
||||||
|
SystemGrey6Dark
|
||||||
|
} else {
|
||||||
|
SystemGrey6Light
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val bubbleShape = bubbleShape()
|
val bubbleShape = bubbleShape()
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,6 @@ sealed class MessagesItemAction(
|
||||||
object Forward : MessagesItemAction("Forward", VectorIcons.ArrowForward)
|
object Forward : MessagesItemAction("Forward", VectorIcons.ArrowForward)
|
||||||
object Copy : MessagesItemAction("Copy", VectorIcons.Copy)
|
object Copy : MessagesItemAction("Copy", VectorIcons.Copy)
|
||||||
object Redact : MessagesItemAction("Redact", VectorIcons.Delete, destructive = true)
|
object Redact : MessagesItemAction("Redact", VectorIcons.Delete, destructive = true)
|
||||||
|
object Reply : MessagesItemAction("Reply", VectorIcons.Reply)
|
||||||
|
object Edit : MessagesItemAction("Edit", VectorIcons.Edit)
|
||||||
}
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ sealed interface MessagesTimelineItemState {
|
||||||
val content: MessagesTimelineItemContent,
|
val content: MessagesTimelineItemContent,
|
||||||
val sentTime: String = "",
|
val sentTime: String = "",
|
||||||
val isMine: Boolean = false,
|
val isMine: Boolean = false,
|
||||||
|
val isHighlighted: Boolean = false,
|
||||||
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None,
|
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None,
|
||||||
val reactionsState: MessagesItemReactionState
|
val reactionsState: MessagesItemReactionState
|
||||||
) : MessagesTimelineItemState {
|
) : MessagesTimelineItemState {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.x.textcomposer.MessageComposerMode
|
||||||
|
|
||||||
data class MessagesViewState(
|
data class MessagesViewState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
|
|
@ -13,6 +14,8 @@ data class MessagesViewState(
|
||||||
val hasMoreToLoad: Boolean = true,
|
val hasMoreToLoad: Boolean = true,
|
||||||
val itemActionsSheetState: Async<MessagesItemActionsSheetState> = Uninitialized,
|
val itemActionsSheetState: Async<MessagesItemActionsSheetState> = Uninitialized,
|
||||||
val snackbarContent: String? = null,
|
val snackbarContent: String? = null,
|
||||||
|
// TODO Highlight item in reply / edit in the timeline
|
||||||
|
val composerMode: MessageComposerMode = MessageComposerMode.Normal(""),
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
|
|
|
||||||
|
|
@ -34,3 +34,8 @@ val Melon = Color(0xFFFF812D)
|
||||||
val ElementGreen = Color(0xFF0DBD8B)
|
val ElementGreen = Color(0xFF0DBD8B)
|
||||||
val ElementOrange = Color(0xFFD9B072)
|
val ElementOrange = Color(0xFFD9B072)
|
||||||
val Vermilion = Color(0xFFFF5B55)
|
val Vermilion = Color(0xFFFF5B55)
|
||||||
|
|
||||||
|
// TODO Update color
|
||||||
|
val MessageHighlightLight = Azure
|
||||||
|
// TODO Update color
|
||||||
|
val MessageHighlightDark = Azure
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,6 @@ object VectorIcons {
|
||||||
val Copy = R.drawable.ic_content_copy
|
val Copy = R.drawable.ic_content_copy
|
||||||
val ArrowForward = R.drawable.ic_content_arrow_forward
|
val ArrowForward = R.drawable.ic_content_arrow_forward
|
||||||
val Delete = R.drawable.ic_baseline_delete_outline_24
|
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
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:autoMirrored="true" 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="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -82,6 +82,22 @@ class MatrixRoom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun editMessage(originalEventId: String, message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||||
|
val transactionId = genTransactionId()
|
||||||
|
val content = messageEventContentFromMarkdown(message)
|
||||||
|
runCatching {
|
||||||
|
room.edit(/* TODO use content */ message, originalEventId, transactionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun replyMessage(eventId: String, message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||||
|
val transactionId = genTransactionId()
|
||||||
|
val content = messageEventContentFromMarkdown(message)
|
||||||
|
runCatching {
|
||||||
|
room.sendReply(/* TODO use content */ message, eventId, transactionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun redactEvent(eventId: String, reason: String? = null, ) = withContext(coroutineDispatchers.io) {
|
suspend fun redactEvent(eventId: String, reason: String? = null, ) = withContext(coroutineDispatchers.io) {
|
||||||
val transactionId = genTransactionId()
|
val transactionId = genTransactionId()
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,14 @@ class MatrixTimeline(
|
||||||
return matrixRoom.sendMessage(message)
|
return matrixRoom.sendMessage(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun editMessage(originalEventId: String, message: String): Result<Unit> {
|
||||||
|
return matrixRoom.editMessage(originalEventId, message = message)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun replyMessage(inReplyToEventId: String, message: String): Result<Unit> {
|
||||||
|
return matrixRoom.replyMessage(inReplyToEventId, message)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onUpdate(update: TimelineDiff) {
|
override fun onUpdate(update: TimelineDiff) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
updateTimelineItems {
|
updateTimelineItems {
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,18 @@ package io.element.android.x.textcomposer
|
||||||
sealed interface MessageComposerMode {
|
sealed interface MessageComposerMode {
|
||||||
data class Normal(val content: CharSequence?) : MessageComposerMode
|
data class Normal(val content: CharSequence?) : MessageComposerMode
|
||||||
|
|
||||||
sealed class Special(open val event: Any /* TODO set correct type here */, open val defaultContent: CharSequence) : MessageComposerMode
|
sealed class Special(open val eventId: String, open val defaultContent: CharSequence) :
|
||||||
data class Edit(override val event: Any, override val defaultContent: CharSequence) : Special(event, defaultContent)
|
MessageComposerMode
|
||||||
class Quote(override val event: Any, override val defaultContent: CharSequence) : Special(event, defaultContent)
|
|
||||||
class Reply(override val event: Any, override val defaultContent: CharSequence) : Special(event, defaultContent)
|
data class Edit(override val eventId: String, override val defaultContent: CharSequence) :
|
||||||
|
Special(eventId, defaultContent)
|
||||||
|
|
||||||
|
class Quote(override val eventId: String, override val defaultContent: CharSequence) :
|
||||||
|
Special(eventId, defaultContent)
|
||||||
|
|
||||||
|
class Reply(
|
||||||
|
val senderName: String,
|
||||||
|
override val eventId: String,
|
||||||
|
override val defaultContent: CharSequence
|
||||||
|
) : Special(eventId, defaultContent)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||||
if (isFullScreen) R.drawable.ic_composer_collapse else R.drawable.ic_composer_full_screen
|
if (isFullScreen) R.drawable.ic_composer_collapse else R.drawable.ic_composer_full_screen
|
||||||
)
|
)
|
||||||
|
|
||||||
views.bottomSheetHandle.isVisible = isFullScreen
|
views.bottomSheetHandle.isVisible = false // EAx: always gone, we do not have a bottom sheet here. // isFullScreen
|
||||||
if (manageKeyboard) {
|
if (manageKeyboard) {
|
||||||
if (isFullScreen) {
|
if (isFullScreen) {
|
||||||
editText.showKeyboard(true)
|
editText.showKeyboard(true)
|
||||||
|
|
@ -171,7 +171,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||||
views.richTextComposerEditText.maxLines = maxLines
|
views.richTextComposerEditText.maxLines = maxLines
|
||||||
views.plainTextComposerEditText.maxLines = maxLines
|
views.plainTextComposerEditText.maxLines = maxLines
|
||||||
|
|
||||||
views.bottomSheetHandle.isVisible = true
|
views.bottomSheetHandle.isVisible = false // EAx: always gone, we do not have a bottom sheet here.
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -506,10 +506,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||||
views.composerModeIconView.setImageResource(R.drawable.ic_quote)
|
views.composerModeIconView.setImageResource(R.drawable.ic_quote)
|
||||||
}
|
}
|
||||||
is MessageComposerMode.Reply -> {
|
is MessageComposerMode.Reply -> {
|
||||||
// TODO We need sender info
|
val userName = mode.senderName
|
||||||
// val senderInfo = mode.event.senderInfo
|
|
||||||
val userName =
|
|
||||||
"TODO Sender name" // senderInfo.displayName ?: senderInfo.disambiguatedDisplayName
|
|
||||||
views.composerModeTitleView.text =
|
views.composerModeTitleView.text =
|
||||||
resources.getString(R.string.replying_to, userName)
|
resources.getString(R.string.replying_to, userName)
|
||||||
views.composerModeIconView.setImageResource(R.drawable.ic_reply)
|
views.composerModeIconView.setImageResource(R.drawable.ic_reply)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ fun TextComposer(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
fullscreen: Boolean,
|
fullscreen: Boolean,
|
||||||
onFullscreenToggle: () -> Unit,
|
onFullscreenToggle: () -> Unit,
|
||||||
|
composerMode: MessageComposerMode,
|
||||||
|
onCloseSpecialMode: () -> Unit,
|
||||||
onComposerTextChange: (CharSequence) -> Unit,
|
onComposerTextChange: (CharSequence) -> Unit,
|
||||||
composerCanSendMessage: Boolean,
|
composerCanSendMessage: Boolean,
|
||||||
composerText: String?,
|
composerText: String?,
|
||||||
|
|
@ -50,6 +52,7 @@ fun TextComposer(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCloseRelatedMessage() {
|
override fun onCloseRelatedMessage() {
|
||||||
|
onCloseSpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSendMessage(text: CharSequence) {
|
override fun onSendMessage(text: CharSequence) {
|
||||||
|
|
@ -70,7 +73,7 @@ fun TextComposer(
|
||||||
}
|
}
|
||||||
setFullScreen(fullscreen, true)
|
setFullScreen(fullscreen, true)
|
||||||
(this as MessageComposerView).apply {
|
(this as MessageComposerView).apply {
|
||||||
setup(isInDarkMode)
|
setup(isInDarkMode, composerMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -83,6 +86,7 @@ fun TextComposer(
|
||||||
// Example of Compose -> View communication
|
// Example of Compose -> View communication
|
||||||
val messageComposerView = (view as MessageComposerView)
|
val messageComposerView = (view as MessageComposerView)
|
||||||
view.setFullScreen(fullscreen, false)
|
view.setFullScreen(fullscreen, false)
|
||||||
|
// TODO messageComposerView.renderComposerMode(composerMode)
|
||||||
messageComposerView.sendButton.isInvisible = !composerCanSendMessage
|
messageComposerView.sendButton.isInvisible = !composerCanSendMessage
|
||||||
messageComposerView.setTextIfDifferent(composerText ?: "")
|
messageComposerView.setTextIfDifferent(composerText ?: "")
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +111,7 @@ private fun FakeComposer(modifier: Modifier) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageComposerView.setup(isDarkMode: Boolean) {
|
private fun MessageComposerView.setup(isDarkMode: Boolean, composerMode: MessageComposerMode) {
|
||||||
val editTextColor = if (isDarkMode) {
|
val editTextColor = if (isDarkMode) {
|
||||||
Color.WHITE
|
Color.WHITE
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -118,6 +122,7 @@ private fun MessageComposerView.setup(isDarkMode: Boolean) {
|
||||||
editText.setHint(ElementR.string.room_message_placeholder)
|
editText.setHint(ElementR.string.room_message_placeholder)
|
||||||
emojiButton?.isVisible = true
|
emojiButton?.isVisible = true
|
||||||
sendButton.isVisible = true
|
sendButton.isVisible = true
|
||||||
|
// TODO renderComposerMode(composerMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
|
@ -128,6 +133,8 @@ fun TextComposerPreview() {
|
||||||
fullscreen = false,
|
fullscreen = false,
|
||||||
onFullscreenToggle = { },
|
onFullscreenToggle = { },
|
||||||
onComposerTextChange = {},
|
onComposerTextChange = {},
|
||||||
|
composerMode = MessageComposerMode.Normal(""),
|
||||||
|
onCloseSpecialMode = {},
|
||||||
composerCanSendMessage = true,
|
composerCanSendMessage = true,
|
||||||
composerText = "Message",
|
composerText = "Message",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,19 @@
|
||||||
android:id="@+id/composerLayout"
|
android:id="@+id/composerLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical">
|
||||||
android:background="@drawable/bg_composer_rich_bottom_sheet">
|
|
||||||
|
<!-- EAx: Remove android:background="@drawable/bg_composer_rich_bottom_sheet" from ^ -->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
There are issues here:
|
||||||
|
|
||||||
|
View class androidx.appcompat.widget.AppCompatImageView is an AppCompat widget that can only be used with a Theme.AppCompat theme (or descendant).
|
||||||
|
View class io.element.android.wysiwyg.EditorEditText is an AppCompat widget that can only be used with a Theme.AppCompat theme (or descendant).
|
||||||
|
layout_constraintHeight_default="wrap" is deprecated. Use layout_height="WRAP_CONTENT" and layout_constrainedHeight="true" instead.
|
||||||
|
View class com.google.android.material.textfield.TextInputEditText is an AppCompat widget that can only be used with a Theme.AppCompat theme (or descendant).
|
||||||
|
layout_constraintHeight_default="wrap" is deprecated. Use layout_height="WRAP_CONTENT" and layout_constrainedHeight="true" instead.
|
||||||
|
-->
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/bottomSheetHandle"
|
android:id="@+id/bottomSheetHandle"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue