@rocapine/community-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/lib/config.d.ts +74 -0
- package/lib/config.js +51 -0
- package/lib/events.d.ts +20 -0
- package/lib/events.js +33 -0
- package/lib/hooks.d.ts +122 -0
- package/lib/hooks.js +494 -0
- package/lib/identity.d.ts +23 -0
- package/lib/identity.js +103 -0
- package/lib/inbox-hooks.d.ts +23 -0
- package/lib/inbox-hooks.js +57 -0
- package/lib/inbox-service.d.ts +26 -0
- package/lib/inbox-service.js +94 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +89 -0
- package/lib/models.d.ts +187 -0
- package/lib/models.js +178 -0
- package/lib/provider.d.ts +7 -0
- package/lib/provider.js +23 -0
- package/lib/schema-check.d.ts +19 -0
- package/lib/schema-check.js +52 -0
- package/lib/service.d.ts +147 -0
- package/lib/service.js +454 -0
- package/lib/time.d.ts +21 -0
- package/lib/time.js +32 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rocapine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @rocapine/community-core
|
|
2
|
+
|
|
3
|
+
Headless data layer for the Community SDK: config, models, Supabase queries,
|
|
4
|
+
React Query hooks, anonymous identity, and the analytics event seam. No
|
|
5
|
+
React Native, no components, no store, no router, no analytics provider.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @rocapine/community-core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies: `react >=18`, `@supabase/supabase-js ^2.45.0`,
|
|
12
|
+
`@tanstack/react-query ^5.50.0`.
|
|
13
|
+
|
|
14
|
+
## `CommunityConfig`
|
|
15
|
+
|
|
16
|
+
The single configuration object passed to `<CommunityProvider config={...}>`:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import type { CommunityConfig } from "@rocapine/community-core";
|
|
20
|
+
|
|
21
|
+
const config: CommunityConfig = {
|
|
22
|
+
// Connection — injected, never created by the SDK.
|
|
23
|
+
supabase: mySupabaseClient, // or `null` for degraded mode, see below
|
|
24
|
+
|
|
25
|
+
// Product vocabulary
|
|
26
|
+
appName: "My App", // pushes, inbox, broadcast fallback title
|
|
27
|
+
anonymousAuthorFallback: "Someone", // shown when an author has no username yet
|
|
28
|
+
|
|
29
|
+
// Topics — nothing hardcoded. `id` resolves to `topics.<id>` in the UI
|
|
30
|
+
// package's i18n catalog (see packages/ui/README.md); the app supplies
|
|
31
|
+
// the key for any topic it invents. `officialOnly: true` topics (e.g.
|
|
32
|
+
// "news") are writable only by `is_official` profiles — enforced by RLS.
|
|
33
|
+
topics: [{ id: "general" }, { id: "question" }, { id: "news", officialOnly: true }],
|
|
34
|
+
|
|
35
|
+
// Opt-in modules — MUST mirror what the CLI installed on the backend.
|
|
36
|
+
modules: {
|
|
37
|
+
polls: true,
|
|
38
|
+
push: false,
|
|
39
|
+
inbox: true,
|
|
40
|
+
reaction: { key: "cheer" }, // or `false` to disable the reaction module
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Host adapters — every field optional, no-op defaults.
|
|
44
|
+
host: {
|
|
45
|
+
getDisplayName: () => myUserStore.firstName,
|
|
46
|
+
getAnalyticsIds: () => ({ amplitudeId: "...", revenuecatId: "..." }),
|
|
47
|
+
onEvent: (name, props) => myAnalytics.track(name, props),
|
|
48
|
+
rulesAcceptance: {
|
|
49
|
+
get: async () => myStore.acceptedRules,
|
|
50
|
+
set: async () => myStore.setAcceptedRules(true),
|
|
51
|
+
},
|
|
52
|
+
onContentPublished: () => myReviewGate.arm(),
|
|
53
|
+
getLocale: () => "en",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Degraded mode
|
|
59
|
+
|
|
60
|
+
`supabase: null` is a first-class, supported state (not an error path): every
|
|
61
|
+
hook returns empty/loading-false states, `useCommunityConfig()`'s
|
|
62
|
+
`requireClient()` throws a `CommunityDisabledError` that hooks catch
|
|
63
|
+
internally, and exactly one `console.warn` is logged the first time a query
|
|
64
|
+
actually needs the client. The app boots and renders normally with no
|
|
65
|
+
backend configured — the pattern `examples/expo-app` uses when
|
|
66
|
+
`EXPO_PUBLIC_SUPABASE_URL`/`_KEY` are unset.
|
|
67
|
+
|
|
68
|
+
### Host adapters (`CommunityHostAdapters`)
|
|
69
|
+
|
|
70
|
+
All optional, all default to a no-op:
|
|
71
|
+
|
|
72
|
+
| Field | Signature | Purpose |
|
|
73
|
+
| -------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
74
|
+
| `getDisplayName` | `() => string \| null` | Feeds the display name shown for the current user before their community username is set |
|
|
75
|
+
| `getAnalyticsIds` | `() => { amplitudeId?: string; revenuecatId?: string }` | Cross-referencing ids attached where the host wants them |
|
|
76
|
+
| `onEvent` | `(name: string, props: Record<string, unknown>) => void` | **The only analytics seam.** Every product event goes through this; the SDK never depends on any analytics provider. Never throws through to the UI even if the adapter itself throws |
|
|
77
|
+
| `rulesAcceptance` | `{ get(): Promise<boolean>; set(): Promise<void> }` | Persists whether the user has accepted the one-time community guidelines sheet |
|
|
78
|
+
| `onContentPublished` | `() => void` | Fired after a post/comment is successfully published (e.g. to arm a review-gate) |
|
|
79
|
+
| `getLocale` | `() => string` | Selects which locale-specific behavior the _host_ wants (distinct from the UI package's own `translations.locale` prop) |
|
|
80
|
+
|
|
81
|
+
### Feed row extension point (`CommunityConfig.feed`)
|
|
82
|
+
|
|
83
|
+
Optional, defaults to `{}` — omitting `feed` entirely keeps today's behavior
|
|
84
|
+
byte-identical. It exists for a host that stores extra columns on `posts` the
|
|
85
|
+
shared schema doesn't know about (e.g. Nightward's `posts.seed_likes`
|
|
86
|
+
top-up), without forking the query or mapping layer:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import type { CommunityConfig } from "@rocapine/community-core";
|
|
90
|
+
|
|
91
|
+
const config: CommunityConfig = {
|
|
92
|
+
// ...supabase, appName, anonymousAuthorFallback, topics, modules...
|
|
93
|
+
feed: {
|
|
94
|
+
// Appended to the posts `select` in every posts-table query
|
|
95
|
+
// (fetchFeedPage, fetchUserPosts, searchPosts). Only `[a-z0-9_]+`
|
|
96
|
+
// column names are accepted — anything else (a relation embed, a rename,
|
|
97
|
+
// a stray `*`) is dropped with a `console.warn` instead of reaching the
|
|
98
|
+
// query string.
|
|
99
|
+
extraPostColumns: ["seed_likes"],
|
|
100
|
+
// Runs last inside `mapPostRow`, given the mapped post and the raw row
|
|
101
|
+
// (including any extraPostColumns) it came from.
|
|
102
|
+
transformPost: (post, row) => ({
|
|
103
|
+
...post,
|
|
104
|
+
likeCount: post.likeCount + (Number(row.seed_likes) || 0),
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Two things worth knowing:
|
|
111
|
+
|
|
112
|
+
- `transformPost` only runs on rows the SDK fetched from the backend. The
|
|
113
|
+
optimistic post shown right after `createPost` has no raw row yet, so it
|
|
114
|
+
renders untransformed until the next refetch swaps in the server-mapped
|
|
115
|
+
(and transformed) version — acceptable since the gap is a moderation-queue
|
|
116
|
+
window, not a rendered-forever state.
|
|
117
|
+
- `fetchThread` queries `comments`, not `posts` — `extraPostColumns` and
|
|
118
|
+
`transformPost` don't apply there; a thread's parent post is the
|
|
119
|
+
already-fetched, already-transformed `FeedPost` the caller passes in, never
|
|
120
|
+
re-fetched.
|
|
121
|
+
|
|
122
|
+
### Event names (`COMMUNITY_EVENTS`)
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { COMMUNITY_EVENTS, type CommunityEventName } from "@rocapine/community-core";
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`COMMUNITY_EVENTS` is a frozen map of stable event-name strings — `opened`,
|
|
129
|
+
`rulesAccepted`, `postPublished`, `pollVoted`, `threadOpened`, `postLiked`,
|
|
130
|
+
`commentPublished`, `profileOpened`, `profileUpdated`, `reactionAdded`,
|
|
131
|
+
`userReported`, `userBlocked`, `contentDeleted`, `inboxOpened` — forwarded to
|
|
132
|
+
`host.onEvent` by the exported `emitEvent(cfg, name, props)` helper. Route or
|
|
133
|
+
ignore these in your own tracking plan; nothing else in the package calls an
|
|
134
|
+
analytics provider directly.
|
|
135
|
+
|
|
136
|
+
### Schema drift check
|
|
137
|
+
|
|
138
|
+
`CommunityProvider` calls a dev-only diagnostic on mount that reads
|
|
139
|
+
`community_meta.schema_version` from the backend and warns (never throws) if
|
|
140
|
+
it doesn't match `REQUIRED_SCHEMA_VERSION` (currently `1`, exported from the
|
|
141
|
+
package). This only runs when React Native's `__DEV__` is true — it never
|
|
142
|
+
runs in production and is silent in degraded mode.
|
|
143
|
+
|
|
144
|
+
## What else is exported
|
|
145
|
+
|
|
146
|
+
- **Models & limits**: `PostRow`, `CommentRow`, `ProfileRow`, `FeedPost`,
|
|
147
|
+
`ThreadComment`, `CommunityProfile`, `FeedPoll`, `PollData`,
|
|
148
|
+
`ReactionData`, and DB-mirroring constants `FEED_PAGE_SIZE`,
|
|
149
|
+
`POST_MAX_LENGTH` (2000), `COMMENT_MAX_LENGTH` (1000), `POLL_MIN_OPTIONS`
|
|
150
|
+
(2), `POLL_MAX_OPTIONS` (4), `POLL_OPTION_MAX_LENGTH` (60) — client-side
|
|
151
|
+
mirrors of the backend's check constraints.
|
|
152
|
+
- **Identity**: `ensureIdentity()`, `resetIdentity()`, `syncProfileFromHost()`
|
|
153
|
+
— anonymous Supabase auth session management.
|
|
154
|
+
- **Queries** (`service.ts`): `fetchFeedPage`, `countNewPosts`, `fetchProfile`,
|
|
155
|
+
`fetchUserPosts`, `searchPosts`, `fetchThread`, `createPost`, `votePoll`,
|
|
156
|
+
`createComment`, `moderateOne`, `setLike`, `setReaction`,
|
|
157
|
+
`fetchReactionSummaries`, `reportContent`, `blockUser`, `deleteOwnPost`,
|
|
158
|
+
`deleteOwnComment`, `updateProfile`, `uploadAvatar`, `buildFeedSelect`
|
|
159
|
+
(pure helper behind `feed.extraPostColumns`, see below).
|
|
160
|
+
- **React Query hooks** (`hooks.ts`): `useCommunityFeed`, `useSearchPosts`,
|
|
161
|
+
`useNewPostsCount`, `useCommunityUnseenCount`, `useThread`, `useProfile`,
|
|
162
|
+
`useUserPosts`, `useMyUid`, `useUpdateProfile`, `useCreatePost`,
|
|
163
|
+
`useCreateComment`, `useToggleLike`, `useVotePoll`, `useReactToPost`,
|
|
164
|
+
`useReport`, `useBlockUser`, `useDeleteContent`.
|
|
165
|
+
- **Inbox** (opt-in via `modules.inbox`): `fetchInbox`, `markInboxSeen`,
|
|
166
|
+
`unreadCount`, and hooks `useNotificationInbox`,
|
|
167
|
+
`useUnreadNotificationCount`, `useMarkInboxSeen`.
|
|
168
|
+
- **`timeAgo(iso)`** — hardcoded-English relative-time formatting, kept for
|
|
169
|
+
back-compat. **`timeAgoParts(iso, nowMs)`** — the language-free unit+value
|
|
170
|
+
decomposition the UI package actually localizes through its own catalog
|
|
171
|
+
(see `formatTimeAgo` in `@rocapine/community-ui`).
|
|
172
|
+
|
|
173
|
+
See `src/index.ts` for the exhaustive, authoritative export list.
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { FeedPost } from "./models";
|
|
3
|
+
export type CommunityTopicDef = {
|
|
4
|
+
id: string;
|
|
5
|
+
officialOnly?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type CommunityModules = {
|
|
8
|
+
polls: boolean;
|
|
9
|
+
push: boolean;
|
|
10
|
+
inbox: boolean;
|
|
11
|
+
reaction: {
|
|
12
|
+
key: string;
|
|
13
|
+
} | false;
|
|
14
|
+
};
|
|
15
|
+
export type CommunityHostAdapters = {
|
|
16
|
+
getDisplayName?: () => string | null;
|
|
17
|
+
getAnalyticsIds?: () => {
|
|
18
|
+
amplitudeId?: string;
|
|
19
|
+
revenuecatId?: string;
|
|
20
|
+
};
|
|
21
|
+
onEvent?: (name: string, props: Record<string, unknown>) => void;
|
|
22
|
+
rulesAcceptance?: {
|
|
23
|
+
get(): Promise<boolean>;
|
|
24
|
+
set(): Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
onContentPublished?: () => void;
|
|
27
|
+
getLocale?: () => string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Host-defined extension point for the raw `posts` row shape and its mapped
|
|
31
|
+
* `FeedPost` output — added for hosts (e.g. Nightward) that store extra
|
|
32
|
+
* columns on `posts` (like a seed-likes top-up) the shared schema doesn't
|
|
33
|
+
* know about. Both fields are optional and default to a no-op, so a host
|
|
34
|
+
* that never sets `feed` gets byte-identical behavior to before this existed.
|
|
35
|
+
*
|
|
36
|
+
* `transformPost` only runs on rows fetched from the backend (`fetchFeedPage`,
|
|
37
|
+
* `fetchUserPosts`, `searchPosts` — see `mapPostRow` call sites in
|
|
38
|
+
* `service.ts`). It does NOT run on the optimistic post shown immediately
|
|
39
|
+
* after `createPost` (there is no raw row yet) — that post renders untransformed
|
|
40
|
+
* until the next refetch replaces it with a server-mapped one. `fetchThread`
|
|
41
|
+
* queries `comments`, not `posts`, so `extraPostColumns`/`transformPost` don't
|
|
42
|
+
* apply there.
|
|
43
|
+
*/
|
|
44
|
+
export type CommunityFeedConfig = {
|
|
45
|
+
/** Appended to the posts select in every posts-table query. Only
|
|
46
|
+
* `[a-z0-9_]+` column names are accepted; anything else is dropped with a
|
|
47
|
+
* `console.warn` (minimal guard against passing a crafted select fragment). */
|
|
48
|
+
extraPostColumns?: string[];
|
|
49
|
+
/** Applied to every server-mapped `FeedPost`, given the raw row (including
|
|
50
|
+
* any `extraPostColumns`) it was built from. */
|
|
51
|
+
transformPost?: (post: FeedPost, row: Record<string, unknown>) => FeedPost;
|
|
52
|
+
};
|
|
53
|
+
export type CommunityConfig = {
|
|
54
|
+
supabase: SupabaseClient | null;
|
|
55
|
+
appName: string;
|
|
56
|
+
anonymousAuthorFallback: string;
|
|
57
|
+
topics: CommunityTopicDef[];
|
|
58
|
+
modules: CommunityModules;
|
|
59
|
+
host?: CommunityHostAdapters;
|
|
60
|
+
feed?: CommunityFeedConfig;
|
|
61
|
+
};
|
|
62
|
+
export declare class CommunityDisabledError extends Error {
|
|
63
|
+
constructor();
|
|
64
|
+
}
|
|
65
|
+
type RequiredCommunityHostAdapters = Required<CommunityHostAdapters>;
|
|
66
|
+
export type ResolvedCommunityConfig = CommunityConfig & {
|
|
67
|
+
host: RequiredCommunityHostAdapters;
|
|
68
|
+
feed: CommunityFeedConfig;
|
|
69
|
+
requireClient(): SupabaseClient;
|
|
70
|
+
composeTopics(): CommunityTopicDef[];
|
|
71
|
+
isOfficialTopic(id: string): boolean;
|
|
72
|
+
};
|
|
73
|
+
export declare function resolveConfig(config: CommunityConfig): ResolvedCommunityConfig;
|
|
74
|
+
export {};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CommunityDisabledError = void 0;
|
|
4
|
+
exports.resolveConfig = resolveConfig;
|
|
5
|
+
class CommunityDisabledError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("community backend not configured");
|
|
8
|
+
this.name = "CommunityDisabledError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.CommunityDisabledError = CommunityDisabledError;
|
|
12
|
+
let warned = false;
|
|
13
|
+
const defaultHost = {
|
|
14
|
+
getDisplayName: () => null,
|
|
15
|
+
getAnalyticsIds: () => ({}),
|
|
16
|
+
onEvent: () => { },
|
|
17
|
+
rulesAcceptance: {
|
|
18
|
+
get: async () => false,
|
|
19
|
+
set: async () => { },
|
|
20
|
+
},
|
|
21
|
+
onContentPublished: () => { },
|
|
22
|
+
getLocale: () => "en",
|
|
23
|
+
};
|
|
24
|
+
function resolveConfig(config) {
|
|
25
|
+
const host = {
|
|
26
|
+
...defaultHost,
|
|
27
|
+
...config.host,
|
|
28
|
+
};
|
|
29
|
+
const resolved = {
|
|
30
|
+
...config,
|
|
31
|
+
host,
|
|
32
|
+
feed: config.feed ?? {},
|
|
33
|
+
requireClient() {
|
|
34
|
+
if (!resolved.supabase) {
|
|
35
|
+
if (!warned) {
|
|
36
|
+
warned = true;
|
|
37
|
+
console.warn("[@rocapine/community-core] community backend not configured — running in degraded mode.");
|
|
38
|
+
}
|
|
39
|
+
throw new CommunityDisabledError();
|
|
40
|
+
}
|
|
41
|
+
return resolved.supabase;
|
|
42
|
+
},
|
|
43
|
+
composeTopics() {
|
|
44
|
+
return resolved.topics.filter((topic) => !topic.officialOnly);
|
|
45
|
+
},
|
|
46
|
+
isOfficialTopic(id) {
|
|
47
|
+
return resolved.topics.some((topic) => topic.id === id && topic.officialOnly === true);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return Object.freeze(resolved);
|
|
51
|
+
}
|
package/lib/events.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ResolvedCommunityConfig } from "./config";
|
|
2
|
+
export declare const COMMUNITY_EVENTS: {
|
|
3
|
+
readonly opened: "community_opened";
|
|
4
|
+
readonly rulesAccepted: "community_rules_accepted";
|
|
5
|
+
readonly postPublished: "community_post_published";
|
|
6
|
+
readonly pollVoted: "community_poll_voted";
|
|
7
|
+
readonly threadOpened: "community_thread_opened";
|
|
8
|
+
readonly postLiked: "community_post_liked";
|
|
9
|
+
readonly commentPublished: "community_comment_published";
|
|
10
|
+
readonly profileOpened: "community_profile_opened";
|
|
11
|
+
readonly profileUpdated: "community_profile_updated";
|
|
12
|
+
readonly reactionAdded: "community_reaction_added";
|
|
13
|
+
readonly userReported: "user_reported";
|
|
14
|
+
readonly userBlocked: "user_blocked";
|
|
15
|
+
readonly contentDeleted: "content_deleted";
|
|
16
|
+
readonly inboxOpened: "notification_center_opened";
|
|
17
|
+
};
|
|
18
|
+
export type CommunityEventName = (typeof COMMUNITY_EVENTS)[keyof typeof COMMUNITY_EVENTS];
|
|
19
|
+
/** Forwards to the host's onEvent adapter. Never throws — a bad adapter must never break the UI. */
|
|
20
|
+
export declare function emitEvent(cfg: ResolvedCommunityConfig, name: CommunityEventName, props?: Record<string, unknown>): void;
|
package/lib/events.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COMMUNITY_EVENTS = void 0;
|
|
4
|
+
exports.emitEvent = emitEvent;
|
|
5
|
+
// ⭐ Analytics seam — community code emits every product event through
|
|
6
|
+
// emitEvent() and nothing else. The host wires cfg.host.onEvent to its own
|
|
7
|
+
// tracking plan; left as a no-op by default so the community works before
|
|
8
|
+
// analytics are wired.
|
|
9
|
+
exports.COMMUNITY_EVENTS = {
|
|
10
|
+
opened: "community_opened",
|
|
11
|
+
rulesAccepted: "community_rules_accepted",
|
|
12
|
+
postPublished: "community_post_published",
|
|
13
|
+
pollVoted: "community_poll_voted",
|
|
14
|
+
threadOpened: "community_thread_opened",
|
|
15
|
+
postLiked: "community_post_liked",
|
|
16
|
+
commentPublished: "community_comment_published",
|
|
17
|
+
profileOpened: "community_profile_opened",
|
|
18
|
+
profileUpdated: "community_profile_updated",
|
|
19
|
+
reactionAdded: "community_reaction_added",
|
|
20
|
+
userReported: "user_reported",
|
|
21
|
+
userBlocked: "user_blocked",
|
|
22
|
+
contentDeleted: "content_deleted",
|
|
23
|
+
inboxOpened: "notification_center_opened",
|
|
24
|
+
};
|
|
25
|
+
/** Forwards to the host's onEvent adapter. Never throws — a bad adapter must never break the UI. */
|
|
26
|
+
function emitEvent(cfg, name, props) {
|
|
27
|
+
try {
|
|
28
|
+
cfg.host.onEvent(name, props ?? {});
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// swallow — analytics must never break the app
|
|
32
|
+
}
|
|
33
|
+
}
|
package/lib/hooks.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type InfiniteData } from "@tanstack/react-query";
|
|
2
|
+
import { type CommunityProfile, type FeedPost, type ThreadComment } from "./models";
|
|
3
|
+
import { type ModerationVerdict, type ReportInput, type UpdateProfileInput, type UpdateProfileResult } from "./service";
|
|
4
|
+
type PostCacheEntry = [readonly unknown[], InfiniteData<FeedPost[]> | undefined];
|
|
5
|
+
/**
|
|
6
|
+
* Snapshot of every post-holding cache, for optimistic-update rollback on
|
|
7
|
+
* error. The feed, user-posts and search caches are all `InfiniteData` now
|
|
8
|
+
* that user-posts and search are paginated like the feed — one flat list of
|
|
9
|
+
* entries instead of the feed/flat split this needed before.
|
|
10
|
+
*/
|
|
11
|
+
interface PostCacheSnapshot {
|
|
12
|
+
entries: PostCacheEntry[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One infinite-scrolling feed, newest (then pinned) first. `enabled` is kept
|
|
16
|
+
* beyond the brief's literal `(topic?)` signature (both source apps take it,
|
|
17
|
+
* e.g. to pause the feed query while an in-progress search query is active) —
|
|
18
|
+
* defaults to `true` so `useCommunityFeed(topic)` still matches the brief.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useCommunityFeed(topic?: string, enabled?: boolean): import("@tanstack/react-query").UseInfiniteQueryResult<InfiniteData<FeedPost[], unknown>, Error>;
|
|
21
|
+
/**
|
|
22
|
+
* Search visible posts by content, infinite-scrolling like the feed.
|
|
23
|
+
* Enabled once the term has >= 2 chars.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useSearchPosts(term: string): import("@tanstack/react-query").UseInfiniteQueryResult<InfiniteData<FeedPost[], unknown>, Error>;
|
|
26
|
+
/**
|
|
27
|
+
* Polls (5s, while `polling`) for posts newer than the loaded top. `topic`
|
|
28
|
+
* and `polling` are kept beyond the brief's literal `(sinceIso)` signature
|
|
29
|
+
* (both source apps take them, needed for a topic-filtered "N new posts" pill —
|
|
30
|
+
* see Task 5's same call on `countNewPosts`) — both default so
|
|
31
|
+
* `useNewPostsCount(sinceIso)` still matches the brief.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useNewPostsCount(sinceIso: string | null, topic?: string, polling?: boolean): number;
|
|
34
|
+
/**
|
|
35
|
+
* Number of posts by others since the community was last opened (0 while
|
|
36
|
+
* loading/failing) — feeds a host's Community tab badge. Takes `lastSeenAtIso`
|
|
37
|
+
* as a parameter instead of reading a host's "last seen" store directly (the
|
|
38
|
+
* mold read a host-seam function, the newer host read its own store — this
|
|
39
|
+
* package has no store access at all, so the host now owns that value and
|
|
40
|
+
* passes it in).
|
|
41
|
+
*/
|
|
42
|
+
export declare function useCommunityUnseenCount(lastSeenAtIso: string | null): number;
|
|
43
|
+
export declare function useThread(postId: string | null): import("@tanstack/react-query").UseQueryResult<ThreadComment[], Error>;
|
|
44
|
+
export declare function useProfile(userId: string | null): import("@tanstack/react-query").UseQueryResult<CommunityProfile, Error>;
|
|
45
|
+
/**
|
|
46
|
+
* One user's posts, newest first, infinite-scrolling like the feed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function useUserPosts(userId: string | null): import("@tanstack/react-query").UseInfiniteQueryResult<InfiniteData<FeedPost[], unknown>, Error>;
|
|
49
|
+
/** Our own stable community uid, resolved once and cached for the app session. */
|
|
50
|
+
export declare function useMyUid(): string | null;
|
|
51
|
+
/** Profile edits refresh everything derived from a profile row: the profile
|
|
52
|
+
* itself, plus the handle/avatar shown on every cached feed and thread. */
|
|
53
|
+
export declare function useUpdateProfile(): import("@tanstack/react-query").UseMutationResult<UpdateProfileResult, Error, UpdateProfileInput, unknown>;
|
|
54
|
+
type PostResult = {
|
|
55
|
+
id: string;
|
|
56
|
+
verdict: ModerationVerdict;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Optimistic post creation with NO refetch: the post shows instantly, and after
|
|
60
|
+
* moderation we reconcile the cache in place, published -> swap the temp id for
|
|
61
|
+
* the real one (stays), rejected -> remove it. Smooth, no loading flicker.
|
|
62
|
+
*/
|
|
63
|
+
export declare function useCreatePost(): import("@tanstack/react-query").UseMutationResult<PostResult, Error, {
|
|
64
|
+
topic: string;
|
|
65
|
+
text: string;
|
|
66
|
+
authorName: string;
|
|
67
|
+
pollOptions?: string[];
|
|
68
|
+
}, {
|
|
69
|
+
tempId: string;
|
|
70
|
+
}>;
|
|
71
|
+
/** Optimistic comment creation, same reconcile-in-place model as posts. */
|
|
72
|
+
export declare function useCreateComment(): import("@tanstack/react-query").UseMutationResult<PostResult, Error, {
|
|
73
|
+
postId: string;
|
|
74
|
+
text: string;
|
|
75
|
+
authorName: string;
|
|
76
|
+
}, {
|
|
77
|
+
tempId: string;
|
|
78
|
+
}>;
|
|
79
|
+
/** Optimistic like toggle across all cached feed pages (every topic filter). */
|
|
80
|
+
export declare function useToggleLike(): import("@tanstack/react-query").UseMutationResult<void, Error, {
|
|
81
|
+
postId: string;
|
|
82
|
+
liked: boolean;
|
|
83
|
+
topic: string | null;
|
|
84
|
+
}, {
|
|
85
|
+
previous: [readonly unknown[], InfiniteData<FeedPost[], unknown> | undefined][];
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* Optimistic poll vote across every post cache (mirror of useToggleLike, but
|
|
89
|
+
* reaching the user-posts/search caches too — see `applyOptimisticToAllPostCaches`).
|
|
90
|
+
* `optionId` is the poll option's row id (Task 5 ruling), not a numeric index —
|
|
91
|
+
* the brief's `optionIndex` naming is dropped since neither `votePoll` nor
|
|
92
|
+
* `applyPollVote` operate on one.
|
|
93
|
+
*/
|
|
94
|
+
export declare function useVotePoll(): import("@tanstack/react-query").UseMutationResult<void, Error, {
|
|
95
|
+
postId: string;
|
|
96
|
+
optionId: string;
|
|
97
|
+
}, PostCacheSnapshot>;
|
|
98
|
+
/**
|
|
99
|
+
* Optimistic reaction across every post cache (mirror of useVotePoll; renamed
|
|
100
|
+
* from the mold's `usePrayForPost`). `applyReaction(post)` is single-argument
|
|
101
|
+
* (Task 3 ruling — it does not filter by post id), so the mapper here pre-filters
|
|
102
|
+
* to the target post itself before calling it, and separately tracks whether
|
|
103
|
+
* that post already had `hasReacted` so a retap of an already-reacted post
|
|
104
|
+
* (the reaction is non-retractable, `setReaction` is idempotent) does not
|
|
105
|
+
* re-emit the analytics event.
|
|
106
|
+
*/
|
|
107
|
+
export declare function useReactToPost(): import("@tanstack/react-query").UseMutationResult<void, Error, {
|
|
108
|
+
postId: string;
|
|
109
|
+
}, PostCacheSnapshot & {
|
|
110
|
+
alreadyReacted: boolean;
|
|
111
|
+
}>;
|
|
112
|
+
export declare function useReport(): import("@tanstack/react-query").UseMutationResult<void, Error, ReportInput, unknown>;
|
|
113
|
+
/** Blocking re-filters everything: drop every community cache and refetch. */
|
|
114
|
+
export declare function useBlockUser(): import("@tanstack/react-query").UseMutationResult<void, Error, {
|
|
115
|
+
userId: string;
|
|
116
|
+
}, unknown>;
|
|
117
|
+
export declare function useDeleteContent(): import("@tanstack/react-query").UseMutationResult<void, Error, {
|
|
118
|
+
kind: "post" | "comment";
|
|
119
|
+
id: string;
|
|
120
|
+
postId: string;
|
|
121
|
+
}, unknown>;
|
|
122
|
+
export {};
|