# Federation Management Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Let users see and manage their incoming remote follow requests, accepted remote followers, and remote following — surfaced via a shared `` component used in settings and the profile page. **Architecture:** Six new methods added to `FederationActionPort` (domain port), delegated to existing `ActivityPubService` logic. Six application-layer use cases wrap the port — no logic in handlers. Four frontend components (`PendingRequests`, `RemoteFollowers`, `RemoteFollowing`, `FederationPanel`) share one data-fetching pattern. **Tech Stack:** Rust / axum / async-trait / domain ports (backend), Next.js 15 / TypeScript / Zod / shadcn Tabs (frontend). --- ## Files | Action | Path | Purpose | |--------|------|---------| | Modify | `crates/domain/src/ports.rs` | Add 6 methods to `FederationActionPort` | | Modify | `crates/domain/src/testing.rs` | Add no-op impls on `TestStore` | | Modify | `crates/adapters/activitypub-base/src/service.rs` | Implement the 6 new port methods | | Create | `crates/application/src/use_cases/federation_management.rs` | 6 use case functions | | Modify | `crates/application/src/use_cases/mod.rs` | Expose new module | | Create | `crates/presentation/src/handlers/federation_management.rs` | 6 HTTP handlers | | Modify | `crates/presentation/src/handlers/mod.rs` | Expose new handler module | | Modify | `crates/presentation/src/routes.rs` | Register 6 new routes | | Modify | `thoughts-frontend/lib/api.ts` | 6 new API functions + schema | | Create | `thoughts-frontend/components/federation/pending-requests.tsx` | Accept/reject pending follows | | Create | `thoughts-frontend/components/federation/remote-followers.tsx` | View/remove accepted followers | | Create | `thoughts-frontend/components/federation/remote-following.tsx` | View/unfollow remote following | | Create | `thoughts-frontend/components/federation/federation-panel.tsx` | Tabbed wrapper | | Create | `thoughts-frontend/app/settings/federation/page.tsx` | Settings page | | Modify | `thoughts-frontend/app/settings/layout.tsx` | Add "Federation" nav item | | Modify | `thoughts-frontend/app/users/[username]/page.tsx` | Add "Federation" tab on own profile | --- ## Task 1: Extend FederationActionPort with management methods **Files:** - Modify: `crates/domain/src/ports.rs` - Modify: `crates/domain/src/testing.rs` - [ ] **Step 1: Add 6 methods to `FederationActionPort` in `crates/domain/src/ports.rs`** Find the `FederationActionPort` trait (around line 223). Add these six methods after `unfollow_remote`: ```rust async fn get_pending_followers( &self, user_id: &UserId, ) -> Result, DomainError>; async fn accept_follow_request( &self, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError>; async fn reject_follow_request( &self, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError>; async fn get_remote_followers( &self, user_id: &UserId, ) -> Result, DomainError>; async fn remove_remote_follower( &self, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError>; async fn get_remote_following( &self, user_id: &UserId, ) -> Result, DomainError>; ``` `RemoteActor` here is `crate::models::remote_actor::RemoteActor` — already in scope via the existing import. - [ ] **Step 2: Add no-op impls to `TestStore` in `crates/domain/src/testing.rs`** Find `impl FederationActionPort for TestStore` (around line 538). Add after `unfollow_remote`: ```rust async fn get_pending_followers( &self, _user_id: &UserId, ) -> Result, DomainError> { Ok(vec![]) } async fn accept_follow_request( &self, _user_id: &UserId, _actor_url: &str, ) -> Result<(), DomainError> { Ok(()) } async fn reject_follow_request( &self, _user_id: &UserId, _actor_url: &str, ) -> Result<(), DomainError> { Ok(()) } async fn get_remote_followers( &self, _user_id: &UserId, ) -> Result, DomainError> { Ok(vec![]) } async fn remove_remote_follower( &self, _user_id: &UserId, _actor_url: &str, ) -> Result<(), DomainError> { Ok(()) } async fn get_remote_following( &self, _user_id: &UserId, ) -> Result, DomainError> { Ok(vec![]) } ``` - [ ] **Step 3: Verify compilation** ```bash cd /mnt/drive/dev/thoughts && cargo build -p domain 2>&1 | grep "^error" ``` Expected: no errors. - [ ] **Step 4: Commit** ```bash git add crates/domain/src/ports.rs crates/domain/src/testing.rs git commit -m "feat(domain): add federation management methods to FederationActionPort" ``` --- ## Task 2: Implement new port methods in ActivityPubService **Files:** - Modify: `crates/adapters/activitypub-base/src/service.rs` The existing private `ActivityPubService` methods (`get_pending_followers`, `accept_follower`, etc.) take `uuid::Uuid` and return adapter-level `RemoteActor` (from `crate::repository::RemoteActor`). The port returns domain `RemoteActor`. Add a private mapping helper and implement the six port methods. - [ ] **Step 1: Add a private mapping helper to `service.rs`** Add this private function anywhere in the `impl ActivityPubService` block (not in the `impl FederationActionPort` block): ```rust fn adapter_actor_to_domain( a: crate::repository::RemoteActor, ) -> domain::models::remote_actor::RemoteActor { domain::models::remote_actor::RemoteActor { url: a.url, handle: a.handle, display_name: a.display_name, inbox_url: a.inbox_url, shared_inbox_url: a.shared_inbox_url, avatar_url: a.avatar_url, outbox_url: a.outbox_url, public_key: String::new(), last_fetched_at: chrono::Utc::now(), bio: None, banner_url: None, also_known_as: None, followers_url: None, following_url: None, attachment: vec![], } } ``` - [ ] **Step 2: Implement the 6 new methods in the `impl domain::ports::FederationActionPort for ActivityPubService` block** Add after the existing `unfollow_remote` impl: ```rust async fn get_pending_followers( &self, user_id: &domain::value_objects::UserId, ) -> Result, domain::errors::DomainError> { self.get_pending_followers(user_id.as_uuid()) .await .map(|v| v.into_iter().map(Self::adapter_actor_to_domain).collect()) .map_err(|e| domain::errors::DomainError::ExternalService(e.to_string())) } async fn accept_follow_request( &self, user_id: &domain::value_objects::UserId, actor_url: &str, ) -> Result<(), domain::errors::DomainError> { self.accept_follower(user_id.as_uuid(), actor_url) .await .map_err(|e| domain::errors::DomainError::ExternalService(e.to_string())) } async fn reject_follow_request( &self, user_id: &domain::value_objects::UserId, actor_url: &str, ) -> Result<(), domain::errors::DomainError> { self.reject_follower(user_id.as_uuid(), actor_url) .await .map_err(|e| domain::errors::DomainError::ExternalService(e.to_string())) } async fn get_remote_followers( &self, user_id: &domain::value_objects::UserId, ) -> Result, domain::errors::DomainError> { self.get_accepted_followers(user_id.as_uuid()) .await .map(|v| v.into_iter().map(Self::adapter_actor_to_domain).collect()) .map_err(|e| domain::errors::DomainError::ExternalService(e.to_string())) } async fn remove_remote_follower( &self, user_id: &domain::value_objects::UserId, actor_url: &str, ) -> Result<(), domain::errors::DomainError> { self.remove_follower(user_id.as_uuid(), actor_url) .await .map_err(|e| domain::errors::DomainError::ExternalService(e.to_string())) } async fn get_remote_following( &self, user_id: &domain::value_objects::UserId, ) -> Result, domain::errors::DomainError> { self.get_following(user_id.as_uuid()) .await .map(|v| v.into_iter().map(Self::adapter_actor_to_domain).collect()) .map_err(|e| domain::errors::DomainError::ExternalService(e.to_string())) } ``` - [ ] **Step 3: Verify compilation** ```bash cd /mnt/drive/dev/thoughts && cargo build -p activitypub-base 2>&1 | grep "^error" ``` Expected: no errors. - [ ] **Step 4: Commit** ```bash git add crates/adapters/activitypub-base/src/service.rs git commit -m "feat(activitypub-base): implement federation management port methods" ``` --- ## Task 3: Application use cases **Files:** - Create: `crates/application/src/use_cases/federation_management.rs` - Modify: `crates/application/src/use_cases/mod.rs` - [ ] **Step 1: Write failing tests first** Create `crates/application/src/use_cases/federation_management.rs` with just the tests (no implementations yet): ```rust use domain::{ errors::DomainError, models::remote_actor::RemoteActor, ports::FederationActionPort, value_objects::UserId, }; pub async fn list_pending_requests( federation: &dyn FederationActionPort, user_id: &UserId, ) -> Result, DomainError> { todo!() } pub async fn accept_follow_request( federation: &dyn FederationActionPort, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError> { todo!() } pub async fn reject_follow_request( federation: &dyn FederationActionPort, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError> { todo!() } pub async fn list_remote_followers( federation: &dyn FederationActionPort, user_id: &UserId, ) -> Result, DomainError> { todo!() } pub async fn remove_remote_follower( federation: &dyn FederationActionPort, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError> { todo!() } pub async fn list_remote_following( federation: &dyn FederationActionPort, user_id: &UserId, ) -> Result, DomainError> { todo!() } #[cfg(test)] mod tests { use super::*; use domain::testing::TestStore; #[tokio::test] async fn list_pending_returns_empty_by_default() { let store = TestStore::default(); let uid = UserId::new(); let result = list_pending_requests(&store, &uid).await.unwrap(); assert!(result.is_empty()); } #[tokio::test] async fn accept_follow_request_returns_ok() { let store = TestStore::default(); let uid = UserId::new(); accept_follow_request(&store, &uid, "https://mastodon.social/users/alice") .await .unwrap(); } #[tokio::test] async fn reject_follow_request_returns_ok() { let store = TestStore::default(); let uid = UserId::new(); reject_follow_request(&store, &uid, "https://mastodon.social/users/alice") .await .unwrap(); } #[tokio::test] async fn list_remote_followers_returns_empty_by_default() { let store = TestStore::default(); let uid = UserId::new(); let result = list_remote_followers(&store, &uid).await.unwrap(); assert!(result.is_empty()); } #[tokio::test] async fn remove_remote_follower_returns_ok() { let store = TestStore::default(); let uid = UserId::new(); remove_remote_follower(&store, &uid, "https://mastodon.social/users/alice") .await .unwrap(); } #[tokio::test] async fn list_remote_following_returns_empty_by_default() { let store = TestStore::default(); let uid = UserId::new(); let result = list_remote_following(&store, &uid).await.unwrap(); assert!(result.is_empty()); } } ``` - [ ] **Step 2: Run tests to confirm they fail (panic on todo!())** ```bash cd /mnt/drive/dev/thoughts && cargo test -p application federation_management 2>&1 | tail -10 ``` Expected: tests fail with `not yet implemented`. - [ ] **Step 3: Implement the use case functions (replace `todo!()` bodies)** ```rust pub async fn list_pending_requests( federation: &dyn FederationActionPort, user_id: &UserId, ) -> Result, DomainError> { federation.get_pending_followers(user_id).await } pub async fn accept_follow_request( federation: &dyn FederationActionPort, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError> { federation.accept_follow_request(user_id, actor_url).await } pub async fn reject_follow_request( federation: &dyn FederationActionPort, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError> { federation.reject_follow_request(user_id, actor_url).await } pub async fn list_remote_followers( federation: &dyn FederationActionPort, user_id: &UserId, ) -> Result, DomainError> { federation.get_remote_followers(user_id).await } pub async fn remove_remote_follower( federation: &dyn FederationActionPort, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError> { federation.remove_remote_follower(user_id, actor_url).await } pub async fn list_remote_following( federation: &dyn FederationActionPort, user_id: &UserId, ) -> Result, DomainError> { federation.get_remote_following(user_id).await } ``` - [ ] **Step 4: Expose the module in `crates/application/src/use_cases/mod.rs`** Add: ```rust pub mod federation_management; ``` - [ ] **Step 5: Run tests — all 6 should pass** ```bash cd /mnt/drive/dev/thoughts && cargo test -p application federation_management 2>&1 | tail -5 ``` Expected: `6 passed`. - [ ] **Step 6: Commit** ```bash git add crates/application/src/use_cases/federation_management.rs \ crates/application/src/use_cases/mod.rs git commit -m "feat(application): federation management use cases" ``` --- ## Task 4: HTTP handlers and routes **Files:** - Create: `crates/presentation/src/handlers/federation_management.rs` - Modify: `crates/presentation/src/handlers/mod.rs` - Modify: `crates/presentation/src/routes.rs` Response shape: a slim subset of `RemoteActorResponse` is enough. Reuse it — it already has `handle`, `display_name`, `avatar_url`, `url`. - [ ] **Step 1: Create `crates/presentation/src/handlers/federation_management.rs`** ```rust use crate::{errors::ApiError, extractors::AuthUser, state::AppState}; use api_types::responses::RemoteActorResponse; use application::use_cases::federation_management::{ accept_follow_request, list_pending_requests, list_remote_followers, list_remote_following, reject_follow_request, remove_remote_follower, }; use axum::{extract::State, http::StatusCode, Json}; use serde::Deserialize; #[derive(Deserialize)] pub struct ActorUrlBody { pub actor_url: String, } #[derive(Deserialize)] pub struct HandleBody { pub handle: String, } fn to_response(a: domain::models::remote_actor::RemoteActor) -> RemoteActorResponse { RemoteActorResponse { handle: a.handle, display_name: a.display_name, avatar_url: a.avatar_url, url: a.url, bio: a.bio, banner_url: a.banner_url, also_known_as: a.also_known_as, outbox_url: a.outbox_url, followers_url: a.followers_url, following_url: a.following_url, attachment: vec![], } } pub async fn get_pending_requests( State(s): State, AuthUser(uid): AuthUser, ) -> Result>, ApiError> { let actors = list_pending_requests(&*s.federation, &uid).await?; Ok(Json(actors.into_iter().map(to_response).collect())) } pub async fn post_accept_request( State(s): State, AuthUser(uid): AuthUser, Json(body): Json, ) -> Result { accept_follow_request(&*s.federation, &uid, &body.actor_url).await?; Ok(StatusCode::NO_CONTENT) } pub async fn delete_follower( State(s): State, AuthUser(uid): AuthUser, Json(body): Json, ) -> Result { reject_follow_request(&*s.federation, &uid, &body.actor_url).await?; Ok(StatusCode::NO_CONTENT) } pub async fn get_remote_followers( State(s): State, AuthUser(uid): AuthUser, ) -> Result>, ApiError> { let actors = list_remote_followers(&*s.federation, &uid).await?; Ok(Json(actors.into_iter().map(to_response).collect())) } pub async fn get_remote_following( State(s): State, AuthUser(uid): AuthUser, ) -> Result>, ApiError> { let actors = list_remote_following(&*s.federation, &uid).await?; Ok(Json(actors.into_iter().map(to_response).collect())) } pub async fn delete_following( State(s): State, AuthUser(uid): AuthUser, Json(body): Json, ) -> Result { application::use_cases::social::unfollow_actor( &*s.follows, &*s.users, &*s.federation, &*s.events, &uid, &body.handle, ) .await?; Ok(StatusCode::NO_CONTENT) } ``` - [ ] **Step 2: Add module to `crates/presentation/src/handlers/mod.rs`** ```rust pub mod federation_management; ``` - [ ] **Step 3: Register routes in `crates/presentation/src/routes.rs`** Add after the existing `/federation/actors/...` routes: ```rust .route( "/federation/me/followers/pending", get(federation_management::get_pending_requests), ) .route( "/federation/me/followers/accept", post(federation_management::post_accept_request), ) .route( "/federation/me/followers", get(federation_management::get_remote_followers) .delete(federation_management::delete_follower), ) .route( "/federation/me/following", get(federation_management::get_remote_following) .delete(federation_management::delete_following), ) ``` - [ ] **Step 4: Verify compilation** ```bash cd /mnt/drive/dev/thoughts && cargo build -p presentation 2>&1 | grep "^error" | head -10 ``` Expected: no errors. - [ ] **Step 5: Run all unit tests** ```bash cd /mnt/drive/dev/thoughts && cargo test -p domain -p application 2>&1 | tail -5 ``` Expected: all pass. - [ ] **Step 6: Commit** ```bash git add crates/presentation/src/handlers/federation_management.rs \ crates/presentation/src/handlers/mod.rs \ crates/presentation/src/routes.rs git commit -m "feat(presentation): federation management endpoints" ``` --- ## Task 5: Frontend API client **Files:** - Modify: `thoughts-frontend/lib/api.ts` - [ ] **Step 1: Add schema and API functions to `thoughts-frontend/lib/api.ts`** The `RemoteActorSchema` and `ActorConnectionSchema` already exist. Add a leaner `FederationActorSchema` for the management responses (same shape as `RemoteActorSchema` — reuse it): After the existing `lookupRemoteActor` function, add: ```typescript // Federation management export const getPendingFollowRequests = (token: string) => apiFetch( "/federation/me/followers/pending", {}, z.array(RemoteActorSchema), token ); export const acceptFollowRequest = (actorUrl: string, token: string) => apiFetch( "/federation/me/followers/accept", { method: "POST", body: JSON.stringify({ actor_url: actorUrl }) }, z.null(), token ); export const rejectFollowRequest = (actorUrl: string, token: string) => apiFetch( "/federation/me/followers", { method: "DELETE", body: JSON.stringify({ actor_url: actorUrl }) }, z.null(), token ); export const getRemoteFollowers = (token: string) => apiFetch( "/federation/me/followers", {}, z.array(RemoteActorSchema), token ); export const getRemoteFollowing = (token: string) => apiFetch( "/federation/me/following", {}, z.array(RemoteActorSchema), token ); export const unfollowRemoteActor = (handle: string, token: string) => apiFetch( "/federation/me/following", { method: "DELETE", body: JSON.stringify({ handle }) }, z.null(), token ); ``` - [ ] **Step 2: Type check** ```bash cd /mnt/drive/dev/thoughts/thoughts-frontend && npx tsc --noEmit 2>&1 | grep "error TS" | head -10 ``` Expected: no errors. - [ ] **Step 3: Commit** ```bash git add thoughts-frontend/lib/api.ts git commit -m "feat(frontend): federation management API client functions" ``` --- ## Task 6: PendingRequests component **Files:** - Create: `thoughts-frontend/components/federation/pending-requests.tsx` - [ ] **Step 1: Create the component** ```tsx "use client"; import { useEffect, useState } from "react"; import { getPendingFollowRequests, acceptFollowRequest, rejectFollowRequest, type RemoteActor, } from "@/lib/api"; import { useAuth } from "@/hooks/use-auth"; import { UserAvatar } from "@/components/user-avatar"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; interface Props { compact?: boolean; } export function PendingRequests({ compact = false }: Props) { const { token } = useAuth(); const [requests, setRequests] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { if (!token) return; getPendingFollowRequests(token) .then(setRequests) .catch(() => toast.error("Failed to load follow requests")) .finally(() => setLoading(false)); }, [token]); const accept = async (actorUrl: string) => { if (!token) return; setRequests((prev) => prev.filter((r) => r.url !== actorUrl)); await acceptFollowRequest(actorUrl, token).catch(() => { toast.error("Failed to accept follow request"); }); }; const reject = async (actorUrl: string) => { if (!token) return; setRequests((prev) => prev.filter((r) => r.url !== actorUrl)); await rejectFollowRequest(actorUrl, token).catch(() => { toast.error("Failed to reject follow request"); }); }; if (loading) return

