@rocapine/community-core 0.8.1 → 0.9.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/README.md CHANGED
@@ -175,7 +175,8 @@ runs in production and is silent in degraded mode.
175
175
  (pure helper behind `feed.extraPostColumns`, see below).
176
176
  - **React Query hooks** (`hooks.ts`): `useCommunityFeed`, `useSearchPosts`,
177
177
  `useNewPostsCount`, `useCommunityUnseenCount`, `useThread`, `useProfile`,
178
- `useUserPosts`, `useMyUid`, `useUpdateProfile`, `useCreatePost`,
178
+ `useUserPosts`, `useMyUid`, `useNotificationPreferences` (push module: the
179
+ caller's `notify_*` flags), `useUpdateProfile`, `useCreatePost`,
179
180
  `useCreateComment`, `useToggleLike`, `useVotePoll`, `useReactToPost`,
180
181
  `useReport`, `useBlockUser`, `useDeleteContent`.
181
182
  - **Inbox** (opt-in via `modules.inbox`): `fetchInbox`, `markInboxSeen`,
package/lib/hooks.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type InfiniteData, type QueryClient } from "@tanstack/react-query";
2
2
  import { type CommunityProfile, type FeedPost, type ThreadComment } from "./models";
3
- import { type ModerationVerdict, type ReportInput, type UpdateProfileInput, type UpdateProfileResult } from "./service";
3
+ import { type NotificationPreferenceKey, type NotificationPreferences, type ModerationVerdict, type ReportInput, type UpdateProfileInput, type UpdateProfileResult } from "./service";
4
4
  /** True while a post/comment is still the optimistic placeholder inserted by
5
5
  * `useCreatePost`/`useCreateComment` (its temp id is swapped for the real one
6
6
  * once moderation answers). Server actions keyed on the id (reactions, likes,
@@ -54,14 +54,23 @@ export declare function useNewPostsCount(sinceIso: string | null, topic?: string
54
54
  * package has no store access at all, so the host now owns that value and
55
55
  * passes it in).
56
56
  */
57
- export declare function useCommunityUnseenCount(lastSeenAtIso: string | null): number;
57
+ export declare function useCommunityUnseenCount(lastSeenAtIso: string | null, options?: {
58
+ enabled?: boolean;
59
+ }): number;
58
60
  export declare function useThread(postId: string | null): import("@tanstack/react-query").UseQueryResult<ThreadComment[], Error>;
59
61
  export declare function useProfile(userId: string | null): import("@tanstack/react-query").UseQueryResult<CommunityProfile, Error>;
60
62
  /**
61
63
  * One user's posts, newest first, infinite-scrolling like the feed.
62
64
  */
63
65
  export declare function useUserPosts(userId: string | null): import("@tanstack/react-query").UseInfiniteQueryResult<InfiniteData<FeedPost[], unknown>, Error>;
64
- /** Our own stable community uid, resolved once and cached for the app session. */
66
+ /** The caller's push preferences (push module). `preferences` is `null` until the
67
+ * device has a push row; `setPreference` updates one flag optimistically. */
68
+ export declare function useNotificationPreferences(): {
69
+ preferences: NotificationPreferences | null;
70
+ isLoading: boolean;
71
+ refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NotificationPreferences | null, Error>>;
72
+ setPreference: (key: NotificationPreferenceKey, enabled: boolean) => Promise<void>;
73
+ };
65
74
  export declare function useMyUid(): string | null;
66
75
  /** Profile edits refresh everything derived from a profile row: the profile
67
76
  * itself, plus the handle/avatar shown on every cached feed and thread. */
package/lib/hooks.js CHANGED
@@ -19,6 +19,7 @@ exports.useCommunityUnseenCount = useCommunityUnseenCount;
19
19
  exports.useThread = useThread;
20
20
  exports.useProfile = useProfile;
21
21
  exports.useUserPosts = useUserPosts;
22
+ exports.useNotificationPreferences = useNotificationPreferences;
22
23
  exports.useMyUid = useMyUid;
23
24
  exports.useUpdateProfile = useUpdateProfile;
