Skip to content

Commit 110960a

Browse files
committed
fix(ssr): restore shared user profiles
1 parent cf224d6 commit 110960a

7 files changed

Lines changed: 117 additions & 30 deletions

File tree

apps/ssr/client/components/common/404.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { m, useAnimationControls } from "motion/react"
77
import { Fragment, useEffect, useState } from "react"
88
import * as React from "react"
99

10-
const NotFoundContent = () => {
10+
export const NotFoundContent = () => {
1111
const [glitchText, setGlitchText] = useState("404")
1212
const [isGlitching, setIsGlitching] = useState(false)
1313

apps/ssr/client/pages/(main)/share/users/[id]/index.tsx

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { NotFoundContent } from "@client/components/common/404"
12
import { FeedIcon } from "@client/components/ui/feed-icon"
23
import { openInFollowApp } from "@client/lib/helper"
34
import { UrlBuilder } from "@client/lib/url-builder"
@@ -10,6 +11,7 @@ import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
1011
import { useTitle } from "@follow/hooks"
1112
import { cn } from "@follow/utils/utils"
1213
import type { SubscriptionWithFeed, UserProfile } from "@follow-app/client-sdk"
14+
import { FollowAPIError } from "@follow-app/client-sdk"
1315
import * as React from "react"
1416
import { Fragment, memo, useState } from "react"
1517
import { useParams } from "react-router"
@@ -89,22 +91,43 @@ export const Component = () => {
8991

9092
useTitle(user.data?.name)
9193

94+
if (user.isLoading) {
95+
return <LoadingCircle size="large" className="center fixed inset-0" />
96+
}
97+
98+
if (!user.data) {
99+
if (user.error instanceof FollowAPIError && user.error.status === 404) {
100+
return <NotFoundContent />
101+
}
102+
103+
return <ProfileLoadError onRetry={() => void user.refetch()} />
104+
}
105+
92106
return (
93-
<>
94-
{user.isLoading ? (
95-
<LoadingCircle size="large" className="center fixed inset-0" />
96-
) : (
97-
<Fragment>
98-
<UserHero user={user.data!} />
99-
<Lists userId={user.data?.id} />
100-
{/* Subscriptions Section */}
101-
<Subscriptions userId={user.data?.id} />
102-
</Fragment>
103-
)}
104-
</>
107+
<Fragment>
108+
<UserHero user={user.data} />
109+
<Lists userId={user.data.id} />
110+
{/* Subscriptions Section */}
111+
<Subscriptions userId={user.data.id} />
112+
</Fragment>
105113
)
106114
}
107115

116+
const ProfileLoadError = ({ onRetry }: { onRetry: () => void }) => (
117+
<div className="mx-auto flex min-h-[60vh] max-w-xl flex-col items-center justify-center px-6 text-center">
118+
<i className="i-mgc-warning-fill mb-6 size-12 text-orange-500" />
119+
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100">
120+
Unable to load this profile
121+
</h1>
122+
<p className="mt-3 text-zinc-500 dark:text-zinc-400">
123+
This may be a temporary problem. Please try again.
124+
</p>
125+
<Button buttonClassName="mt-8" onClick={onRetry}>
126+
Try again
127+
</Button>
128+
</div>
129+
)
130+
108131
const UserHero = ({ user }: { user: UserProfile }) => {
109132
const subscriptions = useUserSubscriptionsQuery(user.id)
110133

apps/ssr/client/query/users.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import { followClient } from "@client/lib/api-fetch"
22
import { getProviders } from "@client/lib/auth"
33
import { getHydrateData } from "@client/lib/helper"
44
import type { LoginHydrateData } from "@client/pages/(login)/login/metadata"
5-
import { isBizId, sortByAlphabet } from "@follow/utils/utils"
5+
import { sortByAlphabet } from "@follow/utils/utils"
66
import type {
77
InboxSubscriptionResponse,
88
ListSubscriptionResponse,
99
SubscriptionWithFeed,
1010
} from "@follow-app/client-sdk"
1111
import { useQuery } from "@tanstack/react-query"
1212

13+
import { getUserProfile } from "../../src/lib/user-profile-params"
14+
1315
type GetUserSubscriptionsResponse = (
1416
SubscriptionWithFeed | ListSubscriptionResponse | InboxSubscriptionResponse
1517
)[]
@@ -69,13 +71,7 @@ export const useUserSubscriptionsQuery = (userId: string | undefined) => {
6971
}
7072

7173
export const fetchUser = async (handleOrId: string | undefined) => {
72-
const handle = isBizId(handleOrId || "")
73-
? handleOrId
74-
: `${handleOrId}`.startsWith("@")
75-
? `${handleOrId}`.slice(1)
76-
: handleOrId
77-
78-
const res = await followClient.api.profiles.getProfile({ id: handleOrId, handle })
74+
const res = await getUserProfile(followClient, handleOrId)
7975
return res.data
8076
}
8177

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { FollowClient } from "@follow-app/client-sdk"
2+
import { describe, expect, it, vi } from "vitest"
3+
4+
import { getUserProfile, resolveUserProfileParams } from "./user-profile-params"
5+
6+
vi.mock("@follow/utils/utils", () => ({
7+
isBizId: (value: string | undefined) => value === "41125409313095680",
8+
}))
9+
10+
describe("resolveUserProfileParams", () => {
11+
it("uses a handle without also sending it as an id", () => {
12+
expect(resolveUserProfileParams("DIYgod")).toEqual({
13+
id: undefined,
14+
handle: "DIYgod",
15+
})
16+
})
17+
18+
it("removes a leading at sign from handles", () => {
19+
expect(resolveUserProfileParams("@DIYgod")).toEqual({
20+
id: undefined,
21+
handle: "DIYgod",
22+
})
23+
})
24+
25+
it("uses a business id without also sending it as a handle", () => {
26+
expect(resolveUserProfileParams("41125409313095680")).toEqual({
27+
id: "41125409313095680",
28+
handle: undefined,
29+
})
30+
})
31+
32+
it("uses the resolved parameters for the profile request", async () => {
33+
const getProfile = vi.fn().mockResolvedValue({ data: { id: "profile-id" } })
34+
const apiClient = {
35+
api: {
36+
profiles: {
37+
getProfile,
38+
},
39+
},
40+
} as unknown as FollowClient
41+
42+
await getUserProfile(apiClient, "DIYgod")
43+
44+
expect(getProfile).toHaveBeenCalledOnce()
45+
expect(getProfile).toHaveBeenCalledWith({
46+
id: undefined,
47+
handle: "DIYgod",
48+
})
49+
})
50+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { isBizId } from "@follow/utils/utils"
2+
import type { FollowClient } from "@follow-app/client-sdk"
3+
4+
export const resolveUserProfileParams = (handleOrId: string | undefined) => {
5+
if (isBizId(handleOrId || "")) {
6+
return {
7+
id: handleOrId,
8+
handle: undefined,
9+
}
10+
}
11+
12+
return {
13+
id: undefined,
14+
handle: handleOrId?.startsWith("@") ? handleOrId.slice(1) : handleOrId,
15+
}
16+
}
17+
18+
export const getUserProfile = (apiClient: FollowClient, handleOrId: string | undefined) =>
19+
apiClient.api.profiles.getProfile(resolveUserProfileParams(handleOrId))

apps/ssr/src/router/og/user.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import { isBizId } from "@follow/utils/utils"
21
import type { FollowClient } from "@follow-app/client-sdk"
32
import * as React from "react"
43

54
import { renderToImage } from "../../lib/og/render-to-image"
5+
import { getUserProfile } from "../../lib/user-profile-params"
66
import { getImageBase64, OGAvatar, OGCanvas } from "./__base"
77

8-
export const renderUserOG = async (apiClient: FollowClient, id: string) => {
9-
const handle = isBizId(id || "") ? id : `${id}`.startsWith("@") ? `${id}`.slice(1) : id
10-
11-
const user = await apiClient.api.profiles.getProfile({
12-
id,
13-
handle,
14-
})
8+
export const renderUserOG = async (apiClient: FollowClient, handleOrId: string) => {
9+
const user = await getUserProfile(apiClient, handleOrId)
1510

1611
if (!user) {
1712
throw 404

apps/ssr/wrangler.jsonc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
"name": "folo-ssr",
44
"main": "dist/worker/worker-entry.mjs",
55
"compatibility_date": "2026-02-01",
6-
"compatibility_flags": ["nodejs_compat"],
6+
"compatibility_flags": [
7+
"nodejs_compat",
8+
// The SSR Worker fetches api.folo.is, another Worker Route in the same zone.
9+
"global_fetch_strictly_public",
10+
],
711
"observability": {
812
"logs": {
913
"enabled": true,

0 commit comments

Comments
 (0)