@rocapine/community-core 0.11.0 → 0.12.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
@@ -24,9 +24,13 @@ const config: CommunityConfig = {
24
24
 
25
25
  // Product vocabulary
26
26
  appName: "My App", // pushes, inbox, broadcast fallback title
27
- // Shown when an author has no username yet. A function receives the host
28
- // locale (`host.getLocale()`), e.g. (locale) => t("anonymous", { lng: locale }).
29
- anonymousAuthorFallback: "Someone",
27
+ // Optional. Every profile starts as "<label> <digits>" / @<handle>-<digits>,
28
+ // assigned on the first launch in the user's language and never re-translated.
29
+ // `label` may be a function of the host locale; defaults to "User" / "user".
30
+ defaultName: {
31
+ label: (locale) => (locale.startsWith("fr") ? "Membre" : "Member"),
32
+ handle: "member",
33
+ },
30
34
 
31
35
  // Topics — nothing hardcoded. `id` resolves to `topics.<id>` in the UI
32
36
  // package's i18n catalog (see packages/ui/README.md); the app supplies
@@ -99,7 +103,7 @@ top-up), without forking the query or mapping layer:
99
103
  import type { CommunityConfig } from "@rocapine/community-core";
100
104
 
101
105
  const config: CommunityConfig = {
102
- // ...supabase, appName, anonymousAuthorFallback, topics, modules...
106
+ // ...supabase, appName, defaultName, topics, modules...
103
107
  feed: {
104
108
  // Appended to the posts `select` in every posts-table query
105
109
  // (fetchFeedPage, fetchUserPosts, searchPosts). Only `[a-z0-9_]+`
package/lib/config.d.ts CHANGED
@@ -60,11 +60,22 @@ export type CommunityFeedConfig = {
60
60
  * any `extraPostColumns`) it was built from. */
61
61
  transformPost?: (post: FeedPost, row: Record<string, unknown>) => FeedPost;
62
62
  };
63
+ /**
64
+ * Every profile starts as "<label> <digits>" / "@<handle>-<digits>", assigned
65
+ * once on the user's first launch and never re-translated afterwards (a name is
66
+ * an identity, not content). `label` may depend on the host locale ("Frère" /
67
+ * "Brother"); keep it a noun, at most 17 characters. `handle` is the ASCII
68
+ * handle prefix ([a-z0-9], at most 12 characters).
69
+ */
70
+ export type CommunityDefaultName = {
71
+ label: string | ((locale: string) => string);
72
+ handle: string;
73
+ };
63
74
  export type CommunityConfig = {
64
75
  supabase: SupabaseClient | null;
65
76
  appName: string;
66
- /** Shown for authors without a display name. A function receives the host locale. */
67
- anonymousAuthorFallback: string | ((locale: string) => string);
77
+ /** Defaults to `DEFAULT_NAME` ("User" / "user"). */
78
+ defaultName?: CommunityDefaultName;
68
79
  topics: CommunityTopicDef[];
69
80
  modules: CommunityModules;
70
81
  host?: CommunityHostAdapters;
@@ -78,10 +89,14 @@ export type ResolvedCommunityConfig = CommunityConfig & {
78
89
  host: RequiredCommunityHostAdapters;
79
90
  feed: CommunityFeedConfig;
80
91
  requireClient(): SupabaseClient;
81
- /** `anonymousAuthorFallback` resolved against `host.getLocale()`. */
92
+ /** `defaultName` resolved against the host locale. */
93
+ defaultName: CommunityDefaultName;
94
+ /** `defaultName.label` resolved against `host.getLocale()`: the name shown for a
95
+ * profile that has none yet (one that has not relaunched since defaults existed). */
82
96
  authorFallback(): string;
83
97
  composeTopics(): CommunityTopicDef[];
84
98
  isOfficialTopic(id: string): boolean;
85
99
  };
100
+ export declare const DEFAULT_NAME: CommunityDefaultName;
86
101
  export declare function resolveConfig(config: CommunityConfig): ResolvedCommunityConfig;
87
102
  export {};
package/lib/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CommunityDisabledError = void 0;
3
+ exports.DEFAULT_NAME = exports.CommunityDisabledError = void 0;
4
4
  exports.resolveConfig = resolveConfig;
5
5
  class CommunityDisabledError extends Error {
6
6
  constructor() {
@@ -9,6 +9,7 @@ class CommunityDisabledError extends Error {
9
9
  }
10
10
  }
11
11
  exports.CommunityDisabledError = CommunityDisabledError;
12
+ exports.DEFAULT_NAME = { label: "User", handle: "user" };
12
13
  let warned = false;
13
14
  const defaultHost = {
14
15
  getDisplayName: () => null,
@@ -30,6 +31,7 @@ function resolveConfig(config) {
30
31
  ...config,
31
32
  host,
32
33
  feed: config.feed ?? {},
34
+ defaultName: config.defaultName ?? exports.DEFAULT_NAME,
33
35
  requireClient() {
34
36
  if (!resolved.supabase) {
35
37
  if (!warned) {
@@ -41,8 +43,8 @@ function resolveConfig(config) {
41
43
  return resolved.supabase;
42
44
  },
43
45
  authorFallback() {
44
- const fallback = resolved.anonymousAuthorFallback;
45
- return typeof fallback === "function" ? fallback(host.getLocale()) : fallback;
46
+ const label = resolved.defaultName.label;
47
+ return typeof label === "function" ? label(host.getLocale()) : label;
46
48
  },
47
49
  composeTopics() {
48
50
  return resolved.topics.filter((topic) => !topic.officialOnly);
package/lib/identity.d.ts CHANGED
@@ -13,6 +13,12 @@ export declare function ensureIdentity(cfg: ResolvedCommunityConfig): Promise<st
13
13
  * fresh anonymous identity.
14
14
  */
15
15
  export declare function resetIdentity(): void;
16
+ /**
17
+ * Account deletion: put the default name and handle back and clear bio,
18
+ * avatar and the user's own edits, so kept posts no longer carry the
19
+ * pseudonym. Resolves false when the backend could not be reached.
20
+ */
21
+ export declare function resetProfileIdentity(cfg: ResolvedCommunityConfig): Promise<boolean>;
16
22
  /**
17
23
  * Mirror the host's display name, locale and analytics ids onto the
18
24
  * profiles row. Safe to call on every app open / community open: each field
package/lib/identity.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ensureIdentity = ensureIdentity;
4
4
  exports.resetIdentity = resetIdentity;
5
+ exports.resetProfileIdentity = resetProfileIdentity;
5
6
  exports.syncProfileFromHost = syncProfileFromHost;
6
7
  // One anonymous Supabase identity per install. signInAnonymously() runs once;
7
8
  // afterwards getSession() restores the same user (and uuid) from AsyncStorage.
@@ -41,8 +42,10 @@ function resetIdentity() {
41
42
  async function bootstrap(cfg) {
42
43
  const client = cfg.requireClient();
43
44
  const { data } = await client.auth.getSession();
44
- if (data.session)
45
+ if (data.session) {
46
+ await assignDefaultProfile(cfg);
45
47
  return data.session.user.id;
48
+ }
46
49
  const { data: signIn, error } = await client.auth.signInAnonymously();
47
50
  if (error || !signIn.user)
48
51
  throw error ?? new Error("anonymous sign-in returned no user");
@@ -54,8 +57,37 @@ async function bootstrap(cfg) {
54
57
  .from("profiles")
55
58
  .update({ amplitude_id: amplitudeId ?? null, revenuecat_id: revenuecatId ?? null })
56
59
  .eq("id", uid);
60
+ await assignDefaultProfile(cfg);
57
61
  return uid;
58
62
  }
63
+ /**
64
+ * Give the profile its default name and handle ("Frère 4821" / "@brother-4821")
65
+ * when it has none yet — once per launch, idempotent server-side (core/009).
66
+ * Best effort: an older backend without the RPC keeps the label-only fallback.
67
+ */
68
+ async function assignDefaultProfile(cfg) {
69
+ const { label, handle } = cfg.defaultName;
70
+ try {
71
+ await cfg.requireClient().rpc("assign_default_profile", {
72
+ p_label: typeof label === "function" ? label(cfg.host.getLocale()) : label,
73
+ p_handle: handle,
74
+ });
75
+ }
76
+ catch {
77
+ // Offline or older backend: retried on the next launch.
78
+ }
79
+ }
80
+ /**
81
+ * Account deletion: put the default name and handle back and clear bio,
82
+ * avatar and the user's own edits, so kept posts no longer carry the
83
+ * pseudonym. Resolves false when the backend could not be reached.
84
+ */
85
+ async function resetProfileIdentity(cfg) {
86
+ if (!cfg.supabase)
87
+ return false;
88
+ const { error } = await cfg.supabase.rpc("reset_profile_identity");
89
+ return !error;
90
+ }
59
91
  /**
60
92
  * Mirror the host's display name, locale and analytics ids onto the
61
93
  * profiles row. Safe to call on every app open / community open: each field
package/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { CommunityTopicDef, CommunityModules, CommunityHostAdapters, CommunityFeedConfig, CommunityConfig, ResolvedCommunityConfig, } from "./config";
2
- export { CommunityDisabledError, resolveConfig } from "./config";
1
+ export type { CommunityTopicDef, CommunityModules, CommunityHostAdapters, CommunityFeedConfig, CommunityConfig, CommunityDefaultName, ResolvedCommunityConfig, } from "./config";
2
+ export { CommunityDisabledError, DEFAULT_NAME, resolveConfig } from "./config";
3
3
  export { readerLocale, languageOf } from "./locale";
4
4
  export { CommunityProvider, useCommunityConfig } from "./provider";
5
5
  export type { PostRow, PollOptionRow, CommentRow, ProfileRow, PollOption, FeedPoll, PollData, ReactionData, FeedPost, ThreadComment, PostImage, LocalImage, CommunityProfile, TranslationInfo, TranslationRowEmbed, } from "./models";
@@ -8,7 +8,7 @@ export type { TimeAgoParts } from "./time";
8
8
  export { timeAgo, timeAgoParts } from "./time";
9
9
  export { COMMUNITY_EVENTS, emitEvent } from "./events";
10
10
  export type { CommunityEventName } from "./events";
11
- export { ensureIdentity, resetIdentity, syncProfileFromHost } from "./identity";
11
+ export { ensureIdentity, resetIdentity, resetProfileIdentity, syncProfileFromHost, } from "./identity";
12
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, uploadPostImage, POST_IMAGES_BUCKET, } from "./service";
13
13
  export type { ModerationVerdict, CreatePostInput, UploadedImage, ReportReason, ReportInput, UpdateProfileInput, UpdateProfileResult, NotificationPreferenceKey, NotificationPreferences, } from "./service";
14
14
  export { useCommunityFeed, useSearchPosts, useNewPostsCount, useCommunityUnseenCount, useThread, useProfile, useUserPosts, useMyUid, useNotificationPreferences, useUpdateProfile, useCreatePost, useCreateComment, useToggleLike, useVotePoll, useReactToPost, useReport, useBlockUser, useDeleteContent, isOptimistic, } from "./hooks";
package/lib/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- 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.mapImage = 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.useNotificationPreferences = exports.useMyUid = exports.useUserPosts = exports.useProfile = exports.useThread = exports.useCommunityUnseenCount = exports.useNewPostsCount = exports.useSearchPosts = exports.useCommunityFeed = exports.POST_IMAGES_BUCKET = exports.uploadPostImage = exports.uploadAvatar = exports.NOTIFICATION_PREFERENCE_KEYS = exports.setNotificationPreference = exports.fetchNotificationPreferences = exports.updateProfile = exports.deleteOwnComment = void 0;
3
+ 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.resetProfileIdentity = 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.mapImage = 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.DEFAULT_NAME = 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.useNotificationPreferences = exports.useMyUid = exports.useUserPosts = exports.useProfile = exports.useThread = exports.useCommunityUnseenCount = exports.useNewPostsCount = exports.useSearchPosts = exports.useCommunityFeed = exports.POST_IMAGES_BUCKET = exports.uploadPostImage = exports.uploadAvatar = exports.NOTIFICATION_PREFERENCE_KEYS = exports.setNotificationPreference = exports.fetchNotificationPreferences = exports.updateProfile = exports.deleteOwnComment = exports.deleteOwnPost = exports.blockUser = void 0;
5
5
  var config_1 = require("./config");
6
6
  Object.defineProperty(exports, "CommunityDisabledError", { enumerable: true, get: function () { return config_1.CommunityDisabledError; } });
7
+ Object.defineProperty(exports, "DEFAULT_NAME", { enumerable: true, get: function () { return config_1.DEFAULT_NAME; } });
7
8
  Object.defineProperty(exports, "resolveConfig", { enumerable: true, get: function () { return config_1.resolveConfig; } });
8
9
  var locale_1 = require("./locale");
9
10
  Object.defineProperty(exports, "readerLocale", { enumerable: true, get: function () { return locale_1.readerLocale; } });
@@ -41,6 +42,7 @@ Object.defineProperty(exports, "emitEvent", { enumerable: true, get: function ()
41
42
  var identity_1 = require("./identity");
42
43
  Object.defineProperty(exports, "ensureIdentity", { enumerable: true, get: function () { return identity_1.ensureIdentity; } });
43
44
  Object.defineProperty(exports, "resetIdentity", { enumerable: true, get: function () { return identity_1.resetIdentity; } });
45
+ Object.defineProperty(exports, "resetProfileIdentity", { enumerable: true, get: function () { return identity_1.resetProfileIdentity; } });
44
46
  Object.defineProperty(exports, "syncProfileFromHost", { enumerable: true, get: function () { return identity_1.syncProfileFromHost; } });
45
47
  var service_1 = require("./service");
46
48
  Object.defineProperty(exports, "buildFeedSelect", { enumerable: true, get: function () { return service_1.buildFeedSelect; } });
package/lib/models.d.ts CHANGED
@@ -180,10 +180,13 @@ export interface ThreadComment {
180
180
  /**
181
181
  * Public display name, with a host-configured fallback for profiles not yet named.
182
182
  * Accepts either a raw username or any profile-shaped object carrying one (a row's
183
- * `.profiles`, a `ProfileRow`, or a previously-mapped `CommunityProfile`).
183
+ * `.profiles`, a `ProfileRow`, or a previously-mapped `CommunityProfile`). An unnamed
184
+ * profile that picked a handle shows the handle before the fallback (handles are only
185
+ * auto-assigned once a username exists, so this only kicks in for a chosen one).
184
186
  */
185
187
  export declare function displayName(profile: string | null | undefined | {
186
188
  username?: string | null;
189
+ handle?: string | null;
187
190
  }, fallback: string): string;
188
191
  /**
189
192
  * The topic column is app-defined free text. Unlike the mold (which dropped topics
package/lib/models.js CHANGED
@@ -42,12 +42,14 @@ exports.EMPTY_REACTION_DATA = {
42
42
  /**
43
43
  * Public display name, with a host-configured fallback for profiles not yet named.
44
44
  * Accepts either a raw username or any profile-shaped object carrying one (a row's
45
- * `.profiles`, a `ProfileRow`, or a previously-mapped `CommunityProfile`).
45
+ * `.profiles`, a `ProfileRow`, or a previously-mapped `CommunityProfile`). An unnamed
46
+ * profile that picked a handle shows the handle before the fallback (handles are only
47
+ * auto-assigned once a username exists, so this only kicks in for a chosen one).
46
48
  */
47
49
  function displayName(profile, fallback) {
48
- const raw = typeof profile === "string" || profile == null ? profile : profile.username;
49
- const trimmed = raw?.trim();
50
- return trimmed ? trimmed : fallback;
50
+ if (typeof profile === "string" || profile == null)
51
+ return profile?.trim() || fallback;
52
+ return profile.username?.trim() || profile.handle?.trim() || fallback;
51
53
  }
52
54
  /**
53
55
  * The topic column is app-defined free text. Unlike the mold (which dropped topics
package/lib/service.d.ts CHANGED
@@ -157,7 +157,9 @@ export type UpdateProfileResult = {
157
157
  reason?: string;
158
158
  } | {
159
159
  status: "error";
160
+ /** `handle_taken` carries a free `suggestion` ("superuser-4821") when one was found. */
160
161
  code?: "handle_taken";
162
+ suggestion?: string | null;
161
163
  error?: string;
162
164
  };
163
165
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocapine/community-core",
3
- "version": "0.11.0",
3
+ "version": "0.12.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": {