24
25
  exports.useCreatePost = useCreatePost;
@@ -168,12 +169,13 @@ function useNewPostsCount(sinceIso, topic, polling = true) {
168
169
  * package has no store access at all, so the host now owns that value and
169
170
  * passes it in).
170
171
  */
171
- function useCommunityUnseenCount(lastSeenAtIso) {
172
+ function useCommunityUnseenCount(lastSeenAtIso, options = {}) {
172
173
  const cfg = (0, provider_1.useCommunityConfig)();
173
174
  const q = (0, react_query_1.useQuery)({
174
175
  queryKey: ["community", "unseen", lastSeenAtIso ?? "never"],
175
176
  queryFn: () => (0, service_1.countNewPosts)(cfg, lastSeenAtIso ?? "1970-01-01T00:00:00Z"),
176
- enabled: cfg.supabase !== null,
177
+ // `enabled: false` keeps a host's kill-switch arm from creating the identity or polling.
178
+ enabled: cfg.supabase !== null && options.enabled !== false,
177
179
  staleTime: 1000 * 60,
178
180
  refetchInterval: 1000 * 120,
179
181
  refetchIntervalInBackground: false,
@@ -219,6 +221,40 @@ function useUserPosts(userId) {
219
221
  });
220
222
  }
221
223
  /** Our own stable community uid, resolved once and cached for the app session. */
224
+ const NOTIFICATION_PREFS_KEY = ["community", "notificationPreferences"];
225
+ /** The caller's push preferences (push module). `preferences` is `null` until the
226
+ * device has a push row; `setPreference` updates one flag optimistically. */
227
+ function useNotificationPreferences() {
228
+ const cfg = (0, provider_1.useCommunityConfig)();
229
+ const queryClient = (0, react_query_1.useQueryClient)();
230
+ const q = (0, react_query_1.useQuery)({
231
+ queryKey: NOTIFICATION_PREFS_KEY,
232
+ queryFn: () => (0, service_1.fetchNotificationPreferences)(cfg),
233
+ enabled: cfg.supabase !== null && cfg.modules.push,
234
+ staleTime: 1000 * 60,
235
+ });
236
+ const mutation = (0, react_query_1.useMutation)({
237
+ mutationFn: ({ key, enabled }) => (0, service_1.setNotificationPreference)(cfg, key, enabled),
238
+ onMutate: async ({ key, enabled }) => {
239
+ await queryClient.cancelQueries({ queryKey: NOTIFICATION_PREFS_KEY });
240
+ const previous = queryClient.getQueryData(NOTIFICATION_PREFS_KEY);
241
+ if (previous)
242
+ queryClient.setQueryData(NOTIFICATION_PREFS_KEY, { ...previous, [key]: enabled });
243
+ return { previous };
244
+ },
245
+ onError: (_error, _vars, context) => {
246
+ if (context)
247
+ queryClient.setQueryData(NOTIFICATION_PREFS_KEY, context.previous);
248
+ },
249
+ onSettled: () => queryClient.invalidateQueries({ queryKey: NOTIFICATION_PREFS_KEY }),
250
+ });
251
+ return {
252
+ preferences: q.data ?? null,
253
+ isLoading: q.isLoading,
254
+ refetch: q.refetch,
255
+ setPreference: (key, enabled) => mutation.mutateAsync({ key, enabled }),
256
+ };
257
+ }
222
258
  function useMyUid() {
223
259
  const cfg = (0, provider_1.useCommunityConfig)();
224
260
  const q = (0, react_query_1.useQuery)({
package/lib/index.d.ts CHANGED
@@ -9,9 +9,9 @@ export { timeAgo, timeAgoParts } from "./time";
9
9
  export { COMMUNITY_EVENTS, emitEvent } from "./events";
10
10
  export type { CommunityEventName } from "./events";
11
11
  export { ensureIdentity, resetIdentity, syncProfileFromHost } from "./identity";
12
- export { buildFeedSelect, fetchFeedPage, countNewPosts, fetchProfile, fetchUserPosts, searchPosts, fetchThread, createPost, votePoll, createComment, moderateOne, setLike, setReaction, fetchReactionSummaries, reportContent, blockUser, deleteOwnPost, deleteOwnComment, updateProfile, uploadAvatar, } from "./service";
13
- export type { ModerationVerdict, CreatePostInput, ReportReason, ReportInput, UpdateProfileInput, UpdateProfileResult, } from "./service";
14
- export { useCommunityFeed, useSearchPosts, useNewPostsCount, useCommunityUnseenCount, useThread, useProfile, useUserPosts, useMyUid, useUpdateProfile, useCreatePost, useCreateComment, useToggleLike, useVotePoll, useReactToPost, useReport, useBlockUser, useDeleteContent, isOptimistic, } from "./hooks";
12
+ export { buildFeedSelect, fetchFeedPage, countNewPosts, fetchProfile, fetchUserPosts, searchPosts, fetchThread, createPost, votePoll, createComment, moderateOne, setLike, setReaction, fetchReactionSummaries, reportContent, blockUser, deleteOwnPost, deleteOwnComment, updateProfile, fetchNotificationPreferences, setNotificationPreference, NOTIFICATION_PREFERENCE_KEYS, uploadAvatar, } from "./service";
13
+ export type { ModerationVerdict, CreatePostInput, ReportReason, ReportInput, UpdateProfileInput, UpdateProfileResult, NotificationPreferenceKey, NotificationPreferences, } from "./service";
14
+ export { useCommunityFeed, useSearchPosts, useNewPostsCount, useCommunityUnseenCount, useThread, useProfile, useUserPosts, useMyUid, useNotificationPreferences, useUpdateProfile, useCreatePost, useCreateComment, useToggleLike, useVotePoll, useReactToPost, useReport, useBlockUser, useDeleteContent, isOptimistic, } from "./hooks";
15
15
  export { fetchInbox, localizeExcerpts, markInboxSeen, unreadCount } from "./inbox-service";
16
16
  export type { InboxItem, InboxState } from "./inbox-service";
17
17
  export { useNotificationInbox, useUnreadNotificationCount, useMarkInboxSeen } from "./inbox-hooks";
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.deleteOwnComment = exports.deleteOwnPost = exports.blockUser = exports.reportContent = exports.fetchReactionSummaries = exports.setReaction = exports.setLike = exports.moderateOne = exports.createComment = exports.votePoll = exports.createPost = exports.fetchThread = exports.searchPosts = exports.fetchUserPosts = exports.fetchProfile = exports.countNewPosts = exports.fetchFeedPage = exports.buildFeedSelect = exports.syncProfileFromHost = exports.resetIdentity = exports.ensureIdentity = exports.emitEvent = exports.COMMUNITY_EVENTS = exports.timeAgoParts = exports.timeAgo = exports.newestCreatedAt = exports.newPostsLabel = exports.mapCommentRow = exports.applyReaction = exports.applyPollVote = exports.mapPostRow = exports.pollPercent = exports.buildPoll = exports.mapProfileRow = exports.normalizeTopic = exports.displayName = exports.EMPTY_REACTION_DATA = exports.EMPTY_POLL_DATA = exports.POLL_OPTION_MAX_LENGTH = exports.POLL_MAX_OPTIONS = exports.POLL_MIN_OPTIONS = exports.COMMENT_MAX_LENGTH = exports.POST_MAX_LENGTH = exports.FEED_PAGE_SIZE = exports.useCommunityConfig = exports.CommunityProvider = exports.languageOf = exports.readerLocale = exports.resolveConfig = exports.CommunityDisabledError = void 0;
4
- exports.REQUIRED_SCHEMA_VERSION = exports.useMarkInboxSeen = exports.useUnreadNotificationCount = exports.useNotificationInbox = exports.unreadCount = exports.markInboxSeen = exports.localizeExcerpts = exports.fetchInbox = exports.isOptimistic = exports.useDeleteContent = exports.useBlockUser = exports.useReport = exports.useReactToPost = exports.useVotePoll = exports.useToggleLike = exports.useCreateComment = exports.useCreatePost = exports.useUpdateProfile = exports.useMyUid = exports.useUserPosts = exports.useProfile = exports.useThread = exports.useCommunityUnseenCount = exports.useNewPostsCount = exports.useSearchPosts = exports.useCommunityFeed = exports.uploadAvatar = exports.updateProfile = void 0;
4
+ exports.REQUIRED_SCHEMA_VERSION = exports.useMarkInboxSeen = exports.useUnreadNotificationCount = exports.useNotificationInbox = exports.unreadCount = exports.markInboxSeen = exports.localizeExcerpts = exports.fetchInbox = exports.isOptimistic = exports.useDeleteContent = exports.useBlockUser = exports.useReport = exports.useReactToPost = exports.useVotePoll = exports.useToggleLike = exports.useCreateComment = exports.useCreatePost = exports.useUpdateProfile = exports.useNotificationPreferences = exports.useMyUid = exports.useUserPosts = exports.useProfile = exports.useThread = exports.useCommunityUnseenCount = exports.useNewPostsCount = exports.useSearchPosts = exports.useCommunityFeed = exports.uploadAvatar = exports.NOTIFICATION_PREFERENCE_KEYS = exports.setNotificationPreference = exports.fetchNotificationPreferences = exports.updateProfile = void 0;
5
5
  var config_1 = require("./config");
6
6
  Object.defineProperty(exports, "CommunityDisabledError", { enumerable: true, get: function () { return config_1.CommunityDisabledError; } });
7
7
  Object.defineProperty(exports, "resolveConfig", { enumerable: true, get: function () { return config_1.resolveConfig; } });
@@ -61,6 +61,9 @@ Object.defineProperty(exports, "blockUser", { enumerable: true, get: function ()
61
61
  Object.defineProperty(exports, "deleteOwnPost", { enumerable: true, get: function () { return service_1.deleteOwnPost; } });
62
62
  Object.defineProperty(exports, "deleteOwnComment", { enumerable: true, get: function () { return service_1.deleteOwnComment; } });
63
63
  Object.defineProperty(exports, "updateProfile", { enumerable: true, get: function () { return service_1.updateProfile; } });
64
+ Object.defineProperty(exports, "fetchNotificationPreferences", { enumerable: true, get: function () { return service_1.fetchNotificationPreferences; } });
65
+ Object.defineProperty(exports, "setNotificationPreference", { enumerable: true, get: function () { return service_1.setNotificationPreference; } });
66
+ Object.defineProperty(exports, "NOTIFICATION_PREFERENCE_KEYS", { enumerable: true, get: function () { return service_1.NOTIFICATION_PREFERENCE_KEYS; } });
64
67
  Object.defineProperty(exports, "uploadAvatar", { enumerable: true, get: function () { return service_1.uploadAvatar; } });
65
68
  var hooks_1 = require("./hooks");
66
69
  Object.defineProperty(exports, "useCommunityFeed", { enumerable: true, get: function () { return hooks_1.useCommunityFeed; } });
@@ -71,6 +74,7 @@ Object.defineProperty(exports, "useThread", { enumerable: true, get: function ()
71
74
  Object.defineProperty(exports, "useProfile", { enumerable: true, get: function () { return hooks_1.useProfile; } });
72
75
  Object.defineProperty(exports, "useUserPosts", { enumerable: true, get: function () { return hooks_1.useUserPosts; } });
73
76
  Object.defineProperty(exports, "useMyUid", { enumerable: true, get: function () { return hooks_1.useMyUid; } });
77
+ Object.defineProperty(exports, "useNotificationPreferences", { enumerable: true, get: function () { return hooks_1.useNotificationPreferences; } });
74
78
  Object.defineProperty(exports, "useUpdateProfile", { enumerable: true, get: function () { return hooks_1.useUpdateProfile; } });
75
79
  Object.defineProperty(exports, "useCreatePost", { enumerable: true, get: function () { return hooks_1.useCreatePost; } });
76
80
  Object.defineProperty(exports, "useCreateComment", { enumerable: true, get: function () { return hooks_1.useCreateComment; } });
package/lib/service.d.ts CHANGED
@@ -116,6 +116,12 @@ export interface ReportInput {
116
116
  }
117
117
  export declare function reportContent(cfg: ResolvedCommunityConfig, input: ReportInput): Promise<void>;
118
118
  export declare function blockUser(cfg: ResolvedCommunityConfig, blockedId: string): Promise<void>;
119
+ export declare const NOTIFICATION_PREFERENCE_KEYS: readonly ["notify_likes", "notify_comments", "notify_reactions"];
120
+ export type NotificationPreferenceKey = (typeof NOTIFICATION_PREFERENCE_KEYS)[number];
121
+ export type NotificationPreferences = Record<NotificationPreferenceKey, boolean>;
122
+ /** The caller's `push_tokens` preferences (push module); `null` while the device has no push row. */
123
+ export declare function fetchNotificationPreferences(cfg: ResolvedCommunityConfig): Promise<NotificationPreferences | null>;
124
+ export declare function setNotificationPreference(cfg: ResolvedCommunityConfig, key: NotificationPreferenceKey, enabled: boolean): Promise<void>;
119
125
  export declare function deleteOwnPost(cfg: ResolvedCommunityConfig, postId: string): Promise<void>;
120
126
  export declare function deleteOwnComment(cfg: ResolvedCommunityConfig, commentId: string): Promise<void>;
121
127
  /**
package/lib/service.js CHANGED
@@ -8,6 +8,7 @@
8
8
  // feed/thread composition (search, poll batching via `poll_vote_counts`, reaction batching
9
9
  // generalized from that host's prayer-style feature's summary RPC into `post_reaction_summary`).
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.NOTIFICATION_PREFERENCE_KEYS = void 0;
11
12
  exports.buildFeedSelect = buildFeedSelect;
12
13
  exports.withTranslationFilters = withTranslationFilters;
13
14
  exports.fetchReactionSummaries = fetchReactionSummaries;
@@ -26,6 +27,8 @@ exports.setLike = setLike;
26
27
  exports.setReaction = setReaction;
27
28
  exports.reportContent = reportContent;
28
29
  exports.blockUser = blockUser;
30
+ exports.fetchNotificationPreferences = fetchNotificationPreferences;
31
+ exports.setNotificationPreference = setNotificationPreference;
29
32
  exports.deleteOwnPost = deleteOwnPost;
30
33
  exports.deleteOwnComment = deleteOwnComment;
31
34
  exports.updateProfile = updateProfile;
@@ -450,6 +453,34 @@ async function blockUser(cfg, blockedId) {
450
453
  if (error && error.code !== "23505")
451
454
  throw error; // already blocked is fine
452
455
  }
456
+ exports.NOTIFICATION_PREFERENCE_KEYS = [
457
+ "notify_likes",
458
+ "notify_comments",
459
+ "notify_reactions",
460
+ ];
461
+ /** The caller's `push_tokens` preferences (push module); `null` while the device has no push row. */
462
+ async function fetchNotificationPreferences(cfg) {
463
+ const client = cfg.requireClient();
464
+ const uid = await requireUid(cfg);
465
+ const { data, error } = await client
466
+ .from("push_tokens")
467
+ .select(exports.NOTIFICATION_PREFERENCE_KEYS.join(","))
468
+ .eq("user_id", uid)
469
+ .maybeSingle();
470
+ if (error)
471
+ throw error;
472
+ return data ?? null;
473
+ }
474
+ async function setNotificationPreference(cfg, key, enabled) {
475
+ const client = cfg.requireClient();
476
+ const uid = await requireUid(cfg);
477
+ const { error } = await client
478
+ .from("push_tokens")
479
+ .update({ [key]: enabled })
480
+ .eq("user_id", uid);
481
+ if (error)
482
+ throw error;
483
+ }
453
484
  async function deleteOwnPost(cfg, postId) {
454
485
  const client = cfg.requireClient();
455
486
  await requireUid(cfg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocapine/community-core",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Headless Supabase-backed community backend: identity, feed/thread/moderation services, and React Query hooks.",
5
5
  "license": "MIT",
6
6
  "repository": {