From 2e3b81de179d28dc4bd18d62d035d599736f9a39 Mon Sep 17 00:00:00 2001
From: Gabriel Kaszewski
Date: Fri, 15 May 2026 04:35:04 +0200
Subject: [PATCH] fix: full fediverse handle display + follower count includes
remote
---
crates/adapters/activitypub-base/src/service.rs | 4 +++-
thoughts-frontend/app/users/[username]/page.tsx | 17 +++++++++++++++--
.../components/federation/pending-requests.tsx | 3 ++-
.../components/federation/remote-followers.tsx | 3 ++-
.../components/federation/remote-following.tsx | 3 ++-
thoughts-frontend/lib/utils.ts | 11 +++++++++++
6 files changed, 35 insertions(+), 6 deletions(-)
diff --git a/crates/adapters/activitypub-base/src/service.rs b/crates/adapters/activitypub-base/src/service.rs
index 4bbe7e7..4a92c57 100644
--- a/crates/adapters/activitypub-base/src/service.rs
+++ b/crates/adapters/activitypub-base/src/service.rs
@@ -422,9 +422,11 @@ impl ActivityPubService {
);
}
+ let domain = remote_actor.ap_id.host_str().unwrap_or("");
+ let full_handle = format!("{}@{}", remote_actor.username, domain);
let remote = RemoteActor {
url: remote_actor.ap_id.to_string(),
- handle: remote_actor.username.clone(),
+ handle: full_handle,
inbox_url: remote_actor.inbox_url.to_string(),
shared_inbox_url: None,
display_name: Some(remote_actor.username.clone()),
diff --git a/thoughts-frontend/app/users/[username]/page.tsx b/thoughts-frontend/app/users/[username]/page.tsx
index 25474f8..ab172d8 100644
--- a/thoughts-frontend/app/users/[username]/page.tsx
+++ b/thoughts-frontend/app/users/[username]/page.tsx
@@ -3,6 +3,8 @@ import {
getFollowersList,
getFollowingList,
getMe,
+ getRemoteFollowers,
+ getRemoteFollowing,
getTopFriends,
getUserProfile,
getUserThoughts,
@@ -95,16 +97,27 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
thoughtsResult.status === "fulfilled" ? thoughtsResult.value.items : [];
const thoughtThreads = buildThoughtThreads(thoughts);
- const followersCount =
+ const localFollowersCount =
followersResult.status === "fulfilled"
? followersResult.value.total
: 0;
- const followingCount =
+ const localFollowingCount =
followingResult.status === "fulfilled"
? followingResult.value.total
: 0;
const isOwnProfile = me?.username === user.username;
+
+ const [remoteFollowersCount, remoteFollowingCount] =
+ isOwnProfile && token
+ ? await Promise.all([
+ getRemoteFollowers(token).then((r) => r.length).catch(() => 0),
+ getRemoteFollowing(token).then((r) => r.length).catch(() => 0),
+ ])
+ : [0, 0];
+
+ const followersCount = localFollowersCount + remoteFollowersCount;
+ const followingCount = localFollowingCount + remoteFollowingCount;
const isFollowing = user.isFollowedByViewer;
const apiDomain = process.env.NEXT_PUBLIC_API_URL
diff --git a/thoughts-frontend/components/federation/pending-requests.tsx b/thoughts-frontend/components/federation/pending-requests.tsx
index 6f995b8..436517b 100644
--- a/thoughts-frontend/components/federation/pending-requests.tsx
+++ b/thoughts-frontend/components/federation/pending-requests.tsx
@@ -12,6 +12,7 @@ import { UserAvatar } from "@/components/user-avatar";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import Link from "next/link";
+import { fullFediverseHandle } from "@/lib/utils";
interface Props {
compact?: boolean;
@@ -71,7 +72,7 @@ export function PendingRequests({ compact = false }: Props) {
{actor.displayName || actor.handle}
- @{actor.handle}
+ @{fullFediverseHandle(actor.handle, actor.url)}
diff --git a/thoughts-frontend/components/federation/remote-followers.tsx b/thoughts-frontend/components/federation/remote-followers.tsx
index 7414c76..0ec49d3 100644
--- a/thoughts-frontend/components/federation/remote-followers.tsx
+++ b/thoughts-frontend/components/federation/remote-followers.tsx
@@ -7,6 +7,7 @@ import { UserAvatar } from "@/components/user-avatar";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import Link from "next/link";
+import { fullFediverseHandle } from "@/lib/utils";
export function RemoteFollowers() {
const { token } = useAuth();
@@ -51,7 +52,7 @@ export function RemoteFollowers() {
{actor.displayName || actor.handle}
- @{actor.handle}
+ @{fullFediverseHandle(actor.handle, actor.url)}
diff --git a/thoughts-frontend/components/federation/remote-following.tsx b/thoughts-frontend/components/federation/remote-following.tsx
index 33e1a67..b27062b 100644
--- a/thoughts-frontend/components/federation/remote-following.tsx
+++ b/thoughts-frontend/components/federation/remote-following.tsx
@@ -7,6 +7,7 @@ import { UserAvatar } from "@/components/user-avatar";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import Link from "next/link";
+import { fullFediverseHandle } from "@/lib/utils";
export function RemoteFollowing() {
const { token } = useAuth();
@@ -51,7 +52,7 @@ export function RemoteFollowing() {
{actor.displayName || actor.handle}
- @{actor.handle}
+ @{fullFediverseHandle(actor.handle, actor.url)}
diff --git a/thoughts-frontend/lib/utils.ts b/thoughts-frontend/lib/utils.ts
index f4a2c83..539a602 100644
--- a/thoughts-frontend/lib/utils.ts
+++ b/thoughts-frontend/lib/utils.ts
@@ -6,6 +6,17 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+/** Construct a full fediverse handle like `user@instance.social`.
+ * Falls back gracefully for existing DB rows that only stored the username. */
+export function fullFediverseHandle(handle: string, actorUrl: string): string {
+ if (handle.includes("@")) return handle;
+ try {
+ return `${handle}@${new URL(actorUrl).hostname}`;
+ } catch {
+ return handle;
+ }
+}
+
export function buildThoughtThreads(thoughts: Thought[]): ThoughtThreadType[] {
const thoughtMap = new Map();
thoughts.forEach((t) => thoughtMap.set(t.id, t));