feat: followers/following links on remote profile; render remote post content as HTML
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m39s
test / unit (pull_request) Failing after 10m48s
test / integration (pull_request) Failing after 16m54s

This commit is contained in:
2026-05-15 00:04:54 +02:00
parent 0b4c8c6c40
commit 8b3dfffd3b
8 changed files with 46 additions and 4 deletions

View File

@@ -1398,6 +1398,8 @@ impl domain::ports::FederationActionPort for ActivityPubService {
banner_url: actor.banner_url.as_ref().map(|u| u.to_string()), banner_url: actor.banner_url.as_ref().map(|u| u.to_string()),
also_known_as: actor.also_known_as.clone(), also_known_as: actor.also_known_as.clone(),
outbox_url: Some(actor.outbox_url.to_string()), outbox_url: Some(actor.outbox_url.to_string()),
followers_url: Some(actor.followers_url.to_string()),
following_url: Some(actor.following_url.to_string()),
attachment: actor attachment: actor
.attachment .attachment
.iter() .iter()

View File

@@ -45,6 +45,6 @@ impl RemoteActorRepository for PgRemoteActorRepository {
"SELECT url,handle,display_name,inbox_url,shared_inbox_url,public_key,avatar_url,last_fetched_at FROM remote_actors WHERE url=$1" "SELECT url,handle,display_name,inbox_url,shared_inbox_url,public_key,avatar_url,last_fetched_at FROM remote_actors WHERE url=$1"
).bind(url).fetch_optional(&self.pool).await ).bind(url).fetch_optional(&self.pool).await
.map_err(|e| DomainError::Internal(e.to_string())) .map_err(|e| DomainError::Internal(e.to_string()))
.map(|o| o.map(|r| RemoteActor { url: r.url, handle: r.handle, display_name: r.display_name, inbox_url: r.inbox_url, shared_inbox_url: r.shared_inbox_url, public_key: r.public_key, avatar_url: r.avatar_url, last_fetched_at: r.last_fetched_at, bio: None, banner_url: None, also_known_as: None, outbox_url: None, attachment: vec![] })) .map(|o| o.map(|r| RemoteActor { url: r.url, handle: r.handle, display_name: r.display_name, inbox_url: r.inbox_url, shared_inbox_url: r.shared_inbox_url, public_key: r.public_key, avatar_url: r.avatar_url, last_fetched_at: r.last_fetched_at, bio: None, banner_url: None, also_known_as: None, outbox_url: None, followers_url: None, following_url: None, attachment: vec![] }))
} }
} }

View File

@@ -106,5 +106,7 @@ pub struct RemoteActorResponse {
pub banner_url: Option<String>, pub banner_url: Option<String>,
pub also_known_as: Option<String>, pub also_known_as: Option<String>,
pub outbox_url: Option<String>, pub outbox_url: Option<String>,
pub followers_url: Option<String>,
pub following_url: Option<String>,
pub attachment: Vec<ProfileField>, pub attachment: Vec<ProfileField>,
} }

View File

@@ -14,5 +14,7 @@ pub struct RemoteActor {
pub banner_url: Option<String>, pub banner_url: Option<String>,
pub also_known_as: Option<String>, pub also_known_as: Option<String>,
pub outbox_url: Option<String>, pub outbox_url: Option<String>,
pub followers_url: Option<String>,
pub following_url: Option<String>,
pub attachment: Vec<(String, String)>, pub attachment: Vec<(String, String)>,
} }

View File

@@ -204,6 +204,8 @@ pub async fn lookup_handler(
banner_url: actor.banner_url, banner_url: actor.banner_url,
also_known_as: actor.also_known_as, also_known_as: actor.also_known_as,
outbox_url: actor.outbox_url, outbox_url: actor.outbox_url,
followers_url: actor.followers_url,
following_url: actor.following_url,
attachment: actor attachment: actor
.attachment .attachment
.into_iter() .into_iter()

View File

@@ -133,6 +133,31 @@ export function RemoteUserProfile({
</Link> </Link>
</Button> </Button>
{(actor.followersUrl || actor.followingUrl) && (
<div className="mt-3 flex gap-3 text-sm">
{actor.followersUrl && (
<Link
href={actor.followersUrl}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground hover:underline"
>
Followers
</Link>
)}
{actor.followingUrl && (
<Link
href={actor.followingUrl}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground hover:underline"
>
Following
</Link>
)}
</div>
)}
{actor.alsoKnownAs && ( {actor.alsoKnownAs && (
<p className="mt-2 text-xs text-muted-foreground"> <p className="mt-2 text-xs text-muted-foreground">
Also known as:{" "} Also known as:{" "}

View File

@@ -152,9 +152,16 @@ export function ThoughtCard({
</DropdownMenu> </DropdownMenu>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{thought.author.local ? (
<p className="whitespace-pre-wrap break-words text-shadow-sm"> <p className="whitespace-pre-wrap break-words text-shadow-sm">
{thought.content} {thought.content}
</p> </p>
) : (
<div
className="text-sm break-words [&_a]:underline [&_a]:text-primary [&_p]:mb-2"
dangerouslySetInnerHTML={{ __html: thought.content }}
/>
)}
</CardContent> </CardContent>
{token && ( {token && (

View File

@@ -30,6 +30,8 @@ export const RemoteActorSchema = z.object({
bannerUrl: z.string().nullable(), bannerUrl: z.string().nullable(),
alsoKnownAs: z.string().nullable(), alsoKnownAs: z.string().nullable(),
outboxUrl: z.string().nullable(), outboxUrl: z.string().nullable(),
followersUrl: z.string().nullable(),
followingUrl: z.string().nullable(),
attachment: z.array(ProfileFieldSchema), attachment: z.array(ProfileFieldSchema),
}); });
export type RemoteActor = z.infer<typeof RemoteActorSchema>; export type RemoteActor = z.infer<typeof RemoteActorSchema>;