feat: implement federated profile handling with support for remote actor URLs across multiple components
All checks were successful
CI / Check / Test (push) Successful in 1h7m53s
All checks were successful
CI / Check / Test (push) Successful in 1h7m53s
This commit is contained in:
@@ -2,7 +2,7 @@ import { Link } from "@tanstack/react-router"
|
||||
import { useCallback } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Bar, BarChart, XAxis, YAxis } from "recharts"
|
||||
import { Search, User } from "lucide-react"
|
||||
import { Globe, Search, User } from "lucide-react"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -23,6 +23,10 @@ type ProfileViewProps = {
|
||||
userId?: string
|
||||
search?: string
|
||||
onSearchChange?: (value: string) => void
|
||||
isFederated?: boolean
|
||||
bio?: string
|
||||
handle?: string
|
||||
actorUrl?: string
|
||||
}
|
||||
|
||||
export function ProfileView({
|
||||
@@ -32,6 +36,9 @@ export function ProfileView({
|
||||
userId,
|
||||
search,
|
||||
onSearchChange,
|
||||
isFederated,
|
||||
bio,
|
||||
handle,
|
||||
}: ProfileViewProps) {
|
||||
const { t } = useTranslation()
|
||||
const initial = (data.username || "?")[0]?.toUpperCase() ?? "?"
|
||||
@@ -50,8 +57,15 @@ export function ProfileView({
|
||||
{avatar && <AvatarImage src={avatar} />}
|
||||
<AvatarFallback>{initial}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-semibold">{data.username}</p>
|
||||
{isFederated && handle && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Globe className="size-3" />
|
||||
<span>{handle}</span>
|
||||
</div>
|
||||
)}
|
||||
{bio && <p className="mt-1 text-sm text-muted-foreground">{bio}</p>}
|
||||
</div>
|
||||
{headerRight}
|
||||
</div>
|
||||
|
||||
@@ -86,6 +86,11 @@ export const userProfileResponseSchema = z.object({
|
||||
history: z.array(monthActivityDtoSchema).optional(),
|
||||
trends: userTrendsDtoSchema.optional(),
|
||||
goals: z.array(goalDtoSchema).optional(),
|
||||
is_federated: z.boolean().optional().default(false),
|
||||
handle: z.string().optional(),
|
||||
display_name: z.string().optional(),
|
||||
bio: z.string().optional(),
|
||||
actor_url: z.string().optional(),
|
||||
})
|
||||
export type UserProfileResponse = z.infer<typeof userProfileResponseSchema>
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ function FeedTab() {
|
||||
movie={entry.movie}
|
||||
review={entry.review}
|
||||
userName={entry.user_display_name}
|
||||
userId={entry.is_federated ? undefined : entry.user_id}
|
||||
userId={entry.user_id}
|
||||
isFederated={entry.is_federated}
|
||||
actorUrl={entry.actor_url}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { UserCheck, UserPlus } from "lucide-react"
|
||||
import { ExternalLink, UserCheck, UserPlus } from "lucide-react"
|
||||
import { BackButton } from "@/components/back-button"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ProfileView, ProfileSkeleton } from "@/components/profile-view"
|
||||
@@ -39,10 +39,14 @@ function UserProfilePage() {
|
||||
<ProfileView
|
||||
data={data}
|
||||
userId={id}
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
search={data.is_federated ? undefined : search}
|
||||
onSearchChange={data.is_federated ? undefined : setSearch}
|
||||
isFederated={data.is_federated}
|
||||
bio={data.bio}
|
||||
handle={data.handle}
|
||||
actorUrl={data.actor_url}
|
||||
actions={
|
||||
data.goals?.length ? (
|
||||
!data.is_federated && data.goals?.length ? (
|
||||
<div className="space-y-2">
|
||||
{data.goals.map((g) => (
|
||||
<GoalCard key={g.year} goal={g} />
|
||||
@@ -51,7 +55,7 @@ function UserProfilePage() {
|
||||
) : undefined
|
||||
}
|
||||
headerRight={
|
||||
!isSelf ? (
|
||||
!isSelf && !data.is_federated ? (
|
||||
isFollowing ? (
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -72,6 +76,13 @@ function UserProfilePage() {
|
||||
{t("common.follow")}
|
||||
</Button>
|
||||
)
|
||||
) : data.is_federated && data.actor_url ? (
|
||||
<a href={data.actor_url} target="_blank" rel="noopener noreferrer">
|
||||
<Button size="sm" variant="outline">
|
||||
<ExternalLink className="mr-1 size-3.5" />
|
||||
{t("common.viewOnRemote", { defaultValue: "Remote" })}
|
||||
</Button>
|
||||
</a>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user