Delete individual messages via long-press; leave-group already shipped
Long-press any bubble for Copy and Delete for me (Unsend on your own), in 1:1 and group threads. Vault.deleteLine rewrites the thread minus the line, keyed by timestamp+side+text — messages carry no stable id here. Leaving a group was already wired. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
68329bcf0f
commit
1799ada15b
@@ -5,7 +5,13 @@ import android.content.pm.PackageManager
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -1263,6 +1269,7 @@ private fun PresenceDot(status: String) {
|
||||
}
|
||||
|
||||
/** One conversation. */
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ColumnScope.ChatScreen(
|
||||
model: MessengerViewModel,
|
||||
@@ -1377,6 +1384,12 @@ fun ColumnScope.ChatScreen(
|
||||
chat.group == null -> chat.peerId
|
||||
else -> line.author.takeIf { it != 0L }
|
||||
}
|
||||
var menuOpen by remember(line.at, line.mine) { mutableStateOf(false) }
|
||||
val clipboard = LocalClipboardManager.current
|
||||
Box(modifier = Modifier.combinedClickable(
|
||||
onClick = {},
|
||||
onLongClick = { menuOpen = true },
|
||||
)) {
|
||||
Bubble(
|
||||
line = line,
|
||||
model = model,
|
||||
@@ -1387,11 +1400,25 @@ fun ColumnScope.ChatScreen(
|
||||
?.let { it.displayName.ifBlank { it.handle } }
|
||||
?: "user $authorId"
|
||||
},
|
||||
// 🔴 Only for a message you did not send. Your own bubble has
|
||||
// no avatar in the kit — you know who you are, and a picture on
|
||||
// both sides halves the width a thread has to say anything in.
|
||||
// 🔴 Only for a message you did not send. Your own bubble
|
||||
// has no avatar in the kit — you know who you are, and a
|
||||
// picture on both sides halves the width a thread has.
|
||||
picture = authorId?.let { avatars[it] },
|
||||
)
|
||||
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
|
||||
if (line.text.isNotBlank()) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Copy") },
|
||||
onClick = { clipboard.setText(AnnotatedString(line.text)); menuOpen = false },
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (line.mine) "Unsend" else "Delete for me") },
|
||||
onClick = { model.deleteMessage(line); menuOpen = false },
|
||||
modifier = Modifier.testTag("delete-message"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -780,6 +780,18 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete one message from this device (delete for me). */
|
||||
fun deleteMessage(line: Session.Line) = viewModelScope.launch {
|
||||
val current = session ?: return@launch
|
||||
val threadId = signedIn()?.openChat?.threadId ?: return@launch
|
||||
withContext(Dispatchers.IO) {
|
||||
current.deleteMessage(threadId, line.at, line.mine, line.text)
|
||||
}
|
||||
update {
|
||||
it.copy(openChat = it.openChat?.copy(lines = current.history(threadId)))
|
||||
}
|
||||
}
|
||||
|
||||
// -- Presence ------------------------------------------------------------
|
||||
|
||||
fun setPresence(status: String) = viewModelScope.launch {
|
||||
|
||||
@@ -131,6 +131,10 @@ class Session(
|
||||
/** Clear a thread. Keeps the session, so the conversation still works. */
|
||||
fun clearHistory(threadId: String) = vault.forgetHistory(threadId)
|
||||
|
||||
/** Delete one message from this device (delete for me). */
|
||||
fun deleteMessage(threadId: String, at: Long, mine: Boolean, text: String) =
|
||||
vault.deleteLine(threadId, at, mine, text)
|
||||
|
||||
/** Send an image. Returns what happened, same as a text send. */
|
||||
fun sendImage(peerUserId: Long, bytes: ByteArray, mime: String, width: Int, height: Int):
|
||||
io.flamenet.client.Conversations.Sent {
|
||||
|
||||
@@ -741,6 +741,25 @@ class Vault(private val storage: Storage, private val key: SecretKey) {
|
||||
*/
|
||||
fun forgetHistory(threadId: String) = storage.delete(historyName(threadId))
|
||||
|
||||
/**
|
||||
* Delete one line from a thread's history (delete for me).
|
||||
*
|
||||
* Keyed by timestamp + side, the same identity the delivery-state
|
||||
* rewriter uses — messages carry no stable id here. Text is included so
|
||||
* two messages sent in the same millisecond can't take each other down.
|
||||
*/
|
||||
fun deleteLine(threadId: String, at: Long, mine: Boolean, text: String) {
|
||||
val existing = history(threadId)
|
||||
val kept = org.json.JSONArray()
|
||||
for (i in 0 until existing.length()) {
|
||||
val m = existing.getJSONObject(i)
|
||||
if (m.optLong("at") == at && m.optBoolean("mine") == mine &&
|
||||
m.optString("text") == text) continue
|
||||
kept.put(m)
|
||||
}
|
||||
replaceHistory(threadId, kept)
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget everything, identity included.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user