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).
|
||||
- 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`.
|
||||
|
||||
### 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(
|
||||
message = message,
|
||||
isSelected = isSelected,
|
||||
isMultiSelecting = state.actionState.mode == MessageSelectionMode.MULTI,
|
||||
reactions = state.reactionByMessageId[message.id].orEmpty(),
|
||||
onAttachmentImageClick = { imageUrl ->
|
||||
val idx = allImageUrls.indexOf(imageUrl)
|
||||
viewerImageIndex = if (idx >= 0) idx else null
|
||||
},
|
||||
onClick = {
|
||||
actionMenuMessage = null
|
||||
if (state.actionState.mode == MessageSelectionMode.MULTI) {
|
||||
onToggleMessageMultiSelection(message)
|
||||
} else {
|
||||
onSelectMessage(message)
|
||||
actionMenuMessage = message
|
||||
}
|
||||
},
|
||||
onLongPress = {
|
||||
if (state.actionState.mode == MessageSelectionMode.MULTI) {
|
||||
onToggleMessageMultiSelection(message)
|
||||
} else {
|
||||
onSelectMessage(message)
|
||||
actionMenuMessage = message
|
||||
actionMenuMessage = null
|
||||
onEnterMultiSelect(message)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -336,7 +339,7 @@ fun ChatScreen(
|
||||
Button(onClick = {}, enabled = false) {
|
||||
Text("Pin")
|
||||
}
|
||||
Button(onClick = { actionMenuMessage = null }) {
|
||||
Button(onClick = { actionMenuMessage = null; onClearSelection() }) {
|
||||
Text("Close")
|
||||
}
|
||||
}
|
||||
@@ -344,7 +347,9 @@ fun ChatScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (state.actionState.hasSelection) {
|
||||
if (state.actionState.hasSelection &&
|
||||
!(state.actionState.mode == MessageSelectionMode.SINGLE && actionMenuMessage != null)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -359,12 +364,15 @@ fun ChatScreen(
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Button(onClick = { onDeleteSelected(false) }) { Text("Delete") }
|
||||
Button(
|
||||
onClick = { onDeleteSelected(true) },
|
||||
enabled = state.actionState.mode == MessageSelectionMode.SINGLE && state.selectedCanDeleteForAll,
|
||||
) { Text("Del for all") }
|
||||
if (state.actionState.mode == MessageSelectionMode.SINGLE) {
|
||||
if (state.actionState.mode == MessageSelectionMode.MULTI) {
|
||||
Button(onClick = onForwardSelected) { Text("Forward") }
|
||||
Button(onClick = { onDeleteSelected(false) }) { Text("Delete") }
|
||||
} else {
|
||||
Button(onClick = { onDeleteSelected(false) }) { Text("Delete") }
|
||||
Button(
|
||||
onClick = { onDeleteSelected(true) },
|
||||
enabled = state.selectedCanDeleteForAll,
|
||||
) { Text("Del for all") }
|
||||
Button(
|
||||
onClick = { state.selectedMessage?.let(onEditSelected) },
|
||||
enabled = state.selectedCanEdit,
|
||||
@@ -383,7 +391,10 @@ fun ChatScreen(
|
||||
if (state.actionState.mode == MessageSelectionMode.SINGLE && state.selectedMessage != null) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -610,6 +621,7 @@ private fun Uri.readMediaPayload(context: Context): PickedMediaPayload? {
|
||||
private fun MessageBubble(
|
||||
message: MessageItem,
|
||||
isSelected: Boolean,
|
||||
isMultiSelecting: Boolean,
|
||||
reactions: List<ru.daemonlord.messenger.domain.message.model.MessageReaction>,
|
||||
onAttachmentImageClick: (String) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
@@ -628,19 +640,31 @@ private fun MessageBubble(
|
||||
}
|
||||
val alignment = if (isOutgoing) Alignment.End else Alignment.Start
|
||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = alignment) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.86f)
|
||||
.background(
|
||||
color = if (isSelected) MaterialTheme.colorScheme.tertiaryContainer else bubbleColor,
|
||||
shape = bubbleShape,
|
||||
)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongPress,
|
||||
)
|
||||
.padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(0.92f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start,
|
||||
) {
|
||||
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) {
|
||||
Text(
|
||||
text = "✓ Selected",
|
||||
@@ -764,6 +788,14 @@ private fun MessageBubble(
|
||||
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