android: split tap context menu and long-press multi-select in chat
Some checks are pending
CI / test (push) Has started running
Some checks are pending
CI / test (push) Has started running
This commit is contained in:
@@ -323,3 +323,10 @@
|
|||||||
- Added `EncryptedPrefsTokenRepository` backed by `EncryptedSharedPreferences` and Android `MasterKey` (Keystore).
|
- Added `EncryptedPrefsTokenRepository` backed by `EncryptedSharedPreferences` and Android `MasterKey` (Keystore).
|
||||||
- Switched DI token binding from DataStore token repository to encrypted shared preferences repository.
|
- Switched DI token binding from DataStore token repository to encrypted shared preferences repository.
|
||||||
- Kept DataStore for non-token app settings and renamed preferences file to `messenger_preferences.preferences_pb`.
|
- Kept DataStore for non-token app settings and renamed preferences file to `messenger_preferences.preferences_pb`.
|
||||||
|
|
||||||
|
### Step 54 - Message interactions: tap menu vs long-press select
|
||||||
|
- Updated chat message gesture behavior to match Telegram pattern:
|
||||||
|
- tap opens contextual message menu with reactions/actions,
|
||||||
|
- long-press enters multi-select mode directly.
|
||||||
|
- Hid single-selection action bars while contextual menu is visible to avoid mixed UX states.
|
||||||
|
- Improved multi-select visual affordance with per-message selection indicator circles.
|
||||||
|
|||||||
@@ -248,23 +248,26 @@ fun ChatScreen(
|
|||||||
MessageBubble(
|
MessageBubble(
|
||||||
message = message,
|
message = message,
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
|
isMultiSelecting = state.actionState.mode == MessageSelectionMode.MULTI,
|
||||||
reactions = state.reactionByMessageId[message.id].orEmpty(),
|
reactions = state.reactionByMessageId[message.id].orEmpty(),
|
||||||
onAttachmentImageClick = { imageUrl ->
|
onAttachmentImageClick = { imageUrl ->
|
||||||
val idx = allImageUrls.indexOf(imageUrl)
|
val idx = allImageUrls.indexOf(imageUrl)
|
||||||
viewerImageIndex = if (idx >= 0) idx else null
|
viewerImageIndex = if (idx >= 0) idx else null
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
actionMenuMessage = null
|
|
||||||
if (state.actionState.mode == MessageSelectionMode.MULTI) {
|
if (state.actionState.mode == MessageSelectionMode.MULTI) {
|
||||||
onToggleMessageMultiSelection(message)
|
onToggleMessageMultiSelection(message)
|
||||||
|
} else {
|
||||||
|
onSelectMessage(message)
|
||||||
|
actionMenuMessage = message
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress = {
|
onLongPress = {
|
||||||
if (state.actionState.mode == MessageSelectionMode.MULTI) {
|
if (state.actionState.mode == MessageSelectionMode.MULTI) {
|
||||||
onToggleMessageMultiSelection(message)
|
onToggleMessageMultiSelection(message)
|
||||||
} else {
|
} else {
|
||||||
onSelectMessage(message)
|
actionMenuMessage = null
|
||||||
actionMenuMessage = message
|
onEnterMultiSelect(message)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -336,7 +339,7 @@ fun ChatScreen(
|
|||||||
Button(onClick = {}, enabled = false) {
|
Button(onClick = {}, enabled = false) {
|
||||||
Text("Pin")
|
Text("Pin")
|
||||||
}
|
}
|
||||||
Button(onClick = { actionMenuMessage = null }) {
|
Button(onClick = { actionMenuMessage = null; onClearSelection() }) {
|
||||||
Text("Close")
|
Text("Close")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,7 +347,9 @@ fun ChatScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.actionState.hasSelection) {
|
if (state.actionState.hasSelection &&
|
||||||
|
!(state.actionState.mode == MessageSelectionMode.SINGLE && actionMenuMessage != null)
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -359,12 +364,15 @@ fun ChatScreen(
|
|||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
Button(onClick = { onDeleteSelected(false) }) { Text("Delete") }
|
if (state.actionState.mode == MessageSelectionMode.MULTI) {
|
||||||
Button(
|
Button(onClick = onForwardSelected) { Text("Forward") }
|
||||||
onClick = { onDeleteSelected(true) },
|
Button(onClick = { onDeleteSelected(false) }) { Text("Delete") }
|
||||||
enabled = state.actionState.mode == MessageSelectionMode.SINGLE && state.selectedCanDeleteForAll,
|
} else {
|
||||||
) { Text("Del for all") }
|
Button(onClick = { onDeleteSelected(false) }) { Text("Delete") }
|
||||||
if (state.actionState.mode == MessageSelectionMode.SINGLE) {
|
Button(
|
||||||
|
onClick = { onDeleteSelected(true) },
|
||||||
|
enabled = state.selectedCanDeleteForAll,
|
||||||
|
) { Text("Del for all") }
|
||||||
Button(
|
Button(
|
||||||
onClick = { state.selectedMessage?.let(onEditSelected) },
|
onClick = { state.selectedMessage?.let(onEditSelected) },
|
||||||
enabled = state.selectedCanEdit,
|
enabled = state.selectedCanEdit,
|
||||||
@@ -383,7 +391,10 @@ fun ChatScreen(
|
|||||||
if (state.actionState.mode == MessageSelectionMode.SINGLE && state.selectedMessage != null) {
|
if (state.actionState.mode == MessageSelectionMode.SINGLE && state.selectedMessage != null) {
|
||||||
Button(onClick = { onReplySelected(state.selectedMessage) }) { Text("Reply") }
|
Button(onClick = { onReplySelected(state.selectedMessage) }) { Text("Reply") }
|
||||||
}
|
}
|
||||||
Button(onClick = onForwardSelected) {
|
Button(
|
||||||
|
onClick = onForwardSelected,
|
||||||
|
enabled = state.actionState.mode == MessageSelectionMode.MULTI,
|
||||||
|
) {
|
||||||
Text(if (state.actionState.mode == MessageSelectionMode.MULTI) "Forward selected" else "Forward")
|
Text(if (state.actionState.mode == MessageSelectionMode.MULTI) "Forward selected" else "Forward")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -610,6 +621,7 @@ private fun Uri.readMediaPayload(context: Context): PickedMediaPayload? {
|
|||||||
private fun MessageBubble(
|
private fun MessageBubble(
|
||||||
message: MessageItem,
|
message: MessageItem,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
|
isMultiSelecting: Boolean,
|
||||||
reactions: List<ru.daemonlord.messenger.domain.message.model.MessageReaction>,
|
reactions: List<ru.daemonlord.messenger.domain.message.model.MessageReaction>,
|
||||||
onAttachmentImageClick: (String) -> Unit,
|
onAttachmentImageClick: (String) -> Unit,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
@@ -628,19 +640,31 @@ private fun MessageBubble(
|
|||||||
}
|
}
|
||||||
val alignment = if (isOutgoing) Alignment.End else Alignment.Start
|
val alignment = if (isOutgoing) Alignment.End else Alignment.Start
|
||||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = alignment) {
|
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = alignment) {
|
||||||
Column(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(0.92f),
|
||||||
.fillMaxWidth(0.86f)
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
.background(
|
horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start,
|
||||||
color = if (isSelected) MaterialTheme.colorScheme.tertiaryContainer else bubbleColor,
|
|
||||||
shape = bubbleShape,
|
|
||||||
)
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongPress,
|
|
||||||
)
|
|
||||||
.padding(horizontal = 10.dp, vertical = 7.dp),
|
|
||||||
) {
|
) {
|
||||||
|
if (isMultiSelecting && !isOutgoing) {
|
||||||
|
Text(
|
||||||
|
text = if (isSelected) "◉" else "○",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(end = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.92f)
|
||||||
|
.background(
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.tertiaryContainer else bubbleColor,
|
||||||
|
shape = bubbleShape,
|
||||||
|
)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongPress,
|
||||||
|
)
|
||||||
|
.padding(horizontal = 10.dp, vertical = 7.dp),
|
||||||
|
) {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
Text(
|
Text(
|
||||||
text = "✓ Selected",
|
text = "✓ Selected",
|
||||||
@@ -764,6 +788,14 @@ private fun MessageBubble(
|
|||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (isMultiSelecting && isOutgoing) {
|
||||||
|
Text(
|
||||||
|
text = if (isSelected) "◉" else "○",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(start = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user