feat(tauri-app): Notifications-View mit Badge, Follower-Listen, DID-Navigation

Bindet die neuen AppView-Endpoints an: sechs IPC-Commands
(fetch_notifications, notification_count, mark_notifications_seen,
fetch_followers, fetch_following, fetch_thread) plus profile_get_by_did,
dazu die TS-Gegenstücke.

* NotificationsView: Liste mit Icon/Text je Art, Avatar, Vorschau des
  subject_text, Cursor-Pagination; Klick auf eine Zeile mit Subject öffnet
  den Thread. Beim Öffnen wird mit dem indexed_at der obersten Zeile als
  Wasserzeichen quittiert.
* NavRail: Eintrag mit Unread-Badge, gepollt im vorhandenen 5s-Timer und
  über den bestehenden Teardown-Pfad abgeräumt; der Poll pausiert, solange
  die Liste offen ist.
* ProfileView: Follower- und Following-Zahlen sind jetzt Buttons und öffnen
  die jeweilige Liste inline.

Navigiert wird über die DID, nicht über den Handle: für Actors, die weder
profiles noch posts kennen, liefert die AppView einen abgeschnittenen
Platzhalter im handle-Feld ("did:plc:abcd…"), und ein Klick darauf landete
in einem synthetischen Leerprofil. Die echte DID steht im DTO und wird jetzt
explizit durchgereicht — keine Heuristik auf das Platzhalter-Format.

Dabei aufgefallen und mitgefixt: ProfileView lud nur in onMount, obwohl die
Komponente bei Profil-zu-Profil-Navigation gemountet bleibt — Posts und
Zahlen des vorherigen Nutzers wären unter dem neuen Namen stehengeblieben.
Jetzt ein auf (did, handle) gekeyter Effekt.

Das Thread-Overlay hing im else-Zweig der leeren Timeline und wäre aus dem
Notifications-View unsichtbar gewesen; es ist jetzt ein Snippet, das beide
Views rendern.

Co-Authored-By: Eifelduds <noreply@eifelduds.de>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
This commit is contained in:
tomdebone
2026-09-09 21:37:00 +02:00
co-authored by Eifelduds
parent 8db4704f8d
commit f281914899
12 changed files with 2241 additions and 48 deletions
+132
View File
@@ -381,6 +381,29 @@ async fn profile_get(
.map_err(|e| e.to_string())
}
/// `profile_get_by_did(did)` — resolve a profile by DID rather than
/// by handle (`GET /api/profile?did=…`).
///
/// This is the *only* correct lookup for an actor that came out of a
/// notification or a follower list: the AppView synthesises a
/// placeholder `handle` (a truncated DID, see `short_did_bare`) for
/// actors it has neither a `profiles` row nor an indexed post for, and
/// feeding that placeholder back into the handle lookup matches
/// nothing — `resolve_profile` then answers with a synthetic empty
/// profile. The DID in the notification / actor DTO is real, so we
/// navigate by that instead.
#[tauri::command]
async fn profile_get_by_did(
state: tauri::State<'_, AppState>,
did: String,
) -> Result<appview_client::ProfileResponse, String> {
state
.appview
.fetch_profile_by_did(&did)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn search(
state: tauri::State<'_, AppState>,
@@ -407,6 +430,108 @@ async fn post_get(
.map_err(|e| e.to_string())
}
/// `fetch_thread(uri, viewer_did?)` — the full thread around `uri`:
/// the whole ancestor chain (root first) plus the direct replies
/// (oldest first). `post_get` stays the narrower two-hop shape the
/// timeline's inline thread modal uses; this is what a dedicated
/// thread view wants.
#[tauri::command]
async fn fetch_thread(
state: tauri::State<'_, AppState>,
uri: String,
viewer_did: Option<String>,
) -> Result<appview_client::ThreadFullResponse, String> {
state
.appview
.fetch_thread(&uri, viewer_did.as_deref())
.await
.map_err(|e| e.to_string())
}
/// `fetch_notifications(did, cursor?, limit?)` — one page of the
/// recipient's notifications, newest first. Same limit clamp as
/// `timeline_home` (the server clamps too, but doing it here means a
/// bad `limit` never costs a round trip).
#[tauri::command]
async fn fetch_notifications(
state: tauri::State<'_, AppState>,
did: String,
cursor: Option<String>,
limit: Option<u32>,
) -> Result<appview_client::NotificationsResponse, String> {
let lim = limit.unwrap_or(30).clamp(1, 100);
state
.appview
.fetch_notifications(&did, cursor.as_deref(), lim)
.await
.map_err(|e| e.to_string())
}
/// `notification_count(did)` — unread count for the NavRail badge.
#[tauri::command]
async fn notification_count(
state: tauri::State<'_, AppState>,
did: String,
) -> Result<appview_client::NotificationCountResponse, String> {
state
.appview
.notification_count(&did)
.await
.map_err(|e| e.to_string())
}
/// `mark_notifications_seen(did, seen_at?)` — mark every notification
/// indexed at or before `seen_at` as read. The frontend passes the
/// `indexed_at` of the topmost row it actually rendered, so a
/// notification that lands mid-scroll is never silently swallowed.
/// Omitting `seen_at` marks everything currently unread.
#[tauri::command]
async fn mark_notifications_seen(
state: tauri::State<'_, AppState>,
did: String,
seen_at: Option<String>,
) -> Result<appview_client::NotificationSeenResponse, String> {
state
.appview
.mark_notifications_seen(&did, seen_at.as_deref())
.await
.map_err(|e| e.to_string())
}
/// `fetch_followers(did, cursor?, limit?)` — one page of the actors
/// who follow `did`.
#[tauri::command]
async fn fetch_followers(
state: tauri::State<'_, AppState>,
did: String,
cursor: Option<String>,
limit: Option<u32>,
) -> Result<appview_client::ActorListResponse, String> {
let lim = limit.unwrap_or(30).clamp(1, 100);
state
.appview
.fetch_followers(&did, cursor.as_deref(), lim)
.await
.map_err(|e| e.to_string())
}
/// `fetch_following(did, cursor?, limit?)` — one page of the actors
/// `did` follows.
#[tauri::command]
async fn fetch_following(
state: tauri::State<'_, AppState>,
did: String,
cursor: Option<String>,
limit: Option<u32>,
) -> Result<appview_client::ActorListResponse, String> {
let lim = limit.unwrap_or(30).clamp(1, 100);
state
.appview
.fetch_following(&did, cursor.as_deref(), lim)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn status_pds(state: tauri::State<'_, AppState>) -> Result<serde_json::Value, String> {
let sess = state.store.load();
@@ -791,8 +916,15 @@ pub fn run() {
resolve_handle,
timeline_home,
profile_get,
profile_get_by_did,
search,
post_get,
fetch_thread,
fetch_notifications,
notification_count,
mark_notifications_seen,
fetch_followers,
fetch_following,
like_post,
unlike_post,
repost_post,