Loading…

; if (requests.length === 0) return

No pending requests.

; return (
    {requests.map((actor) => (
  • {actor.displayName || actor.handle}

    {actor.handle}

  • ))}
); } ``` - [ ] **Step 2: Type check** ```bash cd /mnt/drive/dev/thoughts/thoughts-frontend && npx tsc --noEmit 2>&1 | grep "error TS" | head -10 ``` Expected: no errors. - [ ] **Step 3: Commit** ```bash git add thoughts-frontend/components/federation/pending-requests.tsx git commit -m "feat(frontend): PendingRequests component" ``` --- ## Task 7: RemoteFollowers component **Files:** - Create: `thoughts-frontend/components/federation/remote-followers.tsx` - [ ] **Step 1: Create the component** ```tsx "use client"; import { useEffect, useState } from "react"; import { getRemoteFollowers, rejectFollowRequest, type RemoteActor } from "@/lib/api"; import { useAuth } from "@/hooks/use-auth"; import { UserAvatar } from "@/components/user-avatar"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; export function RemoteFollowers() { const { token } = useAuth(); const [followers, setFollowers] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { if (!token) return; getRemoteFollowers(token) .then(setFollowers) .catch(() => toast.error("Failed to load followers")) .finally(() => setLoading(false)); }, [token]); const remove = async (actorUrl: string) => { if (!token) return; setFollowers((prev) => prev.filter((f) => f.url !== actorUrl)); await rejectFollowRequest(actorUrl, token).catch(() => { toast.error("Failed to remove follower"); }); }; if (loading) return

