feat: social graph features
This commit is contained in:
+147
@@ -101,12 +101,42 @@ export type Collection = {
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
/** The viewer's relationship to a profile, all "viewer → target". Null
|
||||
* when the viewer is anonymous or looking at their own profile. */
|
||||
export type RelationshipState = {
|
||||
/** viewer → target follow is accepted (you follow them). */
|
||||
following: boolean;
|
||||
/** viewer → target follow is pending (you've requested). */
|
||||
follow_requested: boolean;
|
||||
/** target → viewer follow is accepted (they follow you). */
|
||||
follows_you: boolean;
|
||||
/** target → viewer follow is pending (they've asked to follow you). */
|
||||
requests_you: boolean;
|
||||
/** you have blocked them. */
|
||||
blocked: boolean;
|
||||
/** they have blocked you. */
|
||||
blocked_by: boolean;
|
||||
};
|
||||
|
||||
/** Returned by every relationship mutation: the recomputed state plus
|
||||
* the target's live follower/following counts. */
|
||||
export type RelationshipResult = {
|
||||
relationship: RelationshipState;
|
||||
follower_count: number;
|
||||
following_count: number;
|
||||
};
|
||||
|
||||
export type PublicProfile = {
|
||||
handle: string;
|
||||
name: string;
|
||||
avatar_url: string | null;
|
||||
joined_at: string;
|
||||
/** ISO-3166-1 alpha-2 account country, or null when unknown. */
|
||||
country: string | null;
|
||||
post_count: number;
|
||||
follower_count: number;
|
||||
following_count: number;
|
||||
relationship: RelationshipState | null;
|
||||
public_collections: Collection[];
|
||||
};
|
||||
|
||||
@@ -168,6 +198,14 @@ export const posts = {
|
||||
unreshare(id: string) {
|
||||
return call<void>(`/posts/${id}/reshare`, { method: "DELETE" });
|
||||
},
|
||||
/** Report a post. category from PostReportCategory; reason optional.
|
||||
* Auth-required (401 for anonymous callers). Returns nothing (204). */
|
||||
report(id: string, category: PostReportCategory, reason: string) {
|
||||
return call<void>(`/posts/${id}/report`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ category, reason }),
|
||||
});
|
||||
},
|
||||
/** R2 presigned PUT for a single post image. Returns the public URL the post should persist. */
|
||||
async uploadImage(file: File): Promise<string> {
|
||||
const { upload_url, public_url } = await call<{
|
||||
@@ -347,3 +385,112 @@ export const profile = {
|
||||
return call<Post[]>(`/u/${encodeURIComponent(handle)}/feed?${qs}`);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Relationships (follow / block / report) ───────────────────────────────
|
||||
|
||||
/** Allowed user-report categories — mirrors the BFF allow-list. */
|
||||
export type ReportCategory =
|
||||
| "spam"
|
||||
| "harassment"
|
||||
| "impersonation"
|
||||
| "inappropriate"
|
||||
| "other";
|
||||
|
||||
/** Allowed post-report categories — mirrors the BFF allow-list. */
|
||||
export type PostReportCategory =
|
||||
| "spam"
|
||||
| "harassment"
|
||||
| "misinformation"
|
||||
| "inappropriate"
|
||||
| "other";
|
||||
|
||||
// ─── Notifications ─────────────────────────────────────────────────────────
|
||||
|
||||
/** One in-app notification for the signed-in user. `read_at` is absent
|
||||
* (undefined) while unread. Actor fields describe who triggered it. */
|
||||
export type AppNotification = {
|
||||
id: string;
|
||||
kind: string;
|
||||
created_at: string;
|
||||
read_at?: string | null;
|
||||
actor_id?: string | null;
|
||||
actor_name?: string | null;
|
||||
actor_handle?: string | null;
|
||||
actor_avatar_url?: string | null;
|
||||
};
|
||||
|
||||
export const notifications = {
|
||||
/** The caller's notifications, newest first. */
|
||||
list(limit = 30, offset = 0) {
|
||||
const qs = new URLSearchParams({
|
||||
limit: String(limit),
|
||||
offset: String(offset),
|
||||
});
|
||||
return call<AppNotification[]>(`/notifications?${qs}`);
|
||||
},
|
||||
/** Unread count for the header bell badge. */
|
||||
unreadCount() {
|
||||
return call<{ count: number }>("/notifications/unread-count");
|
||||
},
|
||||
/** Mark every notification read (called when the page opens). */
|
||||
markAllRead() {
|
||||
return call<void>("/notifications/read", { method: "POST" });
|
||||
},
|
||||
};
|
||||
|
||||
export const relationships = {
|
||||
/** Send a follow REQUEST. The target must accept before it counts as a
|
||||
* follow — the returned relationship reflects `follow_requested: true`
|
||||
* until then. Requires the viewer to be signed in (401 otherwise). */
|
||||
follow(handle: string) {
|
||||
return call<RelationshipResult>(`/u/${encodeURIComponent(handle)}/follow`, {
|
||||
method: "POST",
|
||||
});
|
||||
},
|
||||
/** Unfollow, or cancel a still-pending request. Idempotent. */
|
||||
unfollow(handle: string) {
|
||||
return call<RelationshipResult>(`/u/${encodeURIComponent(handle)}/follow`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
},
|
||||
/** Accept the target's pending request to follow you. */
|
||||
accept(handle: string) {
|
||||
return call<RelationshipResult>(
|
||||
`/u/${encodeURIComponent(handle)}/follow/accept`,
|
||||
{ method: "POST" },
|
||||
);
|
||||
},
|
||||
/** Reject the target's pending request to follow you. */
|
||||
reject(handle: string) {
|
||||
return call<RelationshipResult>(
|
||||
`/u/${encodeURIComponent(handle)}/follow/reject`,
|
||||
{ method: "POST" },
|
||||
);
|
||||
},
|
||||
/** Remove the target from your followers (they currently follow you). */
|
||||
removeFollower(handle: string) {
|
||||
return call<RelationshipResult>(
|
||||
`/u/${encodeURIComponent(handle)}/follower`,
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
},
|
||||
/** Block the target. Severs follows both ways. */
|
||||
block(handle: string) {
|
||||
return call<RelationshipResult>(`/u/${encodeURIComponent(handle)}/block`, {
|
||||
method: "POST",
|
||||
});
|
||||
},
|
||||
/** Remove your block on the target. */
|
||||
unblock(handle: string) {
|
||||
return call<RelationshipResult>(`/u/${encodeURIComponent(handle)}/block`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
},
|
||||
/** File a report against the target. Returns nothing (204). */
|
||||
report(handle: string, category: ReportCategory, reason: string) {
|
||||
return call<void>(`/u/${encodeURIComponent(handle)}/report`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ category, reason }),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user