feat(contacts): add ignored and blocked contact lists

- New DB tables: ignored_contacts, blocked_contacts (keyed by pubkey)
- Ignored contacts: cached but excluded from pending/auto-add
- Blocked contacts: ignored + messages hidden from chat (stored in DB)
- Backend: filter in _on_new_contact, _on_channel_message, _on_dm_received
- API: /contacts/<pk>/ignore, /contacts/<pk>/block toggle endpoints
- API: filter blocked from /api/messages and /dm/conversations
- Frontend: Ignore/Block buttons on pending cards, existing cards, chat messages
- Frontend: source filter dropdown with Ignored/Blocked options
- Frontend: status icons (eye-slash, slash-circle) on contact cards
- Frontend: real-time blocked message filtering via socketio
- Name→pubkey mapping for chat window block/ignore buttons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-03-09 21:10:21 +01:00
parent b709cc7b14
commit 2a3a48ed5f
7 changed files with 459 additions and 8 deletions
+68
View File
@@ -186,6 +186,74 @@ class Database:
)
return cursor.rowcount > 0
# ================================================================
# Ignored / Blocked Contacts
# ================================================================
def set_contact_ignored(self, public_key: str, ignored: bool) -> bool:
pk = public_key.lower()
with self._connect() as conn:
if ignored:
conn.execute(
"INSERT OR IGNORE INTO ignored_contacts (public_key) VALUES (?)", (pk,))
# Remove from blocked if setting ignored
conn.execute(
"DELETE FROM blocked_contacts WHERE public_key = ?", (pk,))
else:
conn.execute(
"DELETE FROM ignored_contacts WHERE public_key = ?", (pk,))
return True
def set_contact_blocked(self, public_key: str, blocked: bool) -> bool:
pk = public_key.lower()
with self._connect() as conn:
if blocked:
conn.execute(
"INSERT OR IGNORE INTO blocked_contacts (public_key) VALUES (?)", (pk,))
# Remove from ignored if setting blocked
conn.execute(
"DELETE FROM ignored_contacts WHERE public_key = ?", (pk,))
else:
conn.execute(
"DELETE FROM blocked_contacts WHERE public_key = ?", (pk,))
return True
def is_contact_ignored(self, public_key: str) -> bool:
with self._connect() as conn:
row = conn.execute(
"SELECT 1 FROM ignored_contacts WHERE public_key = ?",
(public_key.lower(),)
).fetchone()
return row is not None
def is_contact_blocked(self, public_key: str) -> bool:
with self._connect() as conn:
row = conn.execute(
"SELECT 1 FROM blocked_contacts WHERE public_key = ?",
(public_key.lower(),)
).fetchone()
return row is not None
def get_ignored_keys(self) -> set:
with self._connect() as conn:
rows = conn.execute("SELECT public_key FROM ignored_contacts").fetchall()
return {r['public_key'] for r in rows}
def get_blocked_keys(self) -> set:
with self._connect() as conn:
rows = conn.execute("SELECT public_key FROM blocked_contacts").fetchall()
return {r['public_key'] for r in rows}
def get_blocked_contact_names(self) -> set:
"""Return current names of blocked contacts (for channel msg filtering)."""
with self._connect() as conn:
rows = conn.execute(
"""SELECT c.name FROM blocked_contacts bc
JOIN contacts c ON bc.public_key = c.public_key
WHERE c.name != ''"""
).fetchall()
return {r['name'] for r in rows}
# ================================================================
# Channels
# ================================================================