feat: Nachrichten als gelöscht markieren

- DB: deleted_on TEXT Spalte in messages (Migration)
- Backend: ToggleMessageDelete(), includeDeleted-Parameter in allen History-Queries
- Neuer Endpoint: PUT /api/messages/{id}/delete
- Frontend: Trash-Button zwischen Reply- und Mark-Button
- Gelöschte Nachrichten: durchgestrichen + roter Rand (oder versteckt je nach Einstellung)
- Einstellungen: Toggle-Switch 'Gelöschte Nachrichten anzeigen'
- KI-Payload: gelöschte Nachrichten werden nicht mitgeschickt
This commit is contained in:
Meleki committed 2026-03-03 11:45:52 +01:00
1 parent 493dfcda1c
commit a14a991a0f
11 files changed
+248 -39

No files matched your search

+46 -15
View File
@@ -146,6 +146,7 @@ public class DatabaseService
TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "agents", "use_memory", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "messages", "deleted_on", "TEXT");
TryAddColumn(conn, "agents", "use_web_search", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "agents", "web_search_version", "TEXT DEFAULT ''");
@@ -560,57 +561,62 @@ public class DatabaseService
);
}
public async Task<int> GetActiveMessageCount(string sessionId)
public async Task<int> GetActiveMessageCount(string sessionId, bool includeDeleted = false)
{
using var conn = CreateConnection();
return await conn.ExecuteScalarAsync<int>(@"
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
return await conn.ExecuteScalarAsync<int>($@"
SELECT COUNT(*) FROM messages m
JOIN chains c ON m.chain_id = c.chain_id
WHERE c.archive_id IS NULL AND c.session_id = @sessionId",
WHERE c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}",
new { sessionId });
}
public async Task<List<MessageRecord>> GetLatestActiveMessages(string sessionId, int limit)
public async Task<List<MessageRecord>> GetLatestActiveMessages(string sessionId, int limit, bool includeDeleted = false)
{
using var conn = CreateConnection();
var messages = await conn.QueryAsync<MessageRecord>(@"
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
var messages = await conn.QueryAsync<MessageRecord>($@"
SELECT m.* FROM messages m
JOIN chains c ON m.chain_id = c.chain_id
WHERE c.archive_id IS NULL AND c.session_id = @sessionId
WHERE c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
ORDER BY m.id DESC LIMIT @limit",
new { sessionId, limit });
return messages.Reverse().ToList();
}
public async Task<int> GetActiveMessageCountBeforeId(string sessionId, long beforeId)
public async Task<int> GetActiveMessageCountBeforeId(string sessionId, long beforeId, bool includeDeleted = false)
{
using var conn = CreateConnection();
return await conn.ExecuteScalarAsync<int>(@"
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
return await conn.ExecuteScalarAsync<int>($@"
SELECT COUNT(*) FROM messages m
JOIN chains c ON m.chain_id = c.chain_id
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId",
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}",
new { sessionId, beforeId });
}
public async Task<List<MessageRecord>> GetActiveMessagesBeforeId(string sessionId, long beforeId, int limit)
public async Task<List<MessageRecord>> GetActiveMessagesBeforeId(string sessionId, long beforeId, int limit, bool includeDeleted = false)
{
using var conn = CreateConnection();
var messages = await conn.QueryAsync<MessageRecord>(@"
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
var messages = await conn.QueryAsync<MessageRecord>($@"
SELECT m.* FROM messages m
JOIN chains c ON m.chain_id = c.chain_id
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
ORDER BY m.id DESC LIMIT @limit",
new { sessionId, beforeId, limit });
return messages.Reverse().ToList();
}
public async Task<IEnumerable<MessageRecord>> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200)
public async Task<IEnumerable<MessageRecord>> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200, bool includeDeleted = false)
{
using var conn = CreateConnection();
return await conn.QueryAsync<MessageRecord>(@"
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
return await conn.QueryAsync<MessageRecord>($@"
SELECT m.* FROM messages m
JOIN chains c ON m.chain_id = c.chain_id
WHERE m.id > @afterId AND c.archive_id IS NULL AND c.session_id = @sessionId
WHERE m.id > @afterId AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
ORDER BY m.id LIMIT @limit",
new { sessionId, afterId, limit });
}
@@ -628,6 +634,31 @@ public class DatabaseService
new { messageId });
}
// ── Message Deletion ──
public async Task<string> ToggleMessageDelete(long messageId)
{
using var conn = CreateConnection();
var current = await conn.ExecuteScalarAsync<string>(
"SELECT deleted_on FROM messages WHERE id = @messageId", new { messageId });
string newValue;
if (current == null)
{
newValue = DateTime.UtcNow.ToString("o");
await conn.ExecuteAsync(
"UPDATE messages SET deleted_on = @newValue WHERE id = @messageId",
new { messageId, newValue });
}
else
{
newValue = null;
await conn.ExecuteAsync(
"UPDATE messages SET deleted_on = NULL WHERE id = @messageId",
new { messageId });
}
return newValue;
}
public async Task<IEnumerable<MessageRecord>> GetMarkedMessages(string sessionId)
{
using var conn = CreateConnection();