@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 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.
@@ -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
+ }
@@ -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 {};