@rocapine/community-core 0.2.0 → 0.3.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 +19 -4
- package/lib/config.d.ts +6 -0
- package/lib/events.d.ts +1 -0
- package/lib/events.js +1 -0
- package/lib/hooks.js +30 -15
- package/lib/inbox-service.d.ts +5 -0
- package/lib/inbox-service.js +31 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +6 -2
- package/lib/locale.d.ts +8 -0
- package/lib/locale.js +22 -0
- package/lib/models.d.ts +29 -0
- package/lib/models.js +12 -1
- package/lib/service.d.ts +2 -2
- package/lib/service.js +50 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,10 @@ const config: CommunityConfig = {
|
|
|
38
38
|
push: false,
|
|
39
39
|
inbox: true,
|
|
40
40
|
reaction: { key: "cheer" }, // or `false` to disable the reaction module
|
|
41
|
+
// posts/comments/poll labels translated at publication into these
|
|
42
|
+
// locales — must equal the backend's COMMUNITY_TRANSLATION_LOCALES.
|
|
43
|
+
// Absent or `false` ⇒ every query and screen behaves as before.
|
|
44
|
+
translation: { locales: ["en", "es-419"] },
|
|
41
45
|
},
|
|
42
46
|
|
|
43
47
|
// Host adapters — every field optional, no-op defaults.
|
|
@@ -128,7 +132,8 @@ import { COMMUNITY_EVENTS, type CommunityEventName } from "@rocapine/community-c
|
|
|
128
132
|
`COMMUNITY_EVENTS` is a frozen map of stable event-name strings — `opened`,
|
|
129
133
|
`rulesAccepted`, `postPublished`, `pollVoted`, `threadOpened`, `postLiked`,
|
|
130
134
|
`commentPublished`, `profileOpened`, `profileUpdated`, `reactionAdded`,
|
|
131
|
-
`userReported`, `userBlocked`, `contentDeleted`, `inboxOpened
|
|
135
|
+
`userReported`, `userBlocked`, `contentDeleted`, `inboxOpened`,
|
|
136
|
+
`translationToggled` (`community_translation_toggled`, props `{ postId | commentId, to: "original" | "translation" }`) — forwarded to
|
|
132
137
|
`host.onEvent` by the exported `emitEvent(cfg, name, props)` helper. Route or
|
|
133
138
|
ignore these in your own tracking plan; nothing else in the package calls an
|
|
134
139
|
analytics provider directly.
|
|
@@ -148,7 +153,16 @@ runs in production and is silent in degraded mode.
|
|
|
148
153
|
`ReactionData`, and DB-mirroring constants `FEED_PAGE_SIZE`,
|
|
149
154
|
`POST_MAX_LENGTH` (2000), `COMMENT_MAX_LENGTH` (1000), `POLL_MIN_OPTIONS`
|
|
150
155
|
(2), `POLL_MAX_OPTIONS` (4), `POLL_OPTION_MAX_LENGTH` (60) — client-side
|
|
151
|
-
mirrors of the backend's check constraints.
|
|
156
|
+
mirrors of the backend's check constraints. `FeedPost.translation` /
|
|
157
|
+
`ThreadComment.translation` are `{ text, sourceLocale } | null` (embedded
|
|
158
|
+
only when the translation module is on and the reader locale resolves);
|
|
159
|
+
`text`/`content` always stay the original. Poll options carry
|
|
160
|
+
`translatedLabel: string | null` alongside the original `label`.
|
|
161
|
+
- **Locale** (`locale.ts`): `readerLocale(cfg)` — the locale translations
|
|
162
|
+
are read in (the host's `getLocale()` resolved against
|
|
163
|
+
`modules.translation.locales`), or `null` when the module is off or
|
|
164
|
+
nothing resolves. `languageOf(locale)` — the bare-language prefix of a
|
|
165
|
+
locale tag (e.g. `"es-419"` → `"es"`).
|
|
152
166
|
- **Identity**: `ensureIdentity()`, `resetIdentity()`, `syncProfileFromHost()`
|
|
153
167
|
— anonymous Supabase auth session management.
|
|
154
168
|
- **Queries** (`service.ts`): `fetchFeedPage`, `countNewPosts`, `fetchProfile`,
|
|
@@ -163,8 +177,9 @@ runs in production and is silent in degraded mode.
|
|
|
163
177
|
`useCreateComment`, `useToggleLike`, `useVotePoll`, `useReactToPost`,
|
|
164
178
|
`useReport`, `useBlockUser`, `useDeleteContent`.
|
|
165
179
|
- **Inbox** (opt-in via `modules.inbox`): `fetchInbox`, `markInboxSeen`,
|
|
166
|
-
`unreadCount`,
|
|
167
|
-
|
|
180
|
+
`unreadCount`, `localizeExcerpts` (swaps a notification's excerpt for its
|
|
181
|
+
reader-locale translation when one is available), and hooks
|
|
182
|
+
`useNotificationInbox`, `useUnreadNotificationCount`, `useMarkInboxSeen`.
|
|
168
183
|
- **`timeAgo(iso, nowMs)`** — hardcoded-English relative-time formatting, kept for
|
|
169
184
|
back-compat. **`timeAgoParts(iso, nowMs)`** — the language-free unit+value
|
|
170
185
|
decomposition the UI package actually localizes through its own catalog
|
package/lib/config.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export type CommunityModules = {
|
|
|
11
11
|
reaction: {
|
|
12
12
|
key: string;
|
|
13
13
|
} | false;
|
|
14
|
+
/** Translation module: posts/comments/poll labels translated at publication
|
|
15
|
+
* into these locales (must equal the backend's COMMUNITY_TRANSLATION_LOCALES
|
|
16
|
+
* secret). Absent or false ⇒ every query and screen behaves as before. */
|
|
17
|
+
translation?: {
|
|
18
|
+
locales: string[];
|
|
19
|
+
} | false;
|
|
14
20
|
};
|
|
15
21
|
export type CommunityHostAdapters = {
|
|
16
22
|
getDisplayName?: () => string | null;
|
package/lib/events.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare const COMMUNITY_EVENTS: {
|
|
|
14
14
|
readonly userBlocked: "user_blocked";
|
|
15
15
|
readonly contentDeleted: "content_deleted";
|
|
16
16
|
readonly inboxOpened: "notification_center_opened";
|
|
17
|
+
readonly translationToggled: "community_translation_toggled";
|
|
17
18
|
};
|
|
18
19
|
export type CommunityEventName = (typeof COMMUNITY_EVENTS)[keyof typeof COMMUNITY_EVENTS];
|
|
19
20
|
/** Forwards to the host's onEvent adapter. Never throws — a bad adapter must never break the UI. */
|
package/lib/events.js
CHANGED
|
@@ -21,6 +21,7 @@ exports.COMMUNITY_EVENTS = {
|
|
|
21
21
|
userBlocked: "user_blocked",
|
|
22
22
|
contentDeleted: "content_deleted",
|
|
23
23
|
inboxOpened: "notification_center_opened",
|
|
24
|
+
translationToggled: "community_translation_toggled",
|
|
24
25
|
};
|
|
25
26
|
/** Forwards to the host's onEvent adapter. Never throws — a bad adapter must never break the UI. */
|
|
26
27
|
function emitEvent(cfg, name, props) {
|
package/lib/hooks.js
CHANGED
|
@@ -30,15 +30,16 @@ exports.useDeleteContent = useDeleteContent;
|
|
|
30
30
|
const react_query_1 = require("@tanstack/react-query");
|
|
31
31
|
const events_1 = require("./events");
|
|
32
32
|
const identity_1 = require("./identity");
|
|
33
|
+
const locale_1 = require("./locale");
|
|
33
34
|
const models_1 = require("./models");
|
|
34
35
|
const provider_1 = require("./provider");
|
|
35
36
|
const service_1 = require("./service");
|
|
36
37
|
const FEED_KEY = ["community", "feed"];
|
|
37
|
-
const feedKey = (topic) => [...FEED_KEY, topic ?? "all"];
|
|
38
|
-
const threadKey = (postId) => ["community", "thread", postId];
|
|
38
|
+
const feedKey = (topic, locale) => [...FEED_KEY, topic ?? "all", locale ?? "src"];
|
|
39
|
+
const threadKey = (postId, locale) => ["community", "thread", postId, locale ?? "src"];
|
|
39
40
|
const profileKey = (userId) => ["community", "profile", userId];
|
|
40
|
-
const userPostsKey = (userId) => ["community", "userPosts", userId];
|
|
41
41
|
const USER_POSTS_KEY = ["community", "userPosts"];
|
|
42
|
+
const userPostsKey = (userId, locale) => [...USER_POSTS_KEY, userId, locale ?? "src"];
|
|
42
43
|
const SEARCH_KEY = ["community", "search"];
|
|
43
44
|
/**
|
|
44
45
|
* Adjust a post's commentCount across every cached feed page (all topic
|
|
@@ -94,8 +95,9 @@ function rollbackPostCaches(queryClient, snapshot) {
|
|
|
94
95
|
*/
|
|
95
96
|
function useCommunityFeed(topic, enabled = true) {
|
|
96
97
|
const cfg = (0, provider_1.useCommunityConfig)();
|
|
98
|
+
const locale = (0, locale_1.readerLocale)(cfg);
|
|
97
99
|
return (0, react_query_1.useInfiniteQuery)({
|
|
98
|
-
queryKey: feedKey(topic),
|
|
100
|
+
queryKey: feedKey(topic, locale),
|
|
99
101
|
queryFn: ({ pageParam }) => (0, service_1.fetchFeedPage)(cfg, { topic, cursor: pageParam }),
|
|
100
102
|
initialPageParam: 0,
|
|
101
103
|
getNextPageParam: (lastPage, pages) => lastPage.length === models_1.FEED_PAGE_SIZE ? pages.length : undefined,
|
|
@@ -111,8 +113,9 @@ function useCommunityFeed(topic, enabled = true) {
|
|
|
111
113
|
function useSearchPosts(term) {
|
|
112
114
|
const cfg = (0, provider_1.useCommunityConfig)();
|
|
113
115
|
const cleaned = term.trim();
|
|
116
|
+
const locale = (0, locale_1.readerLocale)(cfg);
|
|
114
117
|
return (0, react_query_1.useInfiniteQuery)({
|
|
115
|
-
queryKey: [...SEARCH_KEY, cleaned],
|
|
118
|
+
queryKey: [...SEARCH_KEY, cleaned, locale ?? "src"],
|
|
116
119
|
queryFn: ({ pageParam }) => (0, service_1.searchPosts)(cfg, cleaned, { cursor: pageParam }),
|
|
117
120
|
initialPageParam: 0,
|
|
118
121
|
getNextPageParam: (lastPage, pages) => lastPage.length === models_1.FEED_PAGE_SIZE ? pages.length : undefined,
|
|
@@ -163,8 +166,9 @@ function useCommunityUnseenCount(lastSeenAtIso) {
|
|
|
163
166
|
}
|
|
164
167
|
function useThread(postId) {
|
|
165
168
|
const cfg = (0, provider_1.useCommunityConfig)();
|
|
169
|
+
const locale = (0, locale_1.readerLocale)(cfg);
|
|
166
170
|
return (0, react_query_1.useQuery)({
|
|
167
|
-
queryKey: threadKey(postId ?? "none"),
|
|
171
|
+
queryKey: threadKey(postId ?? "none", locale),
|
|
168
172
|
queryFn: () => (0, service_1.fetchThread)(cfg, postId),
|
|
169
173
|
enabled: cfg.supabase !== null && !!postId,
|
|
170
174
|
staleTime: 1000 * 15,
|
|
@@ -186,8 +190,9 @@ function useProfile(userId) {
|
|
|
186
190
|
*/
|
|
187
191
|
function useUserPosts(userId) {
|
|
188
192
|
const cfg = (0, provider_1.useCommunityConfig)();
|
|
193
|
+
const locale = (0, locale_1.readerLocale)(cfg);
|
|
189
194
|
return (0, react_query_1.useInfiniteQuery)({
|
|
190
|
-
queryKey: userPostsKey(userId ?? "none"),
|
|
195
|
+
queryKey: userPostsKey(userId ?? "none", locale),
|
|
191
196
|
queryFn: ({ pageParam }) => (0, service_1.fetchUserPosts)(cfg, userId, { cursor: pageParam }),
|
|
192
197
|
initialPageParam: 0,
|
|
193
198
|
getNextPageParam: (lastPage, pages) => lastPage.length === models_1.FEED_PAGE_SIZE ? pages.length : undefined,
|
|
@@ -235,6 +240,7 @@ function useUpdateProfile() {
|
|
|
235
240
|
function useCreatePost() {
|
|
236
241
|
const cfg = (0, provider_1.useCommunityConfig)();
|
|
237
242
|
const queryClient = (0, react_query_1.useQueryClient)();
|
|
243
|
+
const locale = (0, locale_1.readerLocale)(cfg);
|
|
238
244
|
return (0, react_query_1.useMutation)({
|
|
239
245
|
onMutate: async ({ topic, text, authorName, pollOptions }) => {
|
|
240
246
|
await queryClient.cancelQueries({ queryKey: FEED_KEY });
|
|
@@ -259,7 +265,12 @@ function useCreatePost() {
|
|
|
259
265
|
// refetch below swaps in the real uuids.
|
|
260
266
|
poll: pollOptions && pollOptions.length > 0
|
|
261
267
|
? {
|
|
262
|
-
options: pollOptions.map((label, i) => ({
|
|
268
|
+
options: pollOptions.map((label, i) => ({
|
|
269
|
+
id: `temp-${i}`,
|
|
270
|
+
label,
|
|
271
|
+
translatedLabel: null,
|
|
272
|
+
votes: 0,
|
|
273
|
+
})),
|
|
263
274
|
myOptionId: null,
|
|
264
275
|
totalVotes: 0,
|
|
265
276
|
}
|
|
@@ -267,10 +278,11 @@ function useCreatePost() {
|
|
|
267
278
|
reactionCount: 0,
|
|
268
279
|
hasReacted: false,
|
|
269
280
|
lastReactorName: null,
|
|
281
|
+
translation: null,
|
|
270
282
|
};
|
|
271
283
|
const prepend = (key) => queryClient.setQueryData(key, (d) => d ? { ...d, pages: [[optimistic, ...(d.pages[0] ?? [])], ...d.pages.slice(1)] } : d);
|
|
272
|
-
prepend(feedKey(undefined));
|
|
273
|
-
prepend(feedKey(topic));
|
|
284
|
+
prepend(feedKey(undefined, locale));
|
|
285
|
+
prepend(feedKey(topic, locale));
|
|
274
286
|
return { tempId };
|
|
275
287
|
},
|
|
276
288
|
mutationFn: async ({ topic, text, pollOptions }) => {
|
|
@@ -313,9 +325,10 @@ function useCreatePost() {
|
|
|
313
325
|
function useCreateComment() {
|
|
314
326
|
const cfg = (0, provider_1.useCommunityConfig)();
|
|
315
327
|
const queryClient = (0, react_query_1.useQueryClient)();
|
|
328
|
+
const locale = (0, locale_1.readerLocale)(cfg);
|
|
316
329
|
return (0, react_query_1.useMutation)({
|
|
317
330
|
onMutate: async ({ postId, text, authorName }) => {
|
|
318
|
-
await queryClient.cancelQueries({ queryKey: threadKey(postId) });
|
|
331
|
+
await queryClient.cancelQueries({ queryKey: threadKey(postId, locale) });
|
|
319
332
|
await queryClient.cancelQueries({ queryKey: FEED_KEY });
|
|
320
333
|
const tempId = `optimistic-${Date.now()}`;
|
|
321
334
|
const optimistic = {
|
|
@@ -329,8 +342,9 @@ function useCreateComment() {
|
|
|
329
342
|
text,
|
|
330
343
|
isOwn: true,
|
|
331
344
|
createdAt: new Date().toISOString(),
|
|
345
|
+
translation: null,
|
|
332
346
|
};
|
|
333
|
-
queryClient.setQueryData(threadKey(postId), (d) => [
|
|
347
|
+
queryClient.setQueryData(threadKey(postId, locale), (d) => [
|
|
334
348
|
...(d ?? []),
|
|
335
349
|
optimistic,
|
|
336
350
|
]);
|
|
@@ -344,7 +358,7 @@ function useCreateComment() {
|
|
|
344
358
|
},
|
|
345
359
|
onSuccess: ({ id, verdict }, { postId, text }, { tempId }) => {
|
|
346
360
|
const rejected = verdict.status === "rejected";
|
|
347
|
-
queryClient.setQueryData(threadKey(postId), (d) => (d ?? [])
|
|
361
|
+
queryClient.setQueryData(threadKey(postId, locale), (d) => (d ?? [])
|
|
348
362
|
.filter((c) => !(rejected && c.id === tempId))
|
|
349
363
|
.map((c) => (c.id === tempId ? { ...c, id } : c)));
|
|
350
364
|
// A rejected comment never becomes visible, so undo the optimistic bump.
|
|
@@ -359,7 +373,7 @@ function useCreateComment() {
|
|
|
359
373
|
}
|
|
360
374
|
},
|
|
361
375
|
onError: (_e, { postId }, context) => {
|
|
362
|
-
queryClient.setQueryData(threadKey(postId), (d) => (d ?? []).filter((c) => c.id !== context?.tempId));
|
|
376
|
+
queryClient.setQueryData(threadKey(postId, locale), (d) => (d ?? []).filter((c) => c.id !== context?.tempId));
|
|
363
377
|
bumpFeedCommentCount(queryClient, postId, -1);
|
|
364
378
|
},
|
|
365
379
|
});
|
|
@@ -473,11 +487,12 @@ function useBlockUser() {
|
|
|
473
487
|
function useDeleteContent() {
|
|
474
488
|
const cfg = (0, provider_1.useCommunityConfig)();
|
|
475
489
|
const queryClient = (0, react_query_1.useQueryClient)();
|
|
490
|
+
const locale = (0, locale_1.readerLocale)(cfg);
|
|
476
491
|
return (0, react_query_1.useMutation)({
|
|
477
492
|
mutationFn: ({ kind, id }) => kind === "post" ? (0, service_1.deleteOwnPost)(cfg, id) : (0, service_1.deleteOwnComment)(cfg, id),
|
|
478
493
|
onSuccess: (_data, { kind, postId }) => {
|
|
479
494
|
(0, events_1.emitEvent)(cfg, events_1.COMMUNITY_EVENTS.contentDeleted, { contentType: kind });
|
|
480
|
-
queryClient.invalidateQueries({ queryKey: threadKey(postId) });
|
|
495
|
+
queryClient.invalidateQueries({ queryKey: threadKey(postId, locale) });
|
|
481
496
|
queryClient.invalidateQueries({ queryKey: FEED_KEY });
|
|
482
497
|
queryClient.invalidateQueries({ queryKey: USER_POSTS_KEY });
|
|
483
498
|
queryClient.invalidateQueries({ queryKey: SEARCH_KEY });
|
package/lib/inbox-service.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface InboxState {
|
|
|
12
12
|
/** Everything at or before this instant is read; null = never opened. */
|
|
13
13
|
seenAt: string | null;
|
|
14
14
|
}
|
|
15
|
+
/** Same truncation as list_notifications()'s `left(p.content, 140)`. */
|
|
16
|
+
export declare function localizeExcerpts(items: InboxItem[], translations: {
|
|
17
|
+
post_id: string;
|
|
18
|
+
content: string;
|
|
19
|
+
}[]): InboxItem[];
|
|
15
20
|
/** The inbox's whole state in one fetch: items (newest first) + seen marker. */
|
|
16
21
|
export declare function fetchInbox(cfg: ResolvedCommunityConfig): Promise<InboxState>;
|
|
17
22
|
/**
|
package/lib/inbox-service.js
CHANGED
|
@@ -12,11 +12,25 @@
|
|
|
12
12
|
// support-reply kind) passes through untouched instead of being special-cased
|
|
13
13
|
// here — the host UI decides what to do with a kind it doesn't recognize.
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.localizeExcerpts = localizeExcerpts;
|
|
15
16
|
exports.fetchInbox = fetchInbox;
|
|
16
17
|
exports.markInboxSeen = markInboxSeen;
|
|
17
18
|
exports.unreadCount = unreadCount;
|
|
18
19
|
const identity_1 = require("./identity");
|
|
20
|
+
const locale_1 = require("./locale");
|
|
19
21
|
const INBOX_PAGE_SIZE = 50;
|
|
22
|
+
/** Same truncation as list_notifications()'s `left(p.content, 140)`. */
|
|
23
|
+
function localizeExcerpts(items, translations) {
|
|
24
|
+
if (translations.length === 0)
|
|
25
|
+
return items;
|
|
26
|
+
const byPost = new Map(translations.map((t) => [t.post_id, t.content.slice(0, 140)]));
|
|
27
|
+
return items.map((item) => {
|
|
28
|
+
const translated = item.postId ? byPost.get(item.postId) : undefined;
|
|
29
|
+
return translated === undefined
|
|
30
|
+
? item
|
|
31
|
+
: { ...item, payload: { ...item.payload, postExcerpt: translated } };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
20
34
|
async function requireUid(cfg) {
|
|
21
35
|
const uid = await (0, identity_1.ensureIdentity)(cfg);
|
|
22
36
|
if (!uid)
|
|
@@ -61,7 +75,23 @@ async function fetchInbox(cfg) {
|
|
|
61
75
|
if (seenRes.error)
|
|
62
76
|
throw seenRes.error;
|
|
63
77
|
const items = (listRes.data ?? []).map(mapNotificationRow);
|
|
64
|
-
|
|
78
|
+
const locale = (0, locale_1.readerLocale)(cfg);
|
|
79
|
+
const postIds = [
|
|
80
|
+
...new Set(items.map((i) => i.postId).filter((id) => Boolean(id))),
|
|
81
|
+
];
|
|
82
|
+
let localized = items;
|
|
83
|
+
if (locale && postIds.length > 0) {
|
|
84
|
+
const { data, error } = await client
|
|
85
|
+
.from("post_translations")
|
|
86
|
+
.select("post_id, content")
|
|
87
|
+
.eq("locale", locale)
|
|
88
|
+
.in("post_id", postIds);
|
|
89
|
+
if (error) {
|
|
90
|
+
console.warn("[@rocapine/community-core] inbox translations unavailable, showing original excerpts:", error.message);
|
|
91
|
+
}
|
|
92
|
+
localized = localizeExcerpts(items, (data ?? []));
|
|
93
|
+
}
|
|
94
|
+
return { items: localized, seenAt: seenRes.data?.seen_at ?? null };
|
|
65
95
|
}
|
|
66
96
|
/**
|
|
67
97
|
* Marks everything currently in the inbox as read. The brief's signature
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export type { CommunityTopicDef, CommunityModules, CommunityHostAdapters, CommunityFeedConfig, CommunityConfig, ResolvedCommunityConfig, } from "./config";
|
|
2
2
|
export { CommunityDisabledError, resolveConfig } from "./config";
|
|
3
|
+
export { readerLocale, languageOf } from "./locale";
|
|
3
4
|
export { CommunityProvider, useCommunityConfig } from "./provider";
|
|
4
|
-
export type { PostRow, PollOptionRow, CommentRow, ProfileRow, PollOption, FeedPoll, PollData, ReactionData, FeedPost, ThreadComment, CommunityProfile, } from "./models";
|
|
5
|
+
export type { PostRow, PollOptionRow, CommentRow, ProfileRow, PollOption, FeedPoll, PollData, ReactionData, FeedPost, ThreadComment, CommunityProfile, TranslationInfo, TranslationRowEmbed, } from "./models";
|
|
5
6
|
export { FEED_PAGE_SIZE, POST_MAX_LENGTH, COMMENT_MAX_LENGTH, POLL_MIN_OPTIONS, POLL_MAX_OPTIONS, POLL_OPTION_MAX_LENGTH, EMPTY_POLL_DATA, EMPTY_REACTION_DATA, displayName, normalizeTopic, mapProfileRow, buildPoll, pollPercent, mapPostRow, applyPollVote, applyReaction, mapCommentRow, newPostsLabel, newestCreatedAt, } from "./models";
|
|
6
7
|
export type { TimeAgoParts } from "./time";
|
|
7
8
|
export { timeAgo, timeAgoParts } from "./time";
|
|
@@ -11,7 +12,7 @@ export { ensureIdentity, resetIdentity, syncProfileFromHost } from "./identity";
|
|
|
11
12
|
export { buildFeedSelect, fetchFeedPage, countNewPosts, fetchProfile, fetchUserPosts, searchPosts, fetchThread, createPost, votePoll, createComment, moderateOne, setLike, setReaction, fetchReactionSummaries, reportContent, blockUser, deleteOwnPost, deleteOwnComment, updateProfile, uploadAvatar, } from "./service";
|
|
12
13
|
export type { ModerationVerdict, CreatePostInput, ReportReason, ReportInput, UpdateProfileInput, UpdateProfileResult, } from "./service";
|
|
13
14
|
export { useCommunityFeed, useSearchPosts, useNewPostsCount, useCommunityUnseenCount, useThread, useProfile, useUserPosts, useMyUid, useUpdateProfile, useCreatePost, useCreateComment, useToggleLike, useVotePoll, useReactToPost, useReport, useBlockUser, useDeleteContent, } from "./hooks";
|
|
14
|
-
export { fetchInbox, markInboxSeen, unreadCount } from "./inbox-service";
|
|
15
|
+
export { fetchInbox, localizeExcerpts, markInboxSeen, unreadCount } from "./inbox-service";
|
|
15
16
|
export type { InboxItem, InboxState } from "./inbox-service";
|
|
16
17
|
export { useNotificationInbox, useUnreadNotificationCount, useMarkInboxSeen } from "./inbox-hooks";
|
|
17
18
|
export { REQUIRED_SCHEMA_VERSION } from "./schema-check";
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.REQUIRED_SCHEMA_VERSION = exports.useMarkInboxSeen = exports.useUnreadNotificationCount = exports.useNotificationInbox = exports.unreadCount = exports.markInboxSeen = exports.fetchInbox = 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 = void 0;
|
|
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.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;
|
|
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; } });
|
|
8
|
+
var locale_1 = require("./locale");
|
|
9
|
+
Object.defineProperty(exports, "readerLocale", { enumerable: true, get: function () { return locale_1.readerLocale; } });
|
|
10
|
+
Object.defineProperty(exports, "languageOf", { enumerable: true, get: function () { return locale_1.languageOf; } });
|
|
8
11
|
var provider_1 = require("./provider");
|
|
9
12
|
Object.defineProperty(exports, "CommunityProvider", { enumerable: true, get: function () { return provider_1.CommunityProvider; } });
|
|
10
13
|
Object.defineProperty(exports, "useCommunityConfig", { enumerable: true, get: function () { return provider_1.useCommunityConfig; } });
|
|
@@ -79,6 +82,7 @@ Object.defineProperty(exports, "useBlockUser", { enumerable: true, get: function
|
|
|
79
82
|
Object.defineProperty(exports, "useDeleteContent", { enumerable: true, get: function () { return hooks_1.useDeleteContent; } });
|
|
80
83
|
var inbox_service_1 = require("./inbox-service");
|
|
81
84
|
Object.defineProperty(exports, "fetchInbox", { enumerable: true, get: function () { return inbox_service_1.fetchInbox; } });
|
|
85
|
+
Object.defineProperty(exports, "localizeExcerpts", { enumerable: true, get: function () { return inbox_service_1.localizeExcerpts; } });
|
|
82
86
|
Object.defineProperty(exports, "markInboxSeen", { enumerable: true, get: function () { return inbox_service_1.markInboxSeen; } });
|
|
83
87
|
Object.defineProperty(exports, "unreadCount", { enumerable: true, get: function () { return inbox_service_1.unreadCount; } });
|
|
84
88
|
var inbox_hooks_1 = require("./inbox-hooks");
|
package/lib/locale.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResolvedCommunityConfig } from "./config";
|
|
2
|
+
export declare function languageOf(locale: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* The locale translations are read in: the host's locale resolved against
|
|
5
|
+
* `modules.translation.locales` — exact match, else the first declared locale
|
|
6
|
+
* of the same language, else null (originals only). Null when the module is off.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readerLocale(cfg: ResolvedCommunityConfig): string | null;
|
package/lib/locale.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.languageOf = languageOf;
|
|
4
|
+
exports.readerLocale = readerLocale;
|
|
5
|
+
function languageOf(locale) {
|
|
6
|
+
return locale.split("-")[0]?.toLowerCase() ?? "";
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The locale translations are read in: the host's locale resolved against
|
|
10
|
+
* `modules.translation.locales` — exact match, else the first declared locale
|
|
11
|
+
* of the same language, else null (originals only). Null when the module is off.
|
|
12
|
+
*/
|
|
13
|
+
function readerLocale(cfg) {
|
|
14
|
+
const module = cfg.modules.translation;
|
|
15
|
+
if (!module || module.locales.length === 0)
|
|
16
|
+
return null;
|
|
17
|
+
const host = cfg.host.getLocale();
|
|
18
|
+
if (module.locales.includes(host))
|
|
19
|
+
return host;
|
|
20
|
+
const lang = languageOf(host);
|
|
21
|
+
return module.locales.find((l) => languageOf(l) === lang) ?? null;
|
|
22
|
+
}
|
package/lib/models.d.ts
CHANGED
|
@@ -29,11 +29,29 @@ export interface PostRow {
|
|
|
29
29
|
/** Empty for a post without a poll; absent entirely when the polls module
|
|
30
30
|
* is off (the embed is then not part of the select). */
|
|
31
31
|
poll_options?: PollOptionRow[];
|
|
32
|
+
/** At most one row: server-filtered to the reader's locale. Absent when the
|
|
33
|
+
* translation module is off. */
|
|
34
|
+
post_translations?: TranslationRowEmbed[];
|
|
32
35
|
}
|
|
33
36
|
export interface PollOptionRow {
|
|
34
37
|
id: string;
|
|
35
38
|
idx: number;
|
|
36
39
|
label: string;
|
|
40
|
+
/** At most one row: server-filtered to the reader's locale. */
|
|
41
|
+
poll_option_translations?: {
|
|
42
|
+
content: string;
|
|
43
|
+
}[];
|
|
44
|
+
}
|
|
45
|
+
/** One embedded translation row, filtered server-side on the reader locale. */
|
|
46
|
+
export interface TranslationRowEmbed {
|
|
47
|
+
locale: string;
|
|
48
|
+
source_locale: string;
|
|
49
|
+
content: string;
|
|
50
|
+
}
|
|
51
|
+
export interface TranslationInfo {
|
|
52
|
+
text: string;
|
|
53
|
+
/** Detected language of the original (short code: en, es, pt…). */
|
|
54
|
+
sourceLocale: string;
|
|
37
55
|
}
|
|
38
56
|
export interface CommentRow {
|
|
39
57
|
id: string;
|
|
@@ -48,10 +66,15 @@ export interface CommentRow {
|
|
|
48
66
|
handle: string | null;
|
|
49
67
|
avatar_url: string | null;
|
|
50
68
|
} | null;
|
|
69
|
+
/** At most one row: server-filtered to the reader's locale. Absent when the
|
|
70
|
+
* translation module is off. */
|
|
71
|
+
comment_translations?: TranslationRowEmbed[];
|
|
51
72
|
}
|
|
52
73
|
export interface PollOption {
|
|
53
74
|
id: string;
|
|
54
75
|
label: string;
|
|
76
|
+
/** Label in the reader locale when the translation module provided one. */
|
|
77
|
+
translatedLabel: string | null;
|
|
55
78
|
votes: number;
|
|
56
79
|
}
|
|
57
80
|
export interface FeedPoll {
|
|
@@ -107,6 +130,9 @@ export interface FeedPost {
|
|
|
107
130
|
/** Display name of the most recent reactor, for a "X and N others reacted" affordance;
|
|
108
131
|
* null when nobody has reacted yet or the reaction module is off. */
|
|
109
132
|
lastReactorName: string | null;
|
|
133
|
+
/** Reader-locale translation; null when the item is already in the reader's language,
|
|
134
|
+
* not translated yet, or the module is off. `text` stays the original. */
|
|
135
|
+
translation: TranslationInfo | null;
|
|
110
136
|
}
|
|
111
137
|
export interface ThreadComment {
|
|
112
138
|
id: string;
|
|
@@ -119,6 +145,9 @@ export interface ThreadComment {
|
|
|
119
145
|
text: string;
|
|
120
146
|
isOwn: boolean;
|
|
121
147
|
createdAt: string;
|
|
148
|
+
/** Reader-locale translation; null when the item is already in the reader's language,
|
|
149
|
+
* not translated yet, or the module is off. `text` stays the original. */
|
|
150
|
+
translation: TranslationInfo | null;
|
|
122
151
|
}
|
|
123
152
|
/**
|
|
124
153
|
* Public display name, with a host-configured fallback for profiles not yet named.
|
package/lib/models.js
CHANGED
|
@@ -65,13 +65,22 @@ function mapProfileRow(row, fallback) {
|
|
|
65
65
|
username: row.username ?? null,
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
+
function embedToTranslation(rows) {
|
|
69
|
+
const row = rows?.[0];
|
|
70
|
+
return row ? { text: row.content, sourceLocale: row.source_locale } : null;
|
|
71
|
+
}
|
|
68
72
|
/** Assemble a FeedPoll from the embedded option rows + batched vote data. */
|
|
69
73
|
function buildPoll(options, counts, myOptionId) {
|
|
70
74
|
if (!options || options.length === 0)
|
|
71
75
|
return null;
|
|
72
76
|
const mapped = [...options]
|
|
73
77
|
.sort((a, b) => a.idx - b.idx)
|
|
74
|
-
.map((o) => ({
|
|
78
|
+
.map((o) => ({
|
|
79
|
+
id: o.id,
|
|
80
|
+
label: o.label,
|
|
81
|
+
translatedLabel: o.poll_option_translations?.[0]?.content ?? null,
|
|
82
|
+
votes: counts.get(o.id) ?? 0,
|
|
83
|
+
}));
|
|
75
84
|
return {
|
|
76
85
|
options: mapped,
|
|
77
86
|
myOptionId,
|
|
@@ -108,6 +117,7 @@ transformPost) {
|
|
|
108
117
|
reactionCount: reactionData.counts.get(row.id) ?? 0,
|
|
109
118
|
hasReacted: reactionData.mine.has(row.id),
|
|
110
119
|
lastReactorName: reactionData.lastReactorName.get(row.id) ?? null,
|
|
120
|
+
translation: embedToTranslation(row.post_translations),
|
|
111
121
|
};
|
|
112
122
|
return transformPost ? transformPost(post, row) : post;
|
|
113
123
|
}
|
|
@@ -157,6 +167,7 @@ function mapCommentRow(row, myUid, fallback) {
|
|
|
157
167
|
text: row.content,
|
|
158
168
|
isOwn: row.author_id === myUid,
|
|
159
169
|
createdAt: row.created_at,
|
|
170
|
+
translation: embedToTranslation(row.comment_translations),
|
|
160
171
|
};
|
|
161
172
|
}
|
|
162
173
|
/** Floating pill label. "1 new post" / "N new posts". */
|
package/lib/service.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type CommunityProfile, type FeedPost, type ReactionData, type ThreadCom
|
|
|
5
5
|
* exported so it's unit-testable without a Supabase client. Invalid column
|
|
6
6
|
* names are dropped with a `console.warn` rather than failing the query.
|
|
7
7
|
*/
|
|
8
|
-
export declare function buildFeedSelect(extraPostColumns?: readonly string[], polls?: boolean): string;
|
|
8
|
+
export declare function buildFeedSelect(extraPostColumns?: readonly string[], polls?: boolean, translations?: boolean): string;
|
|
9
9
|
/**
|
|
10
10
|
* Batched reaction state for one page: count + last reactor's display name + my-reaction
|
|
11
11
|
* flag, all from the single security-definer `post_reaction_summary` RPC (the backend
|
|
@@ -46,7 +46,7 @@ export declare function fetchUserPosts(cfg: ResolvedCommunityConfig, userId: str
|
|
|
46
46
|
* page-index cursor as `fetchFeedPage` (product decision, reversing the
|
|
47
47
|
* earlier single-page scope cut).
|
|
48
48
|
*/
|
|
49
|
-
export declare function searchPosts(cfg: ResolvedCommunityConfig,
|
|
49
|
+
export declare function searchPosts(cfg: ResolvedCommunityConfig, searchTerm: string, opts?: {
|
|
50
50
|
cursor?: number;
|
|
51
51
|
}): Promise<FeedPost[]>;
|
|
52
52
|
/**
|
package/lib/service.js
CHANGED
|
@@ -30,6 +30,7 @@ exports.updateProfile = updateProfile;
|
|
|
30
30
|
exports.uploadAvatar = uploadAvatar;
|
|
31
31
|
const supabase_js_1 = require("@supabase/supabase-js");
|
|
32
32
|
const identity_1 = require("./identity");
|
|
33
|
+
const locale_1 = require("./locale");
|
|
33
34
|
const models_1 = require("./models");
|
|
34
35
|
/** Avatar images live in this Storage bucket on every install (mold value, unchanged). */
|
|
35
36
|
const AVATAR_BUCKET = "avatars";
|
|
@@ -46,6 +47,14 @@ const FEED_SELECT = "id, author_id, topic, content, status, pinned_at, created_a
|
|
|
46
47
|
* `poll_options` table does not exist and PostgREST rejects the whole select
|
|
47
48
|
* (PGRST200), which took the entire feed down for such installs. */
|
|
48
49
|
const POLL_OPTIONS_SELECT = "poll_options(id, idx, label)";
|
|
50
|
+
/** Same embed, plus each option's translation row — used when the reader has
|
|
51
|
+
* a resolved translation locale (see `readerLocale`). */
|
|
52
|
+
const POLL_OPTIONS_TRANSLATED_SELECT = "poll_options(id, idx, label, poll_option_translations(content))";
|
|
53
|
+
/** Embedded only when the reader has a resolved translation locale. */
|
|
54
|
+
const POST_TRANSLATIONS_SELECT = "post_translations(locale, source_locale, content)";
|
|
55
|
+
const COMMENTS_SELECT = "id, post_id, author_id, content, status, created_at, profiles(username, is_official, handle, avatar_url)";
|
|
56
|
+
/** Embedded only when the reader has a resolved translation locale. */
|
|
57
|
+
const COMMENT_TRANSLATIONS_SELECT = "comment_translations(locale, source_locale, content)";
|
|
49
58
|
const PROFILE_SELECT = "id, username, handle, is_official, bio, avatar_url";
|
|
50
59
|
/** Minimal injection guard for `CommunityConfig.feed.extraPostColumns`: a bare
|
|
51
60
|
* column name only, no `.`, `(`, `:`, whitespace, etc. — those are how a
|
|
@@ -58,8 +67,13 @@ const VALID_EXTRA_COLUMN = /^[a-z0-9_]+$/;
|
|
|
58
67
|
* exported so it's unit-testable without a Supabase client. Invalid column
|
|
59
68
|
* names are dropped with a `console.warn` rather than failing the query.
|
|
60
69
|
*/
|
|
61
|
-
function buildFeedSelect(extraPostColumns, polls = false) {
|
|
62
|
-
const
|
|
70
|
+
function buildFeedSelect(extraPostColumns, polls = false, translations = false) {
|
|
71
|
+
const parts = [FEED_SELECT];
|
|
72
|
+
if (polls)
|
|
73
|
+
parts.push(translations ? POLL_OPTIONS_TRANSLATED_SELECT : POLL_OPTIONS_SELECT);
|
|
74
|
+
if (translations)
|
|
75
|
+
parts.push(POST_TRANSLATIONS_SELECT);
|
|
76
|
+
const base = parts.join(", ");
|
|
63
77
|
if (!extraPostColumns || extraPostColumns.length === 0)
|
|
64
78
|
return base;
|
|
65
79
|
const valid = extraPostColumns.filter((column) => {
|
|
@@ -73,9 +87,25 @@ function buildFeedSelect(extraPostColumns, polls = false) {
|
|
|
73
87
|
return `${base}, ${valid.join(", ")}`;
|
|
74
88
|
}
|
|
75
89
|
/** The posts select for this install: `feed.extraPostColumns` + the poll
|
|
76
|
-
* embed iff `modules.polls
|
|
90
|
+
* embed iff `modules.polls`, + translation embeds iff the reader has a
|
|
91
|
+
* resolved translation locale. */
|
|
77
92
|
function postsSelect(cfg) {
|
|
78
|
-
return buildFeedSelect(cfg.feed.extraPostColumns, cfg.modules.polls);
|
|
93
|
+
return buildFeedSelect(cfg.feed.extraPostColumns, cfg.modules.polls, (0, locale_1.readerLocale)(cfg) !== null);
|
|
94
|
+
}
|
|
95
|
+
/** Embedded-resource filters: keep only the reader locale's translation rows,
|
|
96
|
+
* so `rows?.[0]` in the mappers is always that locale, never some other
|
|
97
|
+
* locale's leftover row. PostgREST accepts dotted paths for nested embeds,
|
|
98
|
+
* the same mechanism as the existing `.eq("comments.status", "visible")`. A
|
|
99
|
+
* no-op (module off / no resolved locale) leaves the query, and therefore
|
|
100
|
+
* every byte of it, unchanged from before translations existed. */
|
|
101
|
+
function withTranslationFilters(query, cfg) {
|
|
102
|
+
const locale = (0, locale_1.readerLocale)(cfg);
|
|
103
|
+
if (!locale)
|
|
104
|
+
return query;
|
|
105
|
+
let q = query.eq("post_translations.locale", locale);
|
|
106
|
+
if (cfg.modules.polls)
|
|
107
|
+
q = q.eq("poll_options.poll_option_translations.locale", locale);
|
|
108
|
+
return q;
|
|
79
109
|
}
|
|
80
110
|
async function fetchMyLikes(client, postIds, uid) {
|
|
81
111
|
if (postIds.length === 0)
|
|
@@ -177,6 +207,7 @@ async function fetchFeedPage(cfg, opts = {}) {
|
|
|
177
207
|
.order("pinned_at", { ascending: false, nullsFirst: false })
|
|
178
208
|
.order("created_at", { ascending: false })
|
|
179
209
|
.range(page * models_1.FEED_PAGE_SIZE, page * models_1.FEED_PAGE_SIZE + models_1.FEED_PAGE_SIZE - 1);
|
|
210
|
+
query = withTranslationFilters(query, cfg);
|
|
180
211
|
if (opts.topic)
|
|
181
212
|
query = query.eq("topic", opts.topic);
|
|
182
213
|
const { data, error } = await query;
|
|
@@ -228,7 +259,7 @@ async function fetchUserPosts(cfg, userId, opts = {}) {
|
|
|
228
259
|
const client = cfg.requireClient();
|
|
229
260
|
const uid = await requireUid(cfg);
|
|
230
261
|
const page = opts.cursor ?? 0;
|
|
231
|
-
|
|
262
|
+
let query = client
|
|
232
263
|
.from("posts")
|
|
233
264
|
.select(postsSelect(cfg))
|
|
234
265
|
.eq("author_id", userId)
|
|
@@ -236,6 +267,8 @@ async function fetchUserPosts(cfg, userId, opts = {}) {
|
|
|
236
267
|
.in("status", ["visible", "pending"])
|
|
237
268
|
.order("created_at", { ascending: false })
|
|
238
269
|
.range(page * models_1.FEED_PAGE_SIZE, page * models_1.FEED_PAGE_SIZE + models_1.FEED_PAGE_SIZE - 1);
|
|
270
|
+
query = withTranslationFilters(query, cfg);
|
|
271
|
+
const { data, error } = await query;
|
|
239
272
|
if (error)
|
|
240
273
|
throw error;
|
|
241
274
|
return toFeedPosts(cfg, client, (data ?? []), uid);
|
|
@@ -245,14 +278,14 @@ async function fetchUserPosts(cfg, userId, opts = {}) {
|
|
|
245
278
|
* page-index cursor as `fetchFeedPage` (product decision, reversing the
|
|
246
279
|
* earlier single-page scope cut).
|
|
247
280
|
*/
|
|
248
|
-
async function searchPosts(cfg,
|
|
249
|
-
const cleaned =
|
|
281
|
+
async function searchPosts(cfg, searchTerm, opts = {}) {
|
|
282
|
+
const cleaned = searchTerm.trim();
|
|
250
283
|
if (cleaned.length === 0)
|
|
251
284
|
return [];
|
|
252
285
|
const client = cfg.requireClient();
|
|
253
286
|
const uid = await requireUid(cfg);
|
|
254
287
|
const page = opts.cursor ?? 0;
|
|
255
|
-
|
|
288
|
+
let query = client
|
|
256
289
|
.from("posts")
|
|
257
290
|
.select(postsSelect(cfg))
|
|
258
291
|
.eq("comments.status", "visible")
|
|
@@ -260,6 +293,8 @@ async function searchPosts(cfg, query, opts = {}) {
|
|
|
260
293
|
.ilike("content", `%${cleaned}%`)
|
|
261
294
|
.order("created_at", { ascending: false })
|
|
262
295
|
.range(page * models_1.FEED_PAGE_SIZE, page * models_1.FEED_PAGE_SIZE + models_1.FEED_PAGE_SIZE - 1);
|
|
296
|
+
query = withTranslationFilters(query, cfg);
|
|
297
|
+
const { data, error } = await query;
|
|
263
298
|
if (error)
|
|
264
299
|
throw error;
|
|
265
300
|
return toFeedPosts(cfg, client, (data ?? []), uid);
|
|
@@ -273,12 +308,17 @@ async function searchPosts(cfg, query, opts = {}) {
|
|
|
273
308
|
async function fetchThread(cfg, postId) {
|
|
274
309
|
const client = cfg.requireClient();
|
|
275
310
|
const uid = await requireUid(cfg);
|
|
276
|
-
const
|
|
311
|
+
const locale = (0, locale_1.readerLocale)(cfg);
|
|
312
|
+
const select = locale ? `${COMMENTS_SELECT}, ${COMMENT_TRANSLATIONS_SELECT}` : COMMENTS_SELECT;
|
|
313
|
+
let query = client
|
|
277
314
|
.from("comments")
|
|
278
|
-
.select(
|
|
315
|
+
.select(select)
|
|
279
316
|
.eq("post_id", postId)
|
|
280
317
|
.in("status", ["visible", "pending"])
|
|
281
318
|
.order("created_at", { ascending: true });
|
|
319
|
+
if (locale)
|
|
320
|
+
query = query.eq("comment_translations.locale", locale);
|
|
321
|
+
const { data, error } = await query;
|
|
282
322
|
if (error)
|
|
283
323
|
throw error;
|
|
284
324
|
return (data ?? []).map((r) => (0, models_1.mapCommentRow)(r, uid, cfg.anonymousAuthorFallback));
|
package/package.json
CHANGED