Social: typed realtime events + searchUsers + cross-resource search
Wires three new shapes to the bindings:
- TenantEventMap gains `social:notification`, `social:post`,
`social:article` — typed payloads for the channel pushes the
Phoenix TenantChannel now forwards. Consumers subscribe via the
existing `useArcadiaSubscription` hook.
- SocialBindings.searchUsers(prefix) → `[{user_id, handle,
display_name}]`. Empty prefix returns []. Powers @ autocomplete.
- SocialBindings.search(query, {limit, kinds}) → discriminated
union of post/article hits. Tenant-scoped server-side;
visibility honored. Empty query returns [].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,27 @@ export interface TenantEventMap {
|
||||
announcement: { id: string; title: string; body?: string; [k: string]: unknown };
|
||||
status_update: { id: string; component?: string; status?: string; [k: string]: unknown };
|
||||
event: { type: string; payload?: unknown; [k: string]: unknown };
|
||||
/** Social: notification row written for the connected user. */
|
||||
"social:notification": {
|
||||
id: string;
|
||||
kind: string;
|
||||
payload: Record<string, unknown>;
|
||||
read_at: string | null;
|
||||
inserted_at: string;
|
||||
};
|
||||
/** Social: board post created/replied/updated/deleted in tenant. */
|
||||
"social:post": {
|
||||
action: "created" | "reply" | "updated" | "deleted";
|
||||
post_id: string;
|
||||
parent_id: string | null;
|
||||
author_id: string;
|
||||
};
|
||||
/** Social: article created/updated/deleted in tenant. */
|
||||
"social:article": {
|
||||
action: "created" | "updated" | "deleted";
|
||||
article_id: string;
|
||||
author_id: string;
|
||||
};
|
||||
[key: string]: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,39 @@ export interface SocialNotificationList {
|
||||
unread_count: number;
|
||||
}
|
||||
|
||||
/** One row from `/social/users/search`. `handle` is the lowercased
|
||||
* email local-part stand-in until profiles carry their own handle
|
||||
* column. `display_name` is null when the user hasn't filled in
|
||||
* their profile. */
|
||||
export interface SocialUserMatch {
|
||||
user_id: string;
|
||||
handle: string;
|
||||
display_name: string | null;
|
||||
}
|
||||
|
||||
/** Search result — discriminated by `kind`. Post hits carry a
|
||||
* body snippet; article hits carry title + summary + body snippet. */
|
||||
export type SocialSearchHit =
|
||||
| {
|
||||
kind: "post";
|
||||
id: string;
|
||||
parent_id: string | null;
|
||||
author_id: string;
|
||||
snippet: string;
|
||||
inserted_at: string;
|
||||
}
|
||||
| {
|
||||
kind: "article";
|
||||
id: string;
|
||||
author_id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
summary: string | null;
|
||||
visibility: SocialVisibility;
|
||||
snippet: string;
|
||||
inserted_at: string;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Bindings
|
||||
// ============================================================================
|
||||
@@ -148,6 +181,10 @@ export interface SocialBindings {
|
||||
updateMyProfile(patch: SocialProfileUpdate): Promise<SocialProfile>;
|
||||
getProfile(userId: string): Promise<SocialProfile>;
|
||||
listProfiles(): Promise<SocialProfile[]>;
|
||||
/** @ autocomplete — returns up to 8 users in the viewer's tenant
|
||||
* whose handle (email local-part stand-in) starts with `prefix`.
|
||||
* Empty prefix → empty array (no full enumeration). */
|
||||
searchUsers(prefix: string): Promise<SocialUserMatch[]>;
|
||||
|
||||
// ---------- articles ----------
|
||||
listArticles(opts?: { authorId?: string }): Promise<SocialArticle[]>;
|
||||
@@ -180,6 +217,14 @@ export interface SocialBindings {
|
||||
): Promise<SocialSubscription>;
|
||||
removeSubscription(agentId: string): Promise<void>;
|
||||
|
||||
// ---------- search ----------
|
||||
/** Cross-resource search across posts + articles, tenant-scoped.
|
||||
* Empty query → empty result. */
|
||||
search(
|
||||
query: string,
|
||||
opts?: { limit?: number; kinds?: ("post" | "article")[] },
|
||||
): Promise<SocialSearchHit[]>;
|
||||
|
||||
// ---------- notifications ----------
|
||||
listNotifications(opts?: {
|
||||
limit?: number;
|
||||
@@ -213,6 +258,15 @@ export function createSocialBindings(client: ArcadiaClient): SocialBindings {
|
||||
.then(unwrap),
|
||||
listProfiles: () =>
|
||||
client.GET<Envelope<SocialProfile[]>>(`${base}/profiles`).then(unwrap),
|
||||
searchUsers: (prefix) => {
|
||||
const trimmed = prefix.trim()
|
||||
if (trimmed === "") return Promise.resolve([])
|
||||
return client
|
||||
.GET<Envelope<SocialUserMatch[]>>(`${base}/users/search`, {
|
||||
params: { q: trimmed },
|
||||
})
|
||||
.then(unwrap)
|
||||
},
|
||||
|
||||
// articles
|
||||
listArticles: (opts) =>
|
||||
@@ -301,6 +355,20 @@ export function createSocialBindings(client: ArcadiaClient): SocialBindings {
|
||||
await client.DELETE<void>(`${base}/subscriptions/${agentId}`);
|
||||
},
|
||||
|
||||
// search
|
||||
search: (query, opts) => {
|
||||
const q = query.trim()
|
||||
if (q === "") return Promise.resolve([])
|
||||
const params: Record<string, string | number> = { q }
|
||||
if (opts?.limit !== undefined) params.limit = opts.limit
|
||||
if (opts?.kinds && opts.kinds.length > 0) {
|
||||
params.kinds = opts.kinds.join(",")
|
||||
}
|
||||
return client
|
||||
.GET<Envelope<SocialSearchHit[]>>(`${base}/search`, { params })
|
||||
.then(unwrap)
|
||||
},
|
||||
|
||||
// notifications
|
||||
listNotifications: (opts) =>
|
||||
client.GET<SocialNotificationList>(`${base}/notifications`, {
|
||||
|
||||
Reference in New Issue
Block a user