Loading…

; if (followers.length === 0) return

No remote followers yet.

; return (
    {followers.map((actor) => (
  • {actor.displayName || actor.handle}

    {actor.handle}

  • ))}
); } ``` - [ ] **Step 2: Type check** ```bash cd /mnt/drive/dev/thoughts/thoughts-frontend && npx tsc --noEmit 2>&1 | grep "error TS" | head -10 ``` - [ ] **Step 3: Commit** ```bash git add thoughts-frontend/components/federation/remote-followers.tsx git commit -m "feat(frontend): RemoteFollowers component" ``` --- ## Task 8: RemoteFollowing component **Files:** - Create: `thoughts-frontend/components/federation/remote-following.tsx` - [ ] **Step 1: Create the component** ```tsx "use client"; import { useEffect, useState } from "react"; import { getRemoteFollowing, unfollowRemoteActor, type RemoteActor } from "@/lib/api"; import { useAuth } from "@/hooks/use-auth"; import { UserAvatar } from "@/components/user-avatar"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; export function RemoteFollowing() { const { token } = useAuth(); const [following, setFollowing] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { if (!token) return; getRemoteFollowing(token) .then(setFollowing) .catch(() => toast.error("Failed to load following")) .finally(() => setLoading(false)); }, [token]); const unfollow = async (handle: string) => { if (!token) return; setFollowing((prev) => prev.filter((f) => f.handle !== handle)); await unfollowRemoteActor(handle, token).catch(() => { toast.error("Failed to unfollow"); }); }; if (loading) return

