structural refactor and codebase improvements
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Link } from "@tanstack/react-router"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { EmptyState } from "@/components/empty-state"
|
||||
@@ -28,9 +29,9 @@ export function ActorList({ data, isPending, emptyIcon, emptyTitle, emptyDescrip
|
||||
}
|
||||
|
||||
function actorHandle(actor: RemoteActorDto): string {
|
||||
if (actor.handle.startsWith("@")) return actor.handle
|
||||
try {
|
||||
const host = new URL(actor.url).host
|
||||
return `@${actor.handle}@${host}`
|
||||
return `@${actor.handle}@${new URL(actor.url).host}`
|
||||
} catch {
|
||||
return `@${actor.handle}`
|
||||
}
|
||||
@@ -39,16 +40,31 @@ function actorHandle(actor: RemoteActorDto): string {
|
||||
function ActorCard({ actor, action }: { actor: RemoteActorDto; action?: React.ReactNode }) {
|
||||
const initial = (actor.display_name || actor.handle)[0]?.toUpperCase() ?? "?"
|
||||
|
||||
const identity = (
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<Avatar>
|
||||
{actor.avatar_url && <AvatarImage src={actor.avatar_url} />}
|
||||
<AvatarFallback>{initial}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-semibold">{actor.display_name || actor.handle}</p>
|
||||
<p className="truncate text-xs text-muted-foreground">{actorHandle(actor)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Card size="sm">
|
||||
<CardContent className="flex items-center gap-3">
|
||||
<Avatar>
|
||||
<AvatarFallback>{initial}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-semibold">{actor.display_name || actor.handle}</p>
|
||||
<p className="truncate text-xs text-muted-foreground">{actorHandle(actor)}</p>
|
||||
</div>
|
||||
{actor.user_id ? (
|
||||
<Link to="/users/$id" params={{ id: actor.user_id }} className="min-w-0 flex-1">
|
||||
{identity}
|
||||
</Link>
|
||||
) : (
|
||||
<a href={actor.url} target="_blank" rel="noopener noreferrer" className="min-w-0 flex-1">
|
||||
{identity}
|
||||
</a>
|
||||
)}
|
||||
{action}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -3,7 +3,13 @@ import { useTranslation } from "react-i18next"
|
||||
import { Home, Search, BookOpen, User } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function BottomTabBar({ onLogTap }: { onLogTap: () => void }) {
|
||||
export function BottomTabBar({
|
||||
onLogTap,
|
||||
pendingCount = 0,
|
||||
}: {
|
||||
onLogTap: () => void
|
||||
pendingCount?: number
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const matchRoute = useMatchRoute()
|
||||
|
||||
@@ -55,7 +61,12 @@ export function BottomTabBar({ onLogTap }: { onLogTap: () => void }) {
|
||||
active ? "text-foreground" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<tab.icon className="size-5" strokeWidth={active ? 2.5 : 2} />
|
||||
<div className="relative">
|
||||
<tab.icon className="size-5" strokeWidth={active ? 2.5 : 2} />
|
||||
{tab.to === "/profile" && pendingCount > 0 && (
|
||||
<span className="absolute -right-1 -top-0.5 size-2 rounded-full bg-primary" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] font-medium">{tab.label}</span>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -41,7 +41,6 @@ export function ProfileView({
|
||||
userId,
|
||||
search,
|
||||
onSearchChange,
|
||||
isFederated,
|
||||
bio,
|
||||
handle,
|
||||
}: ProfileViewProps) {
|
||||
@@ -65,7 +64,7 @@ export function ProfileView({
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-semibold">{data.username}</p>
|
||||
{isFederated && handle && (
|
||||
{handle && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Globe className="size-3" />
|
||||
<span>{handle}</span>
|
||||
|
||||
@@ -6,9 +6,20 @@ export const remoteActorDtoSchema = z.object({
|
||||
handle: z.string(),
|
||||
display_name: z.string().optional(),
|
||||
url: z.string(),
|
||||
user_id: z.string().nullish(),
|
||||
avatar_url: z.string().nullish(),
|
||||
})
|
||||
export type RemoteActorDto = z.infer<typeof remoteActorDtoSchema>
|
||||
|
||||
export const followStateSchema = z.enum(["none", "pending", "accepted", "rejected"])
|
||||
export type FollowState = z.infer<typeof followStateSchema>
|
||||
|
||||
export const followRelationSchema = z.object({
|
||||
following: followStateSchema,
|
||||
followed_by: followStateSchema,
|
||||
})
|
||||
export type FollowRelation = z.infer<typeof followRelationSchema>
|
||||
|
||||
export const actorListResponseSchema = z.object({
|
||||
actors: z.array(remoteActorDtoSchema),
|
||||
})
|
||||
@@ -45,6 +56,11 @@ export const blockedActorResponseSchema = z.object({
|
||||
})
|
||||
export type BlockedActorResponse = z.infer<typeof blockedActorResponseSchema>
|
||||
|
||||
export const pendingCountResponseSchema = z.object({
|
||||
count: z.number(),
|
||||
})
|
||||
export type PendingCountResponse = z.infer<typeof pendingCountResponseSchema>
|
||||
|
||||
function getFollowing() {
|
||||
return get<ActorListResponse>("/social/following")
|
||||
}
|
||||
@@ -65,6 +81,18 @@ function getPendingFollowers() {
|
||||
return get<ActorListResponse>("/social/followers/pending")
|
||||
}
|
||||
|
||||
function getPendingFollowerCount() {
|
||||
return get<PendingCountResponse>("/social/followers/pending/count")
|
||||
}
|
||||
|
||||
function getPendingFollowing() {
|
||||
return get<ActorListResponse>("/social/following/pending")
|
||||
}
|
||||
|
||||
function getRelationship(actorUrl: string) {
|
||||
return get<FollowRelation>(`/social/relationship?actor_url=${encodeURIComponent(actorUrl)}`)
|
||||
}
|
||||
|
||||
function follow(data: FollowRequest) {
|
||||
return post("/social/follow", data)
|
||||
}
|
||||
@@ -113,6 +141,9 @@ export const socialKeys = {
|
||||
following: ["following"] as const,
|
||||
followers: ["followers"] as const,
|
||||
pending: ["followers-pending"] as const,
|
||||
pendingCount: ["followers-pending-count"] as const,
|
||||
pendingFollowing: ["following-pending"] as const,
|
||||
relationship: (url: string) => ["relationship", url] as const,
|
||||
userFollowing: (id: string) => ["following", id] as const,
|
||||
userFollowers: (id: string) => ["followers", id] as const,
|
||||
blockedDomains: ["blocked-domains"] as const,
|
||||
@@ -156,12 +187,35 @@ export function usePendingFollowers() {
|
||||
})
|
||||
}
|
||||
|
||||
export function usePendingFollowerCount() {
|
||||
return useQuery({
|
||||
queryKey: socialKeys.pendingCount,
|
||||
queryFn: getPendingFollowerCount,
|
||||
})
|
||||
}
|
||||
|
||||
export function usePendingFollowing() {
|
||||
return useQuery({
|
||||
queryKey: socialKeys.pendingFollowing,
|
||||
queryFn: getPendingFollowing,
|
||||
})
|
||||
}
|
||||
|
||||
export function useRelationship(actorUrl?: string) {
|
||||
return useQuery({
|
||||
queryKey: socialKeys.relationship(actorUrl ?? ""),
|
||||
queryFn: () => getRelationship(actorUrl!),
|
||||
enabled: !!actorUrl,
|
||||
})
|
||||
}
|
||||
|
||||
export function useFollow() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: FollowRequest) => follow(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.following })
|
||||
qc.invalidateQueries({ queryKey: ["relationship"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -172,6 +226,18 @@ export function useUnfollow() {
|
||||
mutationFn: (data: ActorUrlRequest) => unfollow(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.following })
|
||||
qc.invalidateQueries({ queryKey: ["relationship"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCancelFollow() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: ActorUrlRequest) => unfollow(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.pendingFollowing })
|
||||
qc.invalidateQueries({ queryKey: ["relationship"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -182,7 +248,9 @@ export function useAcceptFollower() {
|
||||
mutationFn: (data: ActorUrlRequest) => acceptFollower(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.pending })
|
||||
qc.invalidateQueries({ queryKey: socialKeys.pendingCount })
|
||||
qc.invalidateQueries({ queryKey: socialKeys.followers })
|
||||
qc.invalidateQueries({ queryKey: ["relationship"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -193,6 +261,8 @@ export function useRejectFollower() {
|
||||
mutationFn: (data: ActorUrlRequest) => rejectFollower(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.pending })
|
||||
qc.invalidateQueries({ queryKey: socialKeys.pendingCount })
|
||||
qc.invalidateQueries({ queryKey: ["relationship"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -203,6 +273,7 @@ export function useRemoveFollower() {
|
||||
mutationFn: (data: ActorUrlRequest) => removeFollower(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.followers })
|
||||
qc.invalidateQueries({ queryKey: ["relationship"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"requested": "Requested",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"save": "Save",
|
||||
@@ -143,11 +144,16 @@
|
||||
"following": "Following",
|
||||
"followers": "Followers",
|
||||
"pending": "Pending",
|
||||
"requested": "Requested",
|
||||
"notFollowing": "Not following anyone",
|
||||
"notFollowingDesc": "Follow users to see their reviews in your feed",
|
||||
"noFollowers": "No followers yet",
|
||||
"noFollowersOther": "No followers",
|
||||
"noPending": "No pending requests",
|
||||
"noRequested": "No requests sent",
|
||||
"noRequestedDesc": "Follow requests you send will appear here until accepted",
|
||||
"cancelRequest": "Cancel request",
|
||||
"cancelRequestConfirm": "Cancel this follow request?",
|
||||
"followSent": "Follow request sent to {{handle}}",
|
||||
"followError": "Could not follow that user",
|
||||
"handlePlaceholder": "@user@instance.example"
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Toaster } from "@/components/ui/sonner"
|
||||
import { BottomTabBar } from "@/components/bottom-tab-bar"
|
||||
import { ReviewSheet } from "@/components/review-sheet"
|
||||
import { getAuth } from "@/lib/auth"
|
||||
import { usePendingFollowerCount } from "@/features/social"
|
||||
|
||||
export const Route = createFileRoute("/_app")({
|
||||
beforeLoad: () => {
|
||||
@@ -39,6 +40,8 @@ function AppLayout() {
|
||||
const [logOpen, setLogOpen] = useState(false)
|
||||
const matches = useMatches()
|
||||
const routeKey = matches.at(-1)?.id ?? ""
|
||||
const { data: pending } = usePendingFollowerCount()
|
||||
const pendingCount = pending?.count ?? 0
|
||||
|
||||
return (
|
||||
<div className="mx-auto min-h-svh max-w-lg">
|
||||
@@ -47,7 +50,7 @@ function AppLayout() {
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
<BottomTabBar onLogTap={() => setLogOpen(true)} />
|
||||
<BottomTabBar onLogTap={() => setLogOpen(true)} pendingCount={pendingCount} />
|
||||
<ReviewSheet mode="log" open={logOpen} onOpenChange={setLogOpen} />
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
|
||||
@@ -46,6 +46,8 @@ function ProfilePage() {
|
||||
userId={auth.user_id}
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
bio={data.bio}
|
||||
handle={data.handle}
|
||||
actions={
|
||||
<>
|
||||
<GoalSection goals={data.goals ?? []} />
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import { createFileRoute, Link } from "@tanstack/react-router"
|
||||
import { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ArrowLeft, UserCheck, UserMinus, UserPlus, UserX, Users } from "lucide-react"
|
||||
import { ArrowLeft, Clock, UserCheck, UserMinus, UserPlus, UserX, Users, X } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { toast } from "sonner"
|
||||
import { useAuth } from "@/components/auth-provider"
|
||||
import { ActorList } from "@/components/actor-list"
|
||||
import { ConfirmDialog } from "@/components/confirm-dialog"
|
||||
import {
|
||||
useFollow,
|
||||
useFollowing,
|
||||
useFollowers,
|
||||
usePendingFollowers,
|
||||
usePendingFollowing,
|
||||
useUnfollow,
|
||||
useCancelFollow,
|
||||
useAcceptFollower,
|
||||
useRejectFollower,
|
||||
useRemoveFollower,
|
||||
useUserFollowing,
|
||||
useUserFollowers,
|
||||
} from "@/features/social"
|
||||
import type { RemoteActorDto } from "@/features/social"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
|
||||
type SearchParams = { user?: string }
|
||||
@@ -57,14 +62,31 @@ function SocialPage() {
|
||||
|
||||
function OwnSocialTabs() {
|
||||
const { t } = useTranslation()
|
||||
const { data: pendingFollowers } = usePendingFollowers()
|
||||
const { data: pendingFollowing } = usePendingFollowing()
|
||||
const pendingCount = pendingFollowers?.actors.length ?? 0
|
||||
const requestedCount = pendingFollowing?.actors.length ?? 0
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="following">
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="following">{t("social.following")}</TabsTrigger>
|
||||
<TabsTrigger value="requested">
|
||||
{t("social.requested")}
|
||||
{requestedCount > 0 && (
|
||||
<Badge variant="secondary" className="ml-1.5 text-[10px]">{requestedCount}</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="followers">{t("social.followers")}</TabsTrigger>
|
||||
<TabsTrigger value="pending">{t("social.pending")}</TabsTrigger>
|
||||
<TabsTrigger value="pending">
|
||||
{t("social.pending")}
|
||||
{pendingCount > 0 && (
|
||||
<Badge variant="secondary" className="ml-1.5 text-[10px]">{pendingCount}</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="following"><OwnFollowingTab /></TabsContent>
|
||||
<TabsContent value="requested"><RequestedTab /></TabsContent>
|
||||
<TabsContent value="followers"><OwnFollowersTab /></TabsContent>
|
||||
<TabsContent value="pending"><PendingTab /></TabsContent>
|
||||
</Tabs>
|
||||
@@ -165,6 +187,49 @@ function PendingTab() {
|
||||
)
|
||||
}
|
||||
|
||||
function RequestedTab() {
|
||||
const { t } = useTranslation()
|
||||
const { data, isPending } = usePendingFollowing()
|
||||
const cancelMutation = useCancelFollow()
|
||||
const [target, setTarget] = useState<RemoteActorDto | null>(null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActorList
|
||||
data={data}
|
||||
isPending={isPending}
|
||||
emptyIcon={Clock}
|
||||
emptyTitle={t("social.noRequested")}
|
||||
emptyDescription={t("social.noRequestedDesc")}
|
||||
renderAction={(actor) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-[10px]">{t("common.requested")}</Badge>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTarget(actor)}
|
||||
disabled={cancelMutation.isPending}
|
||||
>
|
||||
<X className="mr-1 size-3.5" />
|
||||
{t("social.cancelRequest")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={!!target}
|
||||
onOpenChange={(open) => !open && setTarget(null)}
|
||||
title={t("social.cancelRequestConfirm")}
|
||||
confirmLabel={t("social.cancelRequest")}
|
||||
onConfirm={() => {
|
||||
if (target) cancelMutation.mutate({ actor_url: target.url })
|
||||
setTarget(null)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function UserFollowingTab({ userId }: { userId: string }) {
|
||||
const { t } = useTranslation()
|
||||
const { data, isPending } = useUserFollowing(userId)
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ExternalLink, UserCheck, UserPlus } from "lucide-react"
|
||||
import { Clock, ExternalLink, UserCheck, UserPlus } from "lucide-react"
|
||||
import { BackButton } from "@/components/back-button"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ConfirmDialog } from "@/components/confirm-dialog"
|
||||
import { ProfileView, ProfileSkeleton } from "@/components/profile-view"
|
||||
import { GoalCard } from "@/components/goal-card"
|
||||
import { useAuth } from "@/components/auth-provider"
|
||||
import { useUserProfile } from "@/features/users"
|
||||
import { useFollow, useUnfollow, useFollowing } from "@/features/social"
|
||||
import { useFollow, useUnfollow, useCancelFollow, useRelationship } from "@/features/social"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
import type { FollowState } from "@/features/social"
|
||||
|
||||
type FollowButtonContent = {
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
variant: "default" | "outline"
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/_app/users/$id")({
|
||||
component: UserProfilePage,
|
||||
@@ -20,17 +28,77 @@ function UserProfilePage() {
|
||||
const { id } = Route.useParams()
|
||||
const { auth } = useAuth()
|
||||
const { data, isPending } = useUserProfile(id, { view: "trends" })
|
||||
const { data: followingData } = useFollowing()
|
||||
const { data: relation } = useRelationship(data?.actor_url ?? undefined)
|
||||
const followMutation = useFollow()
|
||||
const unfollowMutation = useUnfollow()
|
||||
const cancelFollowMutation = useCancelFollow()
|
||||
|
||||
const [search, setSearch] = useState("")
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
useDocumentTitle(data?.username)
|
||||
|
||||
if (isPending) return <ProfileSkeleton />
|
||||
if (!data) return null
|
||||
const isSelf = auth?.user_id === id
|
||||
const isFollowing = followingData?.actors.some((a) => a.handle === data.username) ?? false
|
||||
const followState: FollowState = relation?.following ?? "none"
|
||||
|
||||
// The follow endpoint requires the full "user@domain" handle — a bare
|
||||
// username 500s server-side (CompositeSocialAdapter.resolve_target_identity
|
||||
// only recognizes the local instance when the handle carries "@<host>").
|
||||
// Arrow consts, not function declarations: hoisted declarations would not see
|
||||
// the `if (!data) return null` narrowing above.
|
||||
const sendFollowRequest = () => {
|
||||
followMutation.mutate({ handle: data.handle ?? data.username })
|
||||
}
|
||||
|
||||
const handleFollowButtonClick = () => {
|
||||
switch (followState) {
|
||||
case "pending":
|
||||
setConfirmOpen(true)
|
||||
return
|
||||
case "accepted":
|
||||
unfollowMutation.mutate({ actor_url: data.actor_url ?? "" })
|
||||
return
|
||||
case "rejected":
|
||||
// A rejected request leaves nothing pending to cancel — let the user ask again.
|
||||
sendFollowRequest()
|
||||
return
|
||||
case "none":
|
||||
sendFollowRequest()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function followButtonContent(): FollowButtonContent {
|
||||
switch (followState) {
|
||||
case "pending":
|
||||
return {
|
||||
icon: <Clock className="mr-1 size-3.5" />,
|
||||
label: t("social.requested", { defaultValue: "Requested" }),
|
||||
variant: "outline",
|
||||
}
|
||||
case "accepted":
|
||||
return {
|
||||
icon: <UserCheck className="mr-1 size-3.5" />,
|
||||
label: t("common.following"),
|
||||
variant: "outline",
|
||||
}
|
||||
case "rejected":
|
||||
return {
|
||||
icon: <UserPlus className="mr-1 size-3.5" />,
|
||||
label: t("common.follow"),
|
||||
variant: "default",
|
||||
}
|
||||
case "none":
|
||||
return {
|
||||
icon: <UserPlus className="mr-1 size-3.5" />,
|
||||
label: t("common.follow"),
|
||||
variant: "default",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { icon: followIcon, label: followLabel, variant: followVariant } = followButtonContent()
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
@@ -55,34 +123,36 @@ function UserProfilePage() {
|
||||
) : undefined
|
||||
}
|
||||
headerRight={
|
||||
!isSelf && !data.is_federated ? (
|
||||
isFollowing ? (
|
||||
!isSelf ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => unfollowMutation.mutate({ actor_url: followingData?.actors.find((a) => a.handle === data.username)?.url ?? "" })}
|
||||
disabled={unfollowMutation.isPending}
|
||||
variant={followVariant}
|
||||
onClick={handleFollowButtonClick}
|
||||
disabled={followMutation.isPending || unfollowMutation.isPending}
|
||||
>
|
||||
<UserCheck className="mr-1 size-3.5" />
|
||||
{t("common.following")}
|
||||
{followIcon}
|
||||
{followLabel}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => followMutation.mutate({ handle: data.username })}
|
||||
disabled={followMutation.isPending}
|
||||
>
|
||||
<UserPlus className="mr-1 size-3.5" />
|
||||
{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>
|
||||
{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>
|
||||
)}
|
||||
<ConfirmDialog
|
||||
open={confirmOpen}
|
||||
onOpenChange={setConfirmOpen}
|
||||
title={t("social.cancelFollowRequest", { defaultValue: "Cancel follow request?" })}
|
||||
confirmLabel={t("social.cancelRequest", { defaultValue: "Cancel request" })}
|
||||
onConfirm={() => {
|
||||
cancelFollowMutation.mutate({ actor_url: data.actor_url ?? "" })
|
||||
setConfirmOpen(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user