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.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.clickable
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -1263,6 +1269,7 @@ private fun PresenceDot(status: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** One conversation. */
|
/** One conversation. */
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ColumnScope.ChatScreen(
|
fun ColumnScope.ChatScreen(
|
||||||
model: MessengerViewModel,
|
model: MessengerViewModel,
|
||||||
@@ -1377,6 +1384,12 @@ fun ColumnScope.ChatScreen(
|
|||||||
chat.group == null -> chat.peerId
|
chat.group == null -> chat.peerId
|
||||||
else -> line.author.takeIf { it != 0L }
|
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(
|
Bubble(
|
||||||
line = line,
|
line = line,
|
||||||
model = model,
|
model = model,
|
||||||
@@ -1387,11 +1400,25 @@ fun ColumnScope.ChatScreen(
|
|||||||
?.let { it.displayName.ifBlank { it.handle } }
|
?.let { it.displayName.ifBlank { it.handle } }
|
||||||
?: "user $authorId"
|
?: "user $authorId"
|
||||||
},
|
},
|
||||||
// 🔴 Only for a message you did not send. Your own bubble has
|
// 🔴 Only for a message you did not send. Your own bubble
|
||||||
// no avatar in the kit — you know who you are, and a picture on
|
// has no avatar in the kit — you know who you are, and a
|
||||||
// both sides halves the width a thread has to say anything in.
|
// picture on both sides halves the width a thread has.
|
||||||
picture = authorId?.let { avatars[it] },
|
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 ------------------------------------------------------------
|
// -- Presence ------------------------------------------------------------
|
||||||
|
|
||||||
fun setPresence(status: String) = viewModelScope.launch {
|
fun setPresence(status: String) = viewModelScope.launch {
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ class Session(
|
|||||||
/** Clear a thread. Keeps the session, so the conversation still works. */
|
/** Clear a thread. Keeps the session, so the conversation still works. */
|
||||||
fun clearHistory(threadId: String) = vault.forgetHistory(threadId)
|
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. */
|
/** Send an image. Returns what happened, same as a text send. */
|
||||||
fun sendImage(peerUserId: Long, bytes: ByteArray, mime: String, width: Int, height: Int):
|
fun sendImage(peerUserId: Long, bytes: ByteArray, mime: String, width: Int, height: Int):
|
||||||
io.flamenet.client.Conversations.Sent {
|
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))
|
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.
|
* Forget everything, identity included.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user