Loading…

; if (following.length === 0) return

Not following anyone remotely yet.

; return (
    {following.map((actor) => (
  • {actor.displayName || actor.handle}

    {actor.handle}

  • ))}
); } ``` - [ ] **Step 2: Type check** ```bash cd /mnt/drive/dev/thoughts/thoughts-frontend && npx tsc --noEmit 2>&1 | grep "error TS" | head -10 ``` - [ ] **Step 3: Commit** ```bash git add thoughts-frontend/components/federation/remote-following.tsx git commit -m "feat(frontend): RemoteFollowing component" ``` --- ## Task 9: FederationPanel wrapper **Files:** - Create: `thoughts-frontend/components/federation/federation-panel.tsx` - [ ] **Step 1: Create the component** ```tsx "use client"; import { useEffect, useState } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { PendingRequests } from "./pending-requests"; import { RemoteFollowers } from "./remote-followers"; import { RemoteFollowing } from "./remote-following"; import { getPendingFollowRequests } from "@/lib/api"; import { useAuth } from "@/hooks/use-auth"; export function FederationPanel() { const { token } = useAuth(); const [pendingCount, setPendingCount] = useState(0); useEffect(() => { if (!token) return; getPendingFollowRequests(token) .then((r) => setPendingCount(r.length)) .catch(() => {}); }, [token]); return ( Requests {pendingCount > 0 && ( {pendingCount} )} Followers Following ); } ``` - [ ] **Step 2: Type check** ```bash cd /mnt/drive/dev/thoughts/thoughts-frontend && npx tsc --noEmit 2>&1 | grep "error TS" | head -10 ``` - [ ] **Step 3: Commit** ```bash git add thoughts-frontend/components/federation/federation-panel.tsx git commit -m "feat(frontend): FederationPanel tabbed wrapper" ``` --- ## Task 10: Settings page **Files:** - Create: `thoughts-frontend/app/settings/federation/page.tsx` - Modify: `thoughts-frontend/app/settings/layout.tsx` - [ ] **Step 1: Create `thoughts-frontend/app/settings/federation/page.tsx`** ```tsx import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { FederationPanel } from "@/components/federation/federation-panel"; export default async function FederationSettingsPage() { const token = (await cookies()).get("auth_token")?.value; if (!token) { redirect("/login"); } return (

Federation

Manage remote follow requests, followers, and accounts you follow on other instances.

); } ``` - [ ] **Step 2: Add "Federation" to the settings nav in `thoughts-frontend/app/settings/layout.tsx`** Find `sidebarNavItems` and add: ```tsx const sidebarNavItems = [ { title: "Profile", href: "/settings/profile", }, { title: "API Keys", href: "/settings/api-keys", }, { title: "Federation", href: "/settings/federation", }, ]; ``` - [ ] **Step 3: Type check** ```bash cd /mnt/drive/dev/thoughts/thoughts-frontend && npx tsc --noEmit 2>&1 | grep "error TS" | head -10 ``` - [ ] **Step 4: Commit** ```bash git add thoughts-frontend/app/settings/federation/page.tsx \ thoughts-frontend/app/settings/layout.tsx git commit -m "feat(frontend): federation settings page" ``` --- ## Task 11: Profile page — Federation tab **Files:** - Modify: `thoughts-frontend/app/users/[username]/page.tsx` The profile page uses a tab pattern for Thoughts / Followers / Following. Add a "Federation" tab visible only when `isOwnProfile`. - [ ] **Step 1: Import FederationPanel** At the top of `thoughts-frontend/app/users/[username]/page.tsx`, add: ```tsx import { FederationPanel } from "@/components/federation/federation-panel"; ``` - [ ] **Step 2: Add the Federation tab** Find the section that renders the profile tabs (the `Tabs` component with Thoughts/Followers/Following). Add a "Federation" tab that only renders when `isOwnProfile`. The exact location depends on how the tabs are structured — look for `` and `` blocks and add alongside them: Inside ``: ```tsx {isOwnProfile && ( Federation )} ``` After the last ``: ```tsx {isOwnProfile && ( )} ``` - [ ] **Step 3: Type check** ```bash cd /mnt/drive/dev/thoughts/thoughts-frontend && npx tsc --noEmit 2>&1 | grep "error TS" | head -10 ``` - [ ] **Step 4: Final build check** ```bash cd /mnt/drive/dev/thoughts && cargo build 2>&1 | grep "^error" ``` - [ ] **Step 5: Commit** ```bash git add thoughts-frontend/app/users/\[username\]/page.tsx git commit -m "feat(frontend): federation tab on own profile" ``` --- ## Notes - **Notifications page**: no notifications page exists yet. `` can be added there once that page is built. - **`delete_follower` vs `reject_follow_request`**: both pending and accepted followers are removed via `DELETE /federation/me/followers`. The service (`reject_follower` / `remove_follower`) handles both cases — accepted actors are removed, pending ones are rejected and a Reject activity is sent.