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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user