memi-board 0.51.2 → 0.52.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/dist/{ar-Dzc31QaB.js → ar-BVwpedHS.js} +23 -14
- package/dist/ar-BVwpedHS.js.map +1 -0
- package/dist/{commentImages-CV0g3ARh.js → commentImages-CzXO42gE.js} +39 -23
- package/dist/commentImages-CzXO42gE.js.map +1 -0
- package/dist/composables/useMemiBoardWritingAssistant.d.ts +2 -0
- package/dist/data/versionHistory.d.ts +5 -0
- package/dist/{de-Wci_gqEw.js → de-K2gYwR8U.js} +23 -14
- package/dist/de-K2gYwR8U.js.map +1 -0
- package/dist/{en-Dhe-tFzi.js → en-CmPLbFcN.js} +30 -14
- package/dist/en-CmPLbFcN.js.map +1 -0
- package/dist/{es-BHJ1VeX-.js → es-1P1miLJH.js} +23 -14
- package/dist/es-1P1miLJH.js.map +1 -0
- package/dist/{fr-aSscFSwK.js → fr-cDQCmNSc.js} +23 -14
- package/dist/fr-cDQCmNSc.js.map +1 -0
- package/dist/{id-BuUIYpdS.js → id-BzDIqeZ4.js} +23 -14
- package/dist/id-BzDIqeZ4.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +597 -427
- package/dist/index.js.map +1 -1
- package/dist/{ja-owJ_AiPl.js → ja-B2wxHGCr.js} +23 -14
- package/dist/ja-B2wxHGCr.js.map +1 -0
- package/dist/{pt-B3Uxrkz2.js → pt-BCY3OODD.js} +23 -14
- package/dist/pt-BCY3OODD.js.map +1 -0
- package/dist/runtime/components/CategorySettings.vue +37 -9
- package/dist/runtime/components/Editor.vue +9 -4
- package/dist/runtime/components/Settings.vue +198 -9
- package/dist/runtime/components/VersionHistory.vue +9 -2
- package/dist/storage.js +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/utils/localizedFields.d.ts +12 -2
- package/dist/{zh-B49hHRSL.js → zh-Y824rwR6.js} +23 -14
- package/dist/zh-Y824rwR6.js.map +1 -0
- package/package.json +1 -1
- package/dist/ar-Dzc31QaB.js.map +0 -1
- package/dist/commentImages-CV0g3ARh.js.map +0 -1
- package/dist/de-Wci_gqEw.js.map +0 -1
- package/dist/en-Dhe-tFzi.js.map +0 -1
- package/dist/es-BHJ1VeX-.js.map +0 -1
- package/dist/fr-aSscFSwK.js.map +0 -1
- package/dist/id-BuUIYpdS.js.map +0 -1
- package/dist/ja-owJ_AiPl.js.map +0 -1
- package/dist/pt-B3Uxrkz2.js.map +0 -1
- package/dist/zh-B49hHRSL.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as e, B as t, C as n, D as r, E as i, F as a, G as o, H as s, I as c, J as l, K as u, L as d, M as f, N as p, O as m, P as h, R as g, S as _, T as v, U as y, V as b, W as x, X as S, Y as C, Z as w, _ as T, a as E, b as D, c as ee, d as O, f as te, g as k, h as ne, j as re, k as ie, l as ae, m as A, o as oe, p as se, q as ce, r as le, s as j, t as ue, u as de, v as fe, w as M, x as pe, y as N, z as me } from "./commentImages-
|
|
1
|
+
import { A as e, B as t, C as n, D as r, E as i, F as a, G as o, H as s, I as c, J as l, K as u, L as d, M as f, N as p, O as m, P as h, R as g, S as _, T as v, U as y, V as b, W as x, X as S, Y as C, Z as w, _ as T, a as E, b as D, c as ee, d as O, f as te, g as k, h as ne, j as re, k as ie, l as ae, m as A, o as oe, p as se, q as ce, r as le, s as j, t as ue, u as de, v as fe, w as M, x as pe, y as N, z as me } from "./commentImages-CzXO42gE.js";
|
|
2
2
|
import { computed as P, onScopeDispose as F, reactive as he, readonly as ge, ref as I, toValue as L, watch as R } from "vue";
|
|
3
3
|
import { collection as _e, deleteDoc as ve, deleteField as ye, doc as z, documentId as B, endAt as be, endBefore as xe, getDoc as V, getDocFromServer as Se, getDocs as H, getDocsFromServer as Ce, increment as we, limit as U, limitToLast as Te, onSnapshot as W, orderBy as G, query as K, runTransaction as Ee, serverTimestamp as q, setDoc as De, startAfter as J, updateDoc as Y, where as X, writeBatch as Oe } from "firebase/firestore";
|
|
4
4
|
import { useCollection as ke, useCurrentUser as Ae, useDocument as je, useFirebaseApp as Me, useFirebaseAuth as Ne, useFirestore as Z } from "vuefire";
|
|
@@ -121,10 +121,19 @@ function ft(e, t, n) {
|
|
|
121
121
|
...i
|
|
122
122
|
}), r;
|
|
123
123
|
}
|
|
124
|
-
function pt(e
|
|
125
|
-
|
|
124
|
+
function pt(e) {
|
|
125
|
+
let t = new Set((e?.translationLocales ?? []).map((e) => N(e)).filter((e) => !!e)), n = T.map((e) => e.code);
|
|
126
|
+
return t.size ? n.filter((e) => t.has(e)) : n;
|
|
126
127
|
}
|
|
127
|
-
function mt(e
|
|
128
|
+
function mt(e) {
|
|
129
|
+
if (!Array.isArray(e)) return [];
|
|
130
|
+
let t = pt({ translationLocales: e.filter((e) => typeof e == "string") });
|
|
131
|
+
return t.length === T.length ? [] : t;
|
|
132
|
+
}
|
|
133
|
+
function ht(e, t, n = T.map((e) => e.code)) {
|
|
134
|
+
return !e || !t.length ? !1 : n.every((n) => t.every((t) => !!lt(e[n], t)));
|
|
135
|
+
}
|
|
136
|
+
function gt(e, t, r, i, a) {
|
|
128
137
|
if (!n()) return i;
|
|
129
138
|
let o = N(t), s = N(a);
|
|
130
139
|
if (!o || s && o === s) return i;
|
|
@@ -136,32 +145,32 @@ function mt(e, t, r, i, a) {
|
|
|
136
145
|
}
|
|
137
146
|
return i;
|
|
138
147
|
}
|
|
139
|
-
function
|
|
140
|
-
return r ? !t || t[n] !== r ? r :
|
|
148
|
+
function _t(e, t, n, r, i) {
|
|
149
|
+
return r ? !t || t[n] !== r ? r : gt(e, i, n, r, t.locale) : "";
|
|
141
150
|
}
|
|
142
|
-
function
|
|
143
|
-
return
|
|
151
|
+
function vt(e, t) {
|
|
152
|
+
return _t(e.i18n, e.i18nSource, "title", e.title ?? "", t);
|
|
144
153
|
}
|
|
145
|
-
function
|
|
146
|
-
return
|
|
154
|
+
function yt(e, t) {
|
|
155
|
+
return _t(e.i18n, e.i18nSource, "summary", e.summary ?? "", t);
|
|
147
156
|
}
|
|
148
|
-
function
|
|
157
|
+
function bt(e, t) {
|
|
149
158
|
let n = e.content || "", r = e.contentI18nSource?.hash;
|
|
150
|
-
return !n || !r || r !== ct(n) ? n :
|
|
159
|
+
return !n || !r || r !== ct(n) ? n : gt(e.contentI18n, t, "content", n, e.contentI18nSource?.locale) || n;
|
|
151
160
|
}
|
|
152
|
-
function
|
|
161
|
+
function xt(e) {
|
|
153
162
|
let t = e.label.trim(), n = (e.description ?? "").trim();
|
|
154
|
-
return !t || !e.i18nSource || e.i18nSource.label !== t || (e.i18nSource.description ?? "") !== n ? !1 :
|
|
163
|
+
return !t || !e.i18nSource || e.i18nSource.label !== t || (e.i18nSource.description ?? "") !== n ? !1 : ht(e.i18n, n ? ["label", "description"] : ["label"], pt(e));
|
|
155
164
|
}
|
|
156
165
|
//#endregion
|
|
157
166
|
//#region src/composables/fetchPublicSeo.ts
|
|
158
|
-
function
|
|
167
|
+
function St() {
|
|
159
168
|
return {
|
|
160
169
|
db: Z(),
|
|
161
170
|
paths: M()
|
|
162
171
|
};
|
|
163
172
|
}
|
|
164
|
-
async function
|
|
173
|
+
async function Ct(e, t, n) {
|
|
165
174
|
try {
|
|
166
175
|
let r = await V(w(e.db, e.paths, t));
|
|
167
176
|
if (!r.exists()) return {
|
|
@@ -171,8 +180,8 @@ async function xt(e, t, n) {
|
|
|
171
180
|
};
|
|
172
181
|
let i = r.data() ?? {}, a = typeof i.label == "string" && i.label.trim() ? i.label.trim() : t, o = typeof i.description == "string" ? i.description.trim() : "", s = Q(i.i18n), c = ut(i.i18nSource);
|
|
173
182
|
return {
|
|
174
|
-
label:
|
|
175
|
-
description:
|
|
183
|
+
label: _t(s, c, "label", a, n) || a,
|
|
184
|
+
description: _t(s, c, "description", o, n),
|
|
176
185
|
hidden: i.visibility === "hidden"
|
|
177
186
|
};
|
|
178
187
|
} catch {
|
|
@@ -183,7 +192,7 @@ async function xt(e, t, n) {
|
|
|
183
192
|
};
|
|
184
193
|
}
|
|
185
194
|
}
|
|
186
|
-
function
|
|
195
|
+
function wt(e) {
|
|
187
196
|
if (e && typeof e == "object" && "toDate" in e) try {
|
|
188
197
|
let t = e.toDate();
|
|
189
198
|
return t instanceof Date ? t.toISOString() : null;
|
|
@@ -192,17 +201,17 @@ function St(e) {
|
|
|
192
201
|
}
|
|
193
202
|
return null;
|
|
194
203
|
}
|
|
195
|
-
async function
|
|
204
|
+
async function Tt(e, t, n, r) {
|
|
196
205
|
let i = e.trim(), a = t.trim();
|
|
197
206
|
if (!i || !a) return null;
|
|
198
|
-
let s = n ??
|
|
207
|
+
let s = n ?? St();
|
|
199
208
|
s.paths = C(s.paths), r && await se(r);
|
|
200
|
-
let c = await
|
|
209
|
+
let c = await Ct(s, i, r);
|
|
201
210
|
if (c.hidden) return null;
|
|
202
211
|
try {
|
|
203
212
|
let e = (await H(K(o(s.db, s.paths), X("boardId", "==", i), X("slug", "==", a), X("listed", "==", !0), X("isPublished", "==", !0), U(1)))).docs[0];
|
|
204
213
|
if (!e) return null;
|
|
205
|
-
let t = e.data() ?? {}, n = typeof t.title == "string" ? t.title : "", l = typeof t.summary == "string" ? t.summary : "", u = Q(t.i18n), d = ut(t.i18nSource), f =
|
|
214
|
+
let t = e.data() ?? {}, n = typeof t.title == "string" ? t.title : "", l = typeof t.summary == "string" ? t.summary : "", u = Q(t.i18n), d = ut(t.i18nSource), f = vt({
|
|
206
215
|
title: n,
|
|
207
216
|
i18n: u,
|
|
208
217
|
i18nSource: d
|
|
@@ -213,24 +222,24 @@ async function Ct(e, t, n, r) {
|
|
|
213
222
|
category: i,
|
|
214
223
|
categoryLabel: c.label,
|
|
215
224
|
title: f,
|
|
216
|
-
summary:
|
|
225
|
+
summary: yt({
|
|
217
226
|
summary: l,
|
|
218
227
|
i18n: u,
|
|
219
228
|
i18nSource: d
|
|
220
229
|
}, r).trim().slice(0, 200),
|
|
221
230
|
previewImage: et(t.previewImage),
|
|
222
231
|
authorName: typeof t.authorName == "string" && t.authorName.trim() ? t.authorName.trim() : null,
|
|
223
|
-
createdAt:
|
|
232
|
+
createdAt: wt(t.createdAt)
|
|
224
233
|
} : null;
|
|
225
234
|
} catch (e) {
|
|
226
235
|
return import.meta.dev && console.warn("[memi-board] fetchPublicPostForSeo failed", e), null;
|
|
227
236
|
}
|
|
228
237
|
}
|
|
229
|
-
async function
|
|
238
|
+
async function Et(e, t, n) {
|
|
230
239
|
n && await se(n);
|
|
231
|
-
let r = e?.trim() || null, i = t ??
|
|
240
|
+
let r = e?.trim() || null, i = t ?? St();
|
|
232
241
|
if (i.paths = C(i.paths), r) {
|
|
233
|
-
let e = await
|
|
242
|
+
let e = await Ct(i, r, n);
|
|
234
243
|
if (e.hidden) return {
|
|
235
244
|
kind: "category",
|
|
236
245
|
category: r,
|
|
@@ -239,7 +248,7 @@ async function wt(e, t, n) {
|
|
|
239
248
|
ogImage: null,
|
|
240
249
|
recentTitles: []
|
|
241
250
|
};
|
|
242
|
-
let { recentTitles: t, ogImage: a } = await
|
|
251
|
+
let { recentTitles: t, ogImage: a } = await Dt(i, r, n);
|
|
243
252
|
return {
|
|
244
253
|
kind: "category",
|
|
245
254
|
category: r,
|
|
@@ -258,12 +267,12 @@ async function wt(e, t, n) {
|
|
|
258
267
|
recentTitles: []
|
|
259
268
|
};
|
|
260
269
|
}
|
|
261
|
-
async function
|
|
270
|
+
async function Dt(e, t, n) {
|
|
262
271
|
let r = [], i = null;
|
|
263
272
|
try {
|
|
264
273
|
let a = await H(K(o(e.db, e.paths), X("boardId", "==", t), X("listed", "==", !0), X("isPublished", "==", !0), G("createdAt", "desc"), U(12)));
|
|
265
274
|
for (let e of a.docs) {
|
|
266
|
-
let t = e.data() ?? {}, a =
|
|
275
|
+
let t = e.data() ?? {}, a = vt({
|
|
267
276
|
title: typeof t.title == "string" ? t.title : "",
|
|
268
277
|
i18n: Q(t.i18n),
|
|
269
278
|
i18nSource: ut(t.i18nSource)
|
|
@@ -280,31 +289,31 @@ async function Tt(e, t, n) {
|
|
|
280
289
|
}
|
|
281
290
|
//#endregion
|
|
282
291
|
//#region src/utils/boardAccess.ts
|
|
283
|
-
function
|
|
292
|
+
function Ot(e, t) {
|
|
284
293
|
return !e || !t ? !1 : [...t.allowedStaffUids ?? [], ...t.allowedManagerUids ?? []].includes(e);
|
|
285
294
|
}
|
|
286
|
-
function
|
|
287
|
-
return e ? t ? !0 : n ?
|
|
295
|
+
function kt(e, t, n, r) {
|
|
296
|
+
return e ? t ? !0 : n ? Ot(e, r) : !1 : !1;
|
|
288
297
|
}
|
|
289
|
-
function
|
|
298
|
+
function At(e, t, n, r) {
|
|
290
299
|
if (!e) return !1;
|
|
291
|
-
if (r?.visibility === "hidden") return
|
|
300
|
+
if (r?.visibility === "hidden") return kt(e, t, n, r);
|
|
292
301
|
let i = r?.commentWriteRole ?? "user";
|
|
293
|
-
return i === "admin" ? t : i !== "staff" ||
|
|
302
|
+
return i === "admin" ? t : i !== "staff" || kt(e, t, n, r);
|
|
294
303
|
}
|
|
295
304
|
//#endregion
|
|
296
305
|
//#region src/composables/boardLookup.ts
|
|
297
|
-
var
|
|
298
|
-
function
|
|
299
|
-
|
|
306
|
+
var jt;
|
|
307
|
+
function Mt(e) {
|
|
308
|
+
jt = e;
|
|
300
309
|
}
|
|
301
|
-
function
|
|
302
|
-
return
|
|
310
|
+
function Nt(e) {
|
|
311
|
+
return jt?.(e);
|
|
303
312
|
}
|
|
304
313
|
//#endregion
|
|
305
314
|
//#region src/utils/moderation-strike.ts
|
|
306
|
-
var
|
|
307
|
-
function
|
|
315
|
+
var Pt = 10, Ft = 864e5;
|
|
316
|
+
function It(e, t, n = Date.now(), r = Ft) {
|
|
308
317
|
let i = Math.max(0, Math.floor(Number(e) || 0));
|
|
309
318
|
if (i <= 0) return 0;
|
|
310
319
|
let a = typeof t == "number" && Number.isFinite(t) ? t : null;
|
|
@@ -312,7 +321,7 @@ function Pt(e, t, n = Date.now(), r = Nt) {
|
|
|
312
321
|
let o = Math.floor((n - a) / r);
|
|
313
322
|
return Math.max(0, i - o);
|
|
314
323
|
}
|
|
315
|
-
function
|
|
324
|
+
function Lt(e, t, n = Date.now(), r = 10, i = Ft) {
|
|
316
325
|
let a = Math.max(0, Math.floor(Number(e) || 0));
|
|
317
326
|
if (a < r) return null;
|
|
318
327
|
let o = typeof t == "number" && Number.isFinite(t) ? t : null;
|
|
@@ -320,10 +329,10 @@ function Ft(e, t, n = Date.now(), r = 10, i = Nt) {
|
|
|
320
329
|
let s = o + (a - (r - 1)) * i;
|
|
321
330
|
return s > n ? s : null;
|
|
322
331
|
}
|
|
323
|
-
function
|
|
324
|
-
return
|
|
332
|
+
function Rt(e, t, n = Date.now(), r = 10, i = Ft) {
|
|
333
|
+
return Lt(e, t, n, r, i) != null;
|
|
325
334
|
}
|
|
326
|
-
function
|
|
335
|
+
function zt(e, t = Date.now(), n) {
|
|
327
336
|
let r = Math.max(0, e - t), i = Math.ceil(r / 36e5);
|
|
328
337
|
if (i <= 0) return A(n, "moderationStrike.liftSoon");
|
|
329
338
|
if (i < 24) return A(n, "moderationStrike.liftInHours", { hours: i });
|
|
@@ -333,7 +342,7 @@ function Lt(e, t = Date.now(), n) {
|
|
|
333
342
|
hours: o
|
|
334
343
|
});
|
|
335
344
|
}
|
|
336
|
-
function
|
|
345
|
+
function Bt(e) {
|
|
337
346
|
if (e == null) return null;
|
|
338
347
|
if (typeof e == "number" && Number.isFinite(e)) return e;
|
|
339
348
|
if (typeof e == "object" && e && "toMillis" in e && typeof e.toMillis == "function") return e.toMillis();
|
|
@@ -345,26 +354,26 @@ function Rt(e) {
|
|
|
345
354
|
}
|
|
346
355
|
//#endregion
|
|
347
356
|
//#region src/composables/useMemiBoardAuth.ts
|
|
348
|
-
var
|
|
349
|
-
function
|
|
357
|
+
var Vt = "__MEMI_BOARD_AUTH_STATE__";
|
|
358
|
+
function Ht() {
|
|
350
359
|
let e = globalThis;
|
|
351
|
-
return e[
|
|
360
|
+
return e[Vt] || (e[Vt] = {
|
|
352
361
|
role: I("user"),
|
|
353
362
|
rolePending: I(!0),
|
|
354
363
|
rawBlockCount: I(0),
|
|
355
364
|
blockAtMs: I(null),
|
|
356
365
|
watchStarted: !1
|
|
357
|
-
}), e[
|
|
366
|
+
}), e[Vt];
|
|
358
367
|
}
|
|
359
|
-
function
|
|
360
|
-
let e = v(), { t, locale: n } = j(), r =
|
|
368
|
+
function Ut() {
|
|
369
|
+
let e = v(), { t, locale: n } = j(), r = Ht(), i = Z(), o = Ne(), s = Ae(), { role: c, rolePending: l, rawBlockCount: u, blockAtMs: d } = r;
|
|
361
370
|
function f() {
|
|
362
371
|
let t = e.moderation?.blockBanThreshold;
|
|
363
372
|
return t === 0 ? 0 : typeof t == "number" && t > 0 ? Math.floor(t) : 10;
|
|
364
373
|
}
|
|
365
374
|
function p() {
|
|
366
375
|
let t = e.moderation?.blockBanDecayMs;
|
|
367
|
-
return typeof t == "number" && t > 0 ? t :
|
|
376
|
+
return typeof t == "number" && t > 0 ? t : Ft;
|
|
368
377
|
}
|
|
369
378
|
function m(e) {
|
|
370
379
|
return h(i, M(), e);
|
|
@@ -380,7 +389,7 @@ function Vt() {
|
|
|
380
389
|
u.value = 0, d.value = null;
|
|
381
390
|
return;
|
|
382
391
|
}
|
|
383
|
-
u.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), d.value =
|
|
392
|
+
u.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), d.value = Bt(e.moderationBlockAt);
|
|
384
393
|
}
|
|
385
394
|
async function _(e) {
|
|
386
395
|
let t = m(e.uid), n = a(i, M(), e.uid), r = await V(t);
|
|
@@ -428,14 +437,14 @@ function Vt() {
|
|
|
428
437
|
l.value = !1;
|
|
429
438
|
}
|
|
430
439
|
}, { immediate: !0 }));
|
|
431
|
-
let b = P(() => !!s.value), x = P(() => c.value === "owner"), S = P(() => c.value === "manager"), C = P(() => x.value || c.value === "admin"), w = P(() => S.value || c.value === "staff"), T = P(() => C.value || w.value), E = P(() =>
|
|
440
|
+
let b = P(() => !!s.value), x = P(() => c.value === "owner"), S = P(() => c.value === "manager"), C = P(() => x.value || c.value === "admin"), w = P(() => S.value || c.value === "staff"), T = P(() => C.value || w.value), E = P(() => It(u.value, d.value, Date.now(), p())), D = P(() => {
|
|
432
441
|
let e = f();
|
|
433
|
-
return e <= 0 ? !1 :
|
|
442
|
+
return e <= 0 ? !1 : Rt(u.value, d.value, Date.now(), e, p());
|
|
434
443
|
}), ee = P(() => {
|
|
435
444
|
let e = f();
|
|
436
445
|
if (e <= 0) return null;
|
|
437
|
-
let t =
|
|
438
|
-
return t == null ? null :
|
|
446
|
+
let t = Lt(u.value, d.value, Date.now(), e, p());
|
|
447
|
+
return t == null ? null : zt(t, Date.now(), n.value);
|
|
439
448
|
}), O = P(() => {
|
|
440
449
|
if (!D.value) return null;
|
|
441
450
|
let e = ee.value;
|
|
@@ -462,12 +471,12 @@ function Vt() {
|
|
|
462
471
|
});
|
|
463
472
|
let o = (a.exists() ? Math.max(0, Math.floor(Number(a.data().moderationBlockCount) || 0)) : 0) + 1, c = Date.now();
|
|
464
473
|
u.value = o, d.value = c;
|
|
465
|
-
let l =
|
|
474
|
+
let l = It(o, c, c, p()), h = Rt(o, c, c, r, p()), g = t("auth.strikeSuffix", {
|
|
466
475
|
count: l,
|
|
467
476
|
threshold: r
|
|
468
477
|
});
|
|
469
478
|
if (h) {
|
|
470
|
-
let e =
|
|
479
|
+
let e = Lt(o, c, c, r, p()), i = e == null ? null : zt(e, c, n.value);
|
|
471
480
|
g = i ? t("auth.strikeRestrictedUntil", {
|
|
472
481
|
count: l,
|
|
473
482
|
until: i
|
|
@@ -501,10 +510,10 @@ function Vt() {
|
|
|
501
510
|
await Be(k());
|
|
502
511
|
}
|
|
503
512
|
function oe(e) {
|
|
504
|
-
return
|
|
513
|
+
return kt(s.value?.uid, C.value, w.value, e ? Nt(e) : void 0);
|
|
505
514
|
}
|
|
506
515
|
function se(e) {
|
|
507
|
-
return s.value ? C.value ? !0 : S.value && !!e && (
|
|
516
|
+
return s.value ? C.value ? !0 : S.value && !!e && (Nt(e)?.allowedManagerUids ?? []).includes(s.value.uid) : !1;
|
|
508
517
|
}
|
|
509
518
|
function ce(e) {
|
|
510
519
|
return b.value && (s.value?.uid === e.authorUid || oe(e.boardId));
|
|
@@ -546,34 +555,34 @@ function Vt() {
|
|
|
546
555
|
}
|
|
547
556
|
//#endregion
|
|
548
557
|
//#region src/utils/imeSafeSubmit.ts
|
|
549
|
-
var
|
|
550
|
-
function
|
|
558
|
+
var Wt = /* @__PURE__ */ new WeakMap();
|
|
559
|
+
function Gt(e) {
|
|
551
560
|
let t = e.currentTarget;
|
|
552
561
|
return t instanceof HTMLButtonElement ? t : t instanceof Element ? t.closest("button") : null;
|
|
553
562
|
}
|
|
554
|
-
function
|
|
563
|
+
function Kt(e) {
|
|
555
564
|
if (e.pointerType !== "mouse" || e.button !== 0) return;
|
|
556
|
-
let t =
|
|
565
|
+
let t = Gt(e), n = t?.form;
|
|
557
566
|
if (!t || !n || t.disabled) return;
|
|
558
|
-
|
|
567
|
+
Wt.set(t, Date.now() + 1e3);
|
|
559
568
|
let r = document.activeElement;
|
|
560
569
|
r instanceof HTMLElement && r.blur(), setTimeout(() => n.requestSubmit(t), 0);
|
|
561
570
|
}
|
|
562
|
-
function
|
|
563
|
-
let t =
|
|
571
|
+
function qt(e) {
|
|
572
|
+
let t = Gt(e);
|
|
564
573
|
if (!t) return;
|
|
565
|
-
let n =
|
|
566
|
-
n && Date.now() < n && (e.preventDefault(),
|
|
574
|
+
let n = Wt.get(t);
|
|
575
|
+
n && Date.now() < n && (e.preventDefault(), Wt.delete(t));
|
|
567
576
|
}
|
|
568
577
|
//#endregion
|
|
569
578
|
//#region src/utils/findReadablePost.ts
|
|
570
|
-
async function
|
|
579
|
+
async function Jt(e, t, n, r, i, a) {
|
|
571
580
|
let s = [X("boardId", "==", n), X("slug", "==", r)];
|
|
572
581
|
return a ? (await H(K(o(e, t), ...s, U(1)))).docs[0] : (await Promise.all([H(K(o(e, t), ...s, X("listed", "==", !0), X("isPublished", "==", !0), U(1))), ...i ? [H(K(o(e, t), ...s, X("authorUid", "==", i), U(1)))] : []])).flatMap((e) => e.docs)[0];
|
|
573
582
|
}
|
|
574
583
|
//#endregion
|
|
575
584
|
//#region src/utils/extractEditorImageUrls.ts
|
|
576
|
-
function
|
|
585
|
+
function Yt(e) {
|
|
577
586
|
if (!e) return [];
|
|
578
587
|
let t = /* @__PURE__ */ new Set();
|
|
579
588
|
for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
@@ -582,7 +591,7 @@ function qt(e) {
|
|
|
582
591
|
}
|
|
583
592
|
//#endregion
|
|
584
593
|
//#region src/utils/storagePath.ts
|
|
585
|
-
function
|
|
594
|
+
function Xt(e) {
|
|
586
595
|
try {
|
|
587
596
|
let t = new URL(e), n = t.hostname.toLowerCase();
|
|
588
597
|
if (!(n === "firebasestorage.googleapis.com" || n.endsWith(".firebasestorage.app"))) return null;
|
|
@@ -592,107 +601,107 @@ function Jt(e) {
|
|
|
592
601
|
return null;
|
|
593
602
|
}
|
|
594
603
|
}
|
|
595
|
-
function
|
|
604
|
+
function Zt(e) {
|
|
596
605
|
return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
|
|
597
606
|
}
|
|
598
607
|
//#endregion
|
|
599
608
|
//#region src/utils/deletePostCascade.ts
|
|
600
|
-
async function
|
|
609
|
+
async function Qt(e) {
|
|
601
610
|
try {
|
|
602
611
|
let t = await Ke(e);
|
|
603
|
-
await Promise.all([...t.items.map((e) => He(e).catch(() => {})), ...t.prefixes.map((e) =>
|
|
612
|
+
await Promise.all([...t.items.map((e) => He(e).catch(() => {})), ...t.prefixes.map((e) => Qt(e))]);
|
|
604
613
|
} catch {}
|
|
605
614
|
}
|
|
606
|
-
async function
|
|
607
|
-
await
|
|
615
|
+
async function $t(e, t) {
|
|
616
|
+
await Qt(qe(e, `${t}/images`)), await Qt(qe(e, `${t}/attachments`)), await Qt(qe(e, `${t}/comment-images`));
|
|
608
617
|
}
|
|
609
|
-
var
|
|
610
|
-
async function
|
|
618
|
+
var en = 450;
|
|
619
|
+
async function tn(e, t, n) {
|
|
611
620
|
return (await H(K(g(e, t), X("postId", "==", n)))).docs.flatMap((e) => ue(e.data().images, `${x(t, n)}/comment-images/`));
|
|
612
621
|
}
|
|
613
|
-
async function
|
|
622
|
+
async function nn(e, t) {
|
|
614
623
|
let n = [...new Set([...t].filter(Boolean))];
|
|
615
624
|
await Promise.all(n.map((t) => He(qe(e, t)).catch(() => {})));
|
|
616
625
|
}
|
|
617
|
-
async function
|
|
626
|
+
async function rn(e, t) {
|
|
618
627
|
let n = await H(t);
|
|
619
|
-
for (let t = 0; t < n.docs.length; t +=
|
|
628
|
+
for (let t = 0; t < n.docs.length; t += en) {
|
|
620
629
|
let r = Oe(e);
|
|
621
|
-
n.docs.slice(t, t +
|
|
630
|
+
n.docs.slice(t, t + en).forEach((e) => r.delete(e.ref)), await r.commit();
|
|
622
631
|
}
|
|
623
632
|
}
|
|
624
|
-
async function
|
|
625
|
-
await
|
|
633
|
+
async function an(e, t, n) {
|
|
634
|
+
await rn(e, K(b(e, t), X("postId", "==", n)));
|
|
626
635
|
}
|
|
627
|
-
async function
|
|
628
|
-
await
|
|
636
|
+
async function on(e, t, n) {
|
|
637
|
+
await rn(e, K(b(e, t), X("commentId", "==", n), X("target", "==", "comment")));
|
|
629
638
|
}
|
|
630
|
-
async function
|
|
639
|
+
async function sn(e, t, n, r) {
|
|
631
640
|
let i = y(e, n, r), a = s(e, n, r), [o, c] = await Promise.all([V(i), V(a)]);
|
|
632
641
|
if (!o.exists()) return;
|
|
633
|
-
let l = o.data(), u = c.exists() ? String(c.data().content ?? "") : "", d = await
|
|
642
|
+
let l = o.data(), u = c.exists() ? String(c.data().content ?? "") : "", d = await tn(e, n, r), f = /* @__PURE__ */ new Set(), p = /* @__PURE__ */ new Set();
|
|
634
643
|
for (let e of l.attachments ?? []) {
|
|
635
644
|
if (!e?.path) continue;
|
|
636
645
|
f.add(e.path);
|
|
637
|
-
let t =
|
|
646
|
+
let t = Zt(e.path);
|
|
638
647
|
t && t !== r && p.add(t);
|
|
639
648
|
}
|
|
640
|
-
for (let e of
|
|
641
|
-
let t =
|
|
649
|
+
for (let e of Yt(u)) {
|
|
650
|
+
let t = Xt(e);
|
|
642
651
|
if (!t) continue;
|
|
643
652
|
f.add(t);
|
|
644
653
|
let n = t.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
|
|
645
654
|
n !== t && n.includes("/images/thumbnails/") && f.add(n);
|
|
646
655
|
let i = t.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
|
|
647
656
|
i && !t.includes("/thumbnails/") && f.add(`${i[1]}thumbnails/${i[2]}.jpg`);
|
|
648
|
-
let a =
|
|
657
|
+
let a = Zt(t);
|
|
649
658
|
a && a !== r && p.add(a);
|
|
650
659
|
}
|
|
651
|
-
await
|
|
652
|
-
for (let e of p) await
|
|
653
|
-
await
|
|
660
|
+
await $t(t, x(n, r));
|
|
661
|
+
for (let e of p) await $t(t, x(n, e));
|
|
662
|
+
await nn(t, d), await nn(t, f), await rn(e, K(g(e, n), X("postId", "==", r))), await an(e, n, r), await ve(a), await ve(i);
|
|
654
663
|
}
|
|
655
664
|
//#endregion
|
|
656
665
|
//#region src/utils/postBody.ts
|
|
657
|
-
function
|
|
666
|
+
function cn(e) {
|
|
658
667
|
return String(e || "").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<img\b[^>]*>/gi, " ").replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, " ").replace(/<video\b[\s\S]*?<\/video>/gi, " ").replace(/<audio\b[\s\S]*?<\/audio>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /gi, " ").replace(/ /g, " ").replace(/&[a-z]+;/gi, " ").replace(/&#\d+;/g, " ").replace(/[\u200B-\u200D\uFEFF\u00AD]/g, "").replace(/\s+/g, " ").trim();
|
|
659
668
|
}
|
|
660
|
-
function
|
|
661
|
-
let n =
|
|
669
|
+
function ln(e, t = 40) {
|
|
670
|
+
let n = cn(e);
|
|
662
671
|
if (!n) return "";
|
|
663
672
|
if (n.length <= t) return n;
|
|
664
673
|
let r = n.slice(0, t), i = r.lastIndexOf(" ");
|
|
665
674
|
return `${(i >= Math.floor(t * .6) ? r.slice(0, i) : r).trimEnd()}…`;
|
|
666
675
|
}
|
|
667
|
-
function
|
|
668
|
-
return
|
|
676
|
+
function un(e) {
|
|
677
|
+
return cn(e).length > 0;
|
|
669
678
|
}
|
|
670
|
-
function
|
|
679
|
+
function dn(e, t) {
|
|
671
680
|
return /<img\b/i.test(String(e || "")) ? !0 : (t || []).some((e) => !!e?.type?.startsWith("image/"));
|
|
672
681
|
}
|
|
673
|
-
function
|
|
674
|
-
return !
|
|
682
|
+
function fn(e) {
|
|
683
|
+
return !un(e);
|
|
675
684
|
}
|
|
676
685
|
//#endregion
|
|
677
686
|
//#region src/utils/postPreview.ts
|
|
678
|
-
function
|
|
687
|
+
function pn(e) {
|
|
679
688
|
return e.replace(/&/g, "&").replace(/"/g, "\"").replace(/'/g, "'");
|
|
680
689
|
}
|
|
681
|
-
function
|
|
690
|
+
function mn(e) {
|
|
682
691
|
if (e) try {
|
|
683
|
-
let t = new URL(
|
|
692
|
+
let t = new URL(pn(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
|
|
684
693
|
if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
|
|
685
694
|
if (n.endsWith("youtube.com") || n === "youtube-nocookie.com") return t.pathname.startsWith("/embed/") || t.pathname.startsWith("/shorts/") ? t.pathname.split("/")[2] : t.searchParams.get("v") || void 0;
|
|
686
695
|
} catch {
|
|
687
696
|
return;
|
|
688
697
|
}
|
|
689
698
|
}
|
|
690
|
-
function
|
|
691
|
-
let t =
|
|
699
|
+
function hn(e) {
|
|
700
|
+
let t = mn(e.videoUrl);
|
|
692
701
|
return t ? `https://i.ytimg.com/vi/${t}/hqdefault.jpg` : e.previewImage || e.attachments?.find((e) => e.type.startsWith("image/"))?.url;
|
|
693
702
|
}
|
|
694
|
-
function
|
|
695
|
-
let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ?
|
|
703
|
+
function gn(e, t = []) {
|
|
704
|
+
let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ? pn(r[1]) : a, s = o ? mn(o) : void 0, c = n ? pn(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
|
|
696
705
|
return {
|
|
697
706
|
summary: e.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|h[1-6]|blockquote)>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/[^\S\n]+/g, " ").replace(/ *\n */g, "\n").replace(/\n{2,}/g, "\n").trim().slice(0, 160),
|
|
698
707
|
...c ? { previewImage: c } : {},
|
|
@@ -701,10 +710,10 @@ function mn(e, t = []) {
|
|
|
701
710
|
}
|
|
702
711
|
//#endregion
|
|
703
712
|
//#region src/utils/saveBoardVisibility.ts
|
|
704
|
-
function
|
|
713
|
+
function _n(e) {
|
|
705
714
|
return e === "hidden" ? "hidden" : "public";
|
|
706
715
|
}
|
|
707
|
-
async function
|
|
716
|
+
async function vn(e, t, n, r) {
|
|
708
717
|
let i;
|
|
709
718
|
for (;;) {
|
|
710
719
|
let a = await H(K(t, X("boardId", "==", n), G("__name__"), ...i ? [J(i)] : [], U(400)));
|
|
@@ -714,20 +723,20 @@ async function gn(e, t, n, r) {
|
|
|
714
723
|
if (s && await o.commit(), i = a.docs[a.docs.length - 1], a.docs.length < 400) break;
|
|
715
724
|
}
|
|
716
725
|
}
|
|
717
|
-
async function
|
|
718
|
-
await
|
|
726
|
+
async function yn(e, t, n, r) {
|
|
727
|
+
await vn(e, o(e, t), n, r), await vn(e, g(e, t), n, r);
|
|
719
728
|
}
|
|
720
|
-
async function
|
|
721
|
-
let i = w(e, t, n), a =
|
|
729
|
+
async function bn(e, t, n, r) {
|
|
730
|
+
let i = w(e, t, n), a = _n(r.visibility);
|
|
722
731
|
await Ee(e, async (e) => {
|
|
723
732
|
let t = await e.get(i), n = t.data(), o = n?.visibilityTransition || "";
|
|
724
733
|
if (o && o !== a) throw Error("Finish the pending visibility change before changing it again.");
|
|
725
|
-
return !t.exists() || !o &&
|
|
734
|
+
return !t.exists() || !o && _n(n?.visibility) === a ? (e.set(i, r, { merge: !0 }), !1) : (e.set(i, {
|
|
726
735
|
...r,
|
|
727
|
-
visibility: a === "public" ? "public" :
|
|
736
|
+
visibility: a === "public" ? "public" : _n(n?.visibility),
|
|
728
737
|
visibilityTransition: a
|
|
729
738
|
}, { merge: !0 }), !0);
|
|
730
|
-
}) && (await
|
|
739
|
+
}) && (await yn(e, t, n, a !== "hidden"), await Ee(e, async (e) => {
|
|
731
740
|
if ((await e.get(i)).data()?.visibilityTransition !== a) throw Error("The visibility transition changed. Reload and retry.");
|
|
732
741
|
e.update(i, {
|
|
733
742
|
visibility: a,
|
|
@@ -738,7 +747,7 @@ async function vn(e, t, n, r) {
|
|
|
738
747
|
}
|
|
739
748
|
//#endregion
|
|
740
749
|
//#region src/utils/moderation-prompt.ts
|
|
741
|
-
var
|
|
750
|
+
var xn = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category를 고르고, reason은 한국어로 **무엇이 문제인지** 구체적으로 1문장\n (예: \"욕설(비속어)이 포함되어 게시할 수 없습니다.\", \"광고·스팸성 문구가 있어 게시할 수 없습니다.\")\n 욕설 단어 전체를 그대로 길게 인용하지 말고, 유형(욕설/혐오/스팸 등)을 분명히 밝힌다.\n- flagged=false면 category는 \"none\", reason은 \"\"", Sn = {
|
|
742
751
|
ko: "Korean",
|
|
743
752
|
en: "English",
|
|
744
753
|
ja: "Japanese",
|
|
@@ -750,12 +759,12 @@ var yn = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
|
|
|
750
759
|
ar: "Arabic",
|
|
751
760
|
id: "Indonesian"
|
|
752
761
|
};
|
|
753
|
-
function
|
|
754
|
-
let t =
|
|
762
|
+
function Cn(e) {
|
|
763
|
+
let t = Sn[e] ?? "English", n = fe(e).name;
|
|
755
764
|
return t === n ? t : `${t} (${n})`;
|
|
756
765
|
}
|
|
757
|
-
function
|
|
758
|
-
let t =
|
|
766
|
+
function wn(e) {
|
|
767
|
+
let t = Cn(e);
|
|
759
768
|
return `You are a strict moderator reviewing content for a message board.
|
|
760
769
|
If the text below contains profanity, vulgar slang, sexual harassment, hate speech, threats, spam, or repetitive/flooding advertisements, you MUST reject it (flagged=true).
|
|
761
770
|
|
|
@@ -782,17 +791,17 @@ Output JSON only:
|
|
|
782
791
|
Do not quote the offensive words at length; clearly state the type (profanity/hate/spam, etc.).
|
|
783
792
|
- If flagged=false, category is "none" and reason is ""`;
|
|
784
793
|
}
|
|
785
|
-
function
|
|
786
|
-
return e === "ko" ?
|
|
794
|
+
function Tn(e = "ko") {
|
|
795
|
+
return e === "ko" ? xn : wn(e);
|
|
787
796
|
}
|
|
788
|
-
function
|
|
797
|
+
function En(e, t = "ko") {
|
|
789
798
|
let n = `"""${e.slice(0, 4e3)}"""`;
|
|
790
799
|
return t === "ko" ? `심사할 텍스트:\n${n}` : `Text to review:\n${n}`;
|
|
791
800
|
}
|
|
792
|
-
function
|
|
801
|
+
function Dn(e = "ko") {
|
|
793
802
|
return e === "ko" ? "이 이미지를 심사해 주세요." : "Please review this image.";
|
|
794
803
|
}
|
|
795
|
-
var
|
|
804
|
+
var On = {
|
|
796
805
|
abuse: "욕설·비속어·모욕",
|
|
797
806
|
spam: "스팸·광고",
|
|
798
807
|
adult: "선정적·성인 내용",
|
|
@@ -800,10 +809,10 @@ var En = {
|
|
|
800
809
|
other: "부적절한 내용",
|
|
801
810
|
none: ""
|
|
802
811
|
};
|
|
803
|
-
function
|
|
804
|
-
return !e || e === "none" ? "" : e in
|
|
812
|
+
function kn(e, t = "ko") {
|
|
813
|
+
return !e || e === "none" ? "" : e in On ? A(t, `moderationPrompt.categories.${e}`) : e;
|
|
805
814
|
}
|
|
806
|
-
function
|
|
815
|
+
function An(e) {
|
|
807
816
|
let t = e.locale ?? "ko";
|
|
808
817
|
if (e.via === "local" && e.localHit) {
|
|
809
818
|
let n = e.localHit.trim();
|
|
@@ -811,10 +820,10 @@ function On(e) {
|
|
|
811
820
|
}
|
|
812
821
|
let n = (e.reason || "").trim();
|
|
813
822
|
if (n) return n;
|
|
814
|
-
let r = e.category ?
|
|
823
|
+
let r = e.category ? kn(e.category, t) : "";
|
|
815
824
|
return r ? A(t, "moderationPrompt.categoryBlocked", { category: r }) : A(t, "moderationPrompt.genericBlocked");
|
|
816
825
|
}
|
|
817
|
-
var
|
|
826
|
+
var jn = [
|
|
818
827
|
"씨발",
|
|
819
828
|
"시발",
|
|
820
829
|
"시이발",
|
|
@@ -840,10 +849,10 @@ var kn = [
|
|
|
840
849
|
"bitch",
|
|
841
850
|
"asshole"
|
|
842
851
|
];
|
|
843
|
-
function
|
|
852
|
+
function Mn(e) {
|
|
844
853
|
return new RegExp(e.join("|"), "i");
|
|
845
854
|
}
|
|
846
|
-
var
|
|
855
|
+
var Nn = /* @__PURE__ */ new Set([
|
|
847
856
|
"none",
|
|
848
857
|
"abuse",
|
|
849
858
|
"spam",
|
|
@@ -851,7 +860,7 @@ var jn = /* @__PURE__ */ new Set([
|
|
|
851
860
|
"violence",
|
|
852
861
|
"other"
|
|
853
862
|
]);
|
|
854
|
-
function
|
|
863
|
+
function Pn(e) {
|
|
855
864
|
let t = e.trim();
|
|
856
865
|
if (!t) return null;
|
|
857
866
|
let n;
|
|
@@ -869,7 +878,7 @@ function Mn(e) {
|
|
|
869
878
|
if (typeof n != "object" || !n) return null;
|
|
870
879
|
let r = n;
|
|
871
880
|
if (typeof r.flagged != "boolean") return null;
|
|
872
|
-
let i = typeof r.category == "string" &&
|
|
881
|
+
let i = typeof r.category == "string" && Nn.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
|
|
873
882
|
return {
|
|
874
883
|
flagged: r.flagged,
|
|
875
884
|
category: i,
|
|
@@ -878,11 +887,11 @@ function Mn(e) {
|
|
|
878
887
|
}
|
|
879
888
|
//#endregion
|
|
880
889
|
//#region src/composables/useMemiBoardAutoTranslate.ts
|
|
881
|
-
var
|
|
882
|
-
function
|
|
890
|
+
var Fn = 3e4, In = 45e3, Ln = 12e3, Rn = 3;
|
|
891
|
+
function zn(e) {
|
|
883
892
|
return e.trim().replace(/^```(?:html|text)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
884
893
|
}
|
|
885
|
-
async function
|
|
894
|
+
async function Bn(e, t, n) {
|
|
886
895
|
let r = Array(e.length), i = 0;
|
|
887
896
|
async function a() {
|
|
888
897
|
for (; i < e.length;) {
|
|
@@ -892,7 +901,7 @@ async function Rn(e, t, n) {
|
|
|
892
901
|
}
|
|
893
902
|
return await Promise.all(Array.from({ length: Math.min(t, e.length) }, () => a())), r;
|
|
894
903
|
}
|
|
895
|
-
function
|
|
904
|
+
function Vn(e, t) {
|
|
896
905
|
return new Promise((n, r) => {
|
|
897
906
|
let i = setTimeout(() => r(/* @__PURE__ */ Error(`translation timed out after ${t / 1e3}s`)), t);
|
|
898
907
|
e.then((e) => {
|
|
@@ -902,7 +911,7 @@ function zn(e, t) {
|
|
|
902
911
|
});
|
|
903
912
|
});
|
|
904
913
|
}
|
|
905
|
-
function
|
|
914
|
+
function Hn() {
|
|
906
915
|
let e = Me(), { moderation: t } = v();
|
|
907
916
|
function n(n, r) {
|
|
908
917
|
let i = Xe(e, {
|
|
@@ -928,12 +937,12 @@ function Bn() {
|
|
|
928
937
|
async function r(e, t, r, i) {
|
|
929
938
|
let a = Object.keys(e.fields), o = [
|
|
930
939
|
`Purpose: ${i}`,
|
|
931
|
-
`Source language: ${
|
|
932
|
-
`Translate into: ${r.map((e) => `${e} = ${
|
|
940
|
+
`Source language: ${Cn(t)} (${t})`,
|
|
941
|
+
`Translate into: ${r.map((e) => `${e} = ${Cn(e)}`).join(", ")}`,
|
|
933
942
|
e.context ? `Context (do not translate): ${e.context}` : "",
|
|
934
943
|
"Text (JSON):",
|
|
935
944
|
JSON.stringify(e.fields)
|
|
936
|
-
].filter(Boolean).join("\n"), s = await
|
|
945
|
+
].filter(Boolean).join("\n"), s = await Vn(n(r, a).generateContent(o), Fn), c = JSON.parse(s.response.text()), l = {};
|
|
937
946
|
for (let e of r) {
|
|
938
947
|
let t = c?.[e];
|
|
939
948
|
if (!t) continue;
|
|
@@ -979,14 +988,14 @@ function Bn() {
|
|
|
979
988
|
D();
|
|
980
989
|
let n = t.source ?? "ko", r = (t.targets ?? T.map((e) => e.code)).filter((e) => e !== n), i = e.trim();
|
|
981
990
|
if (!i || !r.length) return {};
|
|
982
|
-
if (i.length > 12e3) throw Error(`translation is limited to ${
|
|
983
|
-
let o = await
|
|
991
|
+
if (i.length > 12e3) throw Error(`translation is limited to ${Ln} characters`);
|
|
992
|
+
let o = await Bn(r, Rn, async (e) => {
|
|
984
993
|
let t = [
|
|
985
|
-
`Source language: ${
|
|
986
|
-
`Translate into: ${e} = ${
|
|
994
|
+
`Source language: ${Cn(n)} (${n})`,
|
|
995
|
+
`Translate into: ${e} = ${Cn(e)}`,
|
|
987
996
|
"HTML:",
|
|
988
997
|
i
|
|
989
|
-
].join("\n"), r =
|
|
998
|
+
].join("\n"), r = zn((await Vn(a().generateContent(t), In)).response.text());
|
|
990
999
|
if (!r) throw Error(`empty translation for ${e}`);
|
|
991
1000
|
return [e, r];
|
|
992
1001
|
});
|
|
@@ -999,7 +1008,7 @@ function Bn() {
|
|
|
999
1008
|
}
|
|
1000
1009
|
//#endregion
|
|
1001
1010
|
//#region src/composables/useMemiBoardSettings.ts
|
|
1002
|
-
var
|
|
1011
|
+
var Un = [
|
|
1003
1012
|
{
|
|
1004
1013
|
id: "free",
|
|
1005
1014
|
label: "자유",
|
|
@@ -1027,17 +1036,17 @@ var Vn = [
|
|
|
1027
1036
|
visibility: "public",
|
|
1028
1037
|
order: 2
|
|
1029
1038
|
}
|
|
1030
|
-
],
|
|
1031
|
-
function
|
|
1039
|
+
], Wn = Un;
|
|
1040
|
+
function Gn(e) {
|
|
1032
1041
|
return e === "hidden" ? "hidden" : "public";
|
|
1033
1042
|
}
|
|
1034
|
-
function
|
|
1043
|
+
function Kn(e, t, n) {
|
|
1035
1044
|
return {
|
|
1036
1045
|
id: e,
|
|
1037
1046
|
label: typeof t.label == "string" && t.label.trim() ? t.label.trim() : e,
|
|
1038
1047
|
order: typeof t.order == "number" ? t.order : n,
|
|
1039
1048
|
description: (typeof t.description == "string" ? t.description : "") || "",
|
|
1040
|
-
visibility:
|
|
1049
|
+
visibility: Gn(t.visibility),
|
|
1041
1050
|
visibilityTransition: t.visibilityTransition === "hidden" || t.visibilityTransition === "public" ? t.visibilityTransition : void 0,
|
|
1042
1051
|
listView: t.listView ?? "default",
|
|
1043
1052
|
editorType: t.editorType ?? (t.listView === "image" ? "image" : "default"),
|
|
@@ -1048,22 +1057,23 @@ function Wn(e, t, n) {
|
|
|
1048
1057
|
locale: N(t.locale) ?? "",
|
|
1049
1058
|
autoTranslate: t.autoTranslate === !0,
|
|
1050
1059
|
autoTranslatePosts: t.autoTranslatePosts === !0,
|
|
1060
|
+
translationLocales: mt(t.translationLocales),
|
|
1051
1061
|
i18n: Q(t.i18n),
|
|
1052
1062
|
i18nSource: ut(t.i18nSource)
|
|
1053
1063
|
};
|
|
1054
1064
|
}
|
|
1055
|
-
function
|
|
1056
|
-
let e = () => M(), t = Z(), r = Me(), { isSignedIn: i, isAdmin: a, isStaff: s, user: c } =
|
|
1065
|
+
function qn() {
|
|
1066
|
+
let e = () => M(), t = Z(), r = Me(), { isSignedIn: i, isAdmin: a, isStaff: s, user: c } = Ut(), { t: l, locale: u } = j(), { translateEntries: d } = Hn(), f = P(() => K(S(t, e()), G("order", "asc"))), { data: m, pending: h } = ke(f, { ssrKey: p(e(), "boards") }), g = P(() => m.value.map((e, t) => Kn(e.id, e, t))), _ = g, v = P(() => g.value.filter((e) => e.visibility !== "hidden")), y = v;
|
|
1057
1067
|
function b(e, t) {
|
|
1058
1068
|
if (!e) return;
|
|
1059
1069
|
let n = g.value.find((t) => t.id === e);
|
|
1060
|
-
return n ?
|
|
1070
|
+
return n ? _t(n.i18n, n.i18nSource, "label", n.label, t ?? u.value) || n.label : e;
|
|
1061
1071
|
}
|
|
1062
1072
|
let x = b;
|
|
1063
1073
|
function C(e, t) {
|
|
1064
1074
|
if (!e) return;
|
|
1065
1075
|
let n = g.value.find((t) => t.id === e), r = n?.description?.trim();
|
|
1066
|
-
if (n && r) return
|
|
1076
|
+
if (n && r) return _t(n.i18n, n.i18nSource, "description", r, t ?? u.value) || void 0;
|
|
1067
1077
|
}
|
|
1068
1078
|
let T = C;
|
|
1069
1079
|
function D(e) {
|
|
@@ -1077,16 +1087,16 @@ function Gn() {
|
|
|
1077
1087
|
function k(e) {
|
|
1078
1088
|
if (e) return g.value.find((t) => t.id === e);
|
|
1079
1089
|
}
|
|
1080
|
-
|
|
1090
|
+
Mt((e) => k(e));
|
|
1081
1091
|
function ne(e) {
|
|
1082
|
-
return
|
|
1092
|
+
return kt(c.value?.uid, a.value, s.value, e ? k(e) : void 0);
|
|
1083
1093
|
}
|
|
1084
1094
|
let re = P(() => g.value.filter((e) => ne(e.id)));
|
|
1085
1095
|
function ie(e, t) {
|
|
1086
1096
|
return {
|
|
1087
1097
|
label: e.label.trim(),
|
|
1088
1098
|
description: (e.description ?? "").trim(),
|
|
1089
|
-
visibility:
|
|
1099
|
+
visibility: Gn(e.visibility),
|
|
1090
1100
|
listView: e.listView ?? "default",
|
|
1091
1101
|
editorType: e.editorType ?? (e.listView === "image" ? "image" : "default"),
|
|
1092
1102
|
writeRole: e.writeRole ?? "user",
|
|
@@ -1096,6 +1106,7 @@ function Gn() {
|
|
|
1096
1106
|
locale: N(e.locale) ?? "",
|
|
1097
1107
|
autoTranslate: e.autoTranslate === !0,
|
|
1098
1108
|
autoTranslatePosts: e.autoTranslatePosts === !0,
|
|
1109
|
+
translationLocales: mt(e.translationLocales),
|
|
1099
1110
|
...e.i18n ? { i18n: e.i18n } : {},
|
|
1100
1111
|
...e.i18nSource ? { i18nSource: e.i18nSource } : {},
|
|
1101
1112
|
order: t,
|
|
@@ -1106,7 +1117,7 @@ function Gn() {
|
|
|
1106
1117
|
if (!n()) return e;
|
|
1107
1118
|
let t = e.label.trim(), r = (e.description ?? "").trim();
|
|
1108
1119
|
if (!t) return e;
|
|
1109
|
-
if (
|
|
1120
|
+
if (xt({
|
|
1110
1121
|
...e,
|
|
1111
1122
|
label: t,
|
|
1112
1123
|
description: r
|
|
@@ -1115,22 +1126,23 @@ function Gn() {
|
|
|
1115
1126
|
label: t,
|
|
1116
1127
|
description: r
|
|
1117
1128
|
};
|
|
1118
|
-
let i = u.value, a = { label: t };
|
|
1119
|
-
r && (
|
|
1120
|
-
let
|
|
1129
|
+
let i = u.value, a = pt(e), o = { label: t };
|
|
1130
|
+
r && (o.description = r);
|
|
1131
|
+
let s = ft(i, o, (await d([{
|
|
1121
1132
|
id: e.id || "board",
|
|
1122
|
-
fields:
|
|
1133
|
+
fields: o,
|
|
1123
1134
|
context: e.id
|
|
1124
1135
|
}], {
|
|
1125
1136
|
source: i,
|
|
1137
|
+
targets: a,
|
|
1126
1138
|
purpose: "board name and short description shown in navigation and page headers"
|
|
1127
1139
|
}))[e.id || "board"]);
|
|
1128
|
-
if (!
|
|
1140
|
+
if (!ht(s, Object.keys(o), [...a, i])) throw Error(l("settings.autoTranslate.failed", { count: a.length }));
|
|
1129
1141
|
return {
|
|
1130
1142
|
...e,
|
|
1131
1143
|
label: t,
|
|
1132
1144
|
description: r,
|
|
1133
|
-
i18n:
|
|
1145
|
+
i18n: s,
|
|
1134
1146
|
i18nSource: {
|
|
1135
1147
|
locale: i,
|
|
1136
1148
|
label: t,
|
|
@@ -1142,12 +1154,12 @@ function Gn() {
|
|
|
1142
1154
|
if (!i.value) throw Error(l("boardSettings.signInRequired"));
|
|
1143
1155
|
let a = n.id.trim(), o = n.label.trim();
|
|
1144
1156
|
if (!a || !o) throw Error(l("boardSettings.idAndNameRequired"));
|
|
1145
|
-
let s =
|
|
1157
|
+
let s = Gn(n.visibility), c = ie({
|
|
1146
1158
|
...n,
|
|
1147
1159
|
label: o,
|
|
1148
1160
|
visibility: s
|
|
1149
1161
|
}, r);
|
|
1150
|
-
await
|
|
1162
|
+
await bn(t, e(), a, c);
|
|
1151
1163
|
}
|
|
1152
1164
|
let oe = A;
|
|
1153
1165
|
async function se(e) {
|
|
@@ -1163,7 +1175,7 @@ function Gn() {
|
|
|
1163
1175
|
for (;;) {
|
|
1164
1176
|
let n = await H(K(o(t, e()), X("boardId", "==", a), U(25)));
|
|
1165
1177
|
if (n.empty) break;
|
|
1166
|
-
for (let i of n.docs) await
|
|
1178
|
+
for (let i of n.docs) await sn(t, We(r), e(), i.id);
|
|
1167
1179
|
}
|
|
1168
1180
|
await ve(w(t, e(), n));
|
|
1169
1181
|
}
|
|
@@ -1217,25 +1229,25 @@ function Gn() {
|
|
|
1217
1229
|
saveCategories: ce,
|
|
1218
1230
|
deleteBoard: le,
|
|
1219
1231
|
deleteCategory: ue,
|
|
1220
|
-
DEFAULT_BOARDS:
|
|
1221
|
-
DEFAULT_CATEGORIES:
|
|
1232
|
+
DEFAULT_BOARDS: Un,
|
|
1233
|
+
DEFAULT_CATEGORIES: Wn
|
|
1222
1234
|
};
|
|
1223
1235
|
}
|
|
1224
1236
|
//#endregion
|
|
1225
1237
|
//#region src/composables/useMemiBoardPosts.ts
|
|
1226
|
-
var
|
|
1227
|
-
function
|
|
1238
|
+
var Jn = /* @__PURE__ */ new Map();
|
|
1239
|
+
function Yn(e) {
|
|
1228
1240
|
if (!e || typeof e != "object") return !1;
|
|
1229
1241
|
let t = "code" in e ? String(e.code) : "";
|
|
1230
1242
|
return t === "permission-denied" || t === "firestore/permission-denied";
|
|
1231
1243
|
}
|
|
1232
|
-
function
|
|
1244
|
+
function Xn(e, t) {
|
|
1233
1245
|
let n = L(e)?.trim();
|
|
1234
1246
|
if (!n) throw Error(A(t, "posts.boardIdRequired"));
|
|
1235
1247
|
return n;
|
|
1236
1248
|
}
|
|
1237
|
-
function
|
|
1238
|
-
let t = Z(), n = Me(), { isBoardHidden: r, getBoard: i } =
|
|
1249
|
+
function Zn(e) {
|
|
1250
|
+
let t = Z(), n = Me(), { isBoardHidden: r, getBoard: i } = qn(), { user: a, canManageBoard: c } = Ut(), { t: l, locale: u } = j(), d = () => M(), f = () => Xn(e, u.value), p = () => o(t, d()), m = (e) => y(t, d(), e), h = (e) => s(t, d(), e);
|
|
1239
1251
|
function g(e, t, n) {
|
|
1240
1252
|
return {
|
|
1241
1253
|
id: e,
|
|
@@ -1254,7 +1266,7 @@ function Yn(e) {
|
|
|
1254
1266
|
return i(f())?.editorType === "image";
|
|
1255
1267
|
}
|
|
1256
1268
|
function b(e) {
|
|
1257
|
-
let t = v(), n =
|
|
1269
|
+
let t = v(), n = un(e.content), r = dn(e.content, e.attachments);
|
|
1258
1270
|
if (t) {
|
|
1259
1271
|
if (!r) throw Error(l("posts.photoRequired"));
|
|
1260
1272
|
return;
|
|
@@ -1266,7 +1278,7 @@ function Yn(e) {
|
|
|
1266
1278
|
return z(p()).id;
|
|
1267
1279
|
}
|
|
1268
1280
|
function S(e) {
|
|
1269
|
-
return
|
|
1281
|
+
return Jt(t, d(), f(), e, a.value?.uid, c(f()));
|
|
1270
1282
|
}
|
|
1271
1283
|
async function C(e = {}) {
|
|
1272
1284
|
let t = e.pageSize ?? 20, n = [X("boardId", "==", f()), X("isPublished", "==", !0)];
|
|
@@ -1296,20 +1308,20 @@ function Yn(e) {
|
|
|
1296
1308
|
post: e
|
|
1297
1309
|
} : { status: "not-found" };
|
|
1298
1310
|
} catch (e) {
|
|
1299
|
-
return
|
|
1311
|
+
return Yn(e) ? { status: "permission-denied" } : {
|
|
1300
1312
|
status: "error",
|
|
1301
1313
|
message: e instanceof Error ? e.message : String(e)
|
|
1302
1314
|
};
|
|
1303
1315
|
}
|
|
1304
1316
|
} catch (e) {
|
|
1305
|
-
return
|
|
1317
|
+
return Yn(e) ? { status: "permission-denied" } : {
|
|
1306
1318
|
status: "error",
|
|
1307
1319
|
message: e instanceof Error ? e.message : String(e)
|
|
1308
1320
|
};
|
|
1309
1321
|
}
|
|
1310
1322
|
}
|
|
1311
1323
|
async function ee(e) {
|
|
1312
|
-
let t = e.trim(), n =
|
|
1324
|
+
let t = e.trim(), n = Jn.get(`${f()}/${t}`);
|
|
1313
1325
|
if (n) return {
|
|
1314
1326
|
status: "ok",
|
|
1315
1327
|
id: n
|
|
@@ -1321,7 +1333,7 @@ function Yn(e) {
|
|
|
1321
1333
|
id: e.id
|
|
1322
1334
|
} : { status: r(f()) ? "permission-denied" : "not-found" };
|
|
1323
1335
|
} catch (e) {
|
|
1324
|
-
return
|
|
1336
|
+
return Yn(e) ? { status: "permission-denied" } : {
|
|
1325
1337
|
status: "error",
|
|
1326
1338
|
message: e instanceof Error ? e.message : String(e)
|
|
1327
1339
|
};
|
|
@@ -1362,8 +1374,8 @@ function Yn(e) {
|
|
|
1362
1374
|
async function k(e) {
|
|
1363
1375
|
b(e);
|
|
1364
1376
|
let n = e.postId || x(), r = v(), i, a;
|
|
1365
|
-
r ? (i = n, a =
|
|
1366
|
-
let o =
|
|
1377
|
+
r ? (i = n, a = ln(e.content)) : (a = e.title.trim(), i = e.slug?.trim() || await O(a));
|
|
1378
|
+
let o = gn(e.content, e.attachments), s = _(), c = Oe(t);
|
|
1367
1379
|
return c.set(m(n), {
|
|
1368
1380
|
boardId: f(),
|
|
1369
1381
|
slug: i,
|
|
@@ -1393,11 +1405,11 @@ function Yn(e) {
|
|
|
1393
1405
|
i18n: e.translations.contentI18n,
|
|
1394
1406
|
i18nSource: e.translations.contentI18nSource
|
|
1395
1407
|
} : {}
|
|
1396
|
-
}), await c.commit(),
|
|
1408
|
+
}), await c.commit(), Jn.set(`${f()}/${i}`, n), i;
|
|
1397
1409
|
}
|
|
1398
1410
|
async function ne(e, t) {
|
|
1399
1411
|
b(t);
|
|
1400
|
-
let n = v() ?
|
|
1412
|
+
let n = v() ? ln(t.content) : t.title.trim(), r = gn(t.content, t.attachments), i = _();
|
|
1401
1413
|
await Promise.all([Y(m(e), {
|
|
1402
1414
|
title: n,
|
|
1403
1415
|
...r,
|
|
@@ -1452,8 +1464,8 @@ function Yn(e) {
|
|
|
1452
1464
|
});
|
|
1453
1465
|
}
|
|
1454
1466
|
async function A(e) {
|
|
1455
|
-
await
|
|
1456
|
-
for (let [t, n] of
|
|
1467
|
+
await sn(t, We(n), d(), e);
|
|
1468
|
+
for (let [t, n] of Jn) n === e && Jn.delete(t);
|
|
1457
1469
|
}
|
|
1458
1470
|
return {
|
|
1459
1471
|
createPostId: x,
|
|
@@ -1471,9 +1483,9 @@ function Yn(e) {
|
|
|
1471
1483
|
deletePost: A
|
|
1472
1484
|
};
|
|
1473
1485
|
}
|
|
1474
|
-
var
|
|
1475
|
-
function
|
|
1476
|
-
let n = Z(), { isAdmin: r, user: i, canManageBoard: a } =
|
|
1486
|
+
var Qn = 10, $n = 10;
|
|
1487
|
+
function er(e, t = {}) {
|
|
1488
|
+
let n = Z(), { isAdmin: r, user: i, canManageBoard: a } = Ut(), s = () => M(), c = t.pageSize ?? Qn, l = P(() => i.value?.uid ?? ""), u = P(() => Xn(e)), d = () => o(n, s()), f = P(() => t.publicOnly === void 0 ? !(r.value || a(u.value)) : typeof t.publicOnly == "boolean" ? t.publicOnly : t.publicOnly.value), p = I([]), m = I([]), h = I([]), g = I([]), _ = I(!0), v = I(!0), y = I(!0), b = I(!1), x = I(""), S = null, C = null, w, T, E;
|
|
1477
1489
|
function D(e) {
|
|
1478
1490
|
return e.createdAt?.toMillis?.() ?? 0;
|
|
1479
1491
|
}
|
|
@@ -1501,7 +1513,7 @@ function Qn(e, t = {}) {
|
|
|
1501
1513
|
});
|
|
1502
1514
|
}
|
|
1503
1515
|
function k() {
|
|
1504
|
-
E?.(), E = W(K(d(), ...O(), X("pinned", "==", !0), G("pinnedAt", "desc"), U(
|
|
1516
|
+
E?.(), E = W(K(d(), ...O(), X("pinned", "==", !0), G("pinnedAt", "desc"), U($n)), (e) => {
|
|
1505
1517
|
g.value = e.docs.map((e) => ({
|
|
1506
1518
|
id: e.id,
|
|
1507
1519
|
...e.data()
|
|
@@ -1567,8 +1579,8 @@ function Qn(e, t = {}) {
|
|
|
1567
1579
|
loadMore: re
|
|
1568
1580
|
};
|
|
1569
1581
|
}
|
|
1570
|
-
function
|
|
1571
|
-
let n = Z(), r = () => M(), i = P(() =>
|
|
1582
|
+
function tr(e, t) {
|
|
1583
|
+
let n = Z(), r = () => M(), i = P(() => Xn(e)), a = P(() => typeof t == "string" ? t : t.value), o = P(() => y(n, r(), a.value)), c = P(() => s(n, r(), a.value)), { data: l, pending: u } = je(o), { data: d, pending: f } = je(c);
|
|
1572
1584
|
return {
|
|
1573
1585
|
post: P(() => {
|
|
1574
1586
|
if (!l.value || l.value.boardId !== i.value) return null;
|
|
@@ -1588,9 +1600,9 @@ function $n(e, t) {
|
|
|
1588
1600
|
}
|
|
1589
1601
|
//#endregion
|
|
1590
1602
|
//#region src/composables/useMemiBoardUserPosts.ts
|
|
1591
|
-
var
|
|
1592
|
-
function
|
|
1593
|
-
let n = Z(), r = () => M(), i = () => o(n, r()), a = t.pageSize ??
|
|
1603
|
+
var nr = 10;
|
|
1604
|
+
function rr(e, t = {}) {
|
|
1605
|
+
let n = Z(), r = () => M(), i = () => o(n, r()), a = t.pageSize ?? nr, s = P(() => L(e)?.trim() || ""), c = I([]), l = I([]), u = I(!0), d = I(!0), f = I(!1), p = I(""), m = null, h = null, g;
|
|
1594
1606
|
function _(e) {
|
|
1595
1607
|
return {
|
|
1596
1608
|
id: e.id,
|
|
@@ -1656,9 +1668,9 @@ function tr(e, t = {}) {
|
|
|
1656
1668
|
}
|
|
1657
1669
|
//#endregion
|
|
1658
1670
|
//#region src/composables/useMemiBoardUserComments.ts
|
|
1659
|
-
var
|
|
1660
|
-
function
|
|
1661
|
-
let n = Z(), r = () => M(), i = () => g(n, r()), a = t.pageSize ??
|
|
1671
|
+
var ir = 10;
|
|
1672
|
+
function ar(e, t = {}) {
|
|
1673
|
+
let n = Z(), r = () => M(), i = () => g(n, r()), a = t.pageSize ?? ir, o = P(() => L(e)?.trim() || ""), s = I([]), c = I([]), l = I(!0), u = I(!0), d = I(!1), f = I(""), p = null, m = null, h;
|
|
1662
1674
|
function _(e) {
|
|
1663
1675
|
return {
|
|
1664
1676
|
id: e.id,
|
|
@@ -1720,11 +1732,11 @@ function rr(e, t = {}) {
|
|
|
1720
1732
|
}
|
|
1721
1733
|
//#endregion
|
|
1722
1734
|
//#region src/composables/useMemiBoardCommentFeed.ts
|
|
1723
|
-
var
|
|
1724
|
-
function
|
|
1735
|
+
var or = 10;
|
|
1736
|
+
function sr(e = "latest", t = {}) {
|
|
1725
1737
|
let n = Z(), r = () => M(), i = () => g(n, r()), a = P(() => {
|
|
1726
1738
|
let e = Math.floor(Number(L(t.pageSize)));
|
|
1727
|
-
return Number.isFinite(e) && e > 0 ? e :
|
|
1739
|
+
return Number.isFinite(e) && e > 0 ? e : or;
|
|
1728
1740
|
}), o = P(() => L(e) === "likes" ? "likes" : "latest"), s = I([]), c = I([]), l = I(!0), u = I(!0), d = I(!1), f = I(""), p = null, m = null, h;
|
|
1729
1741
|
function _(e) {
|
|
1730
1742
|
return {
|
|
@@ -1794,9 +1806,9 @@ function ar(e = "latest", t = {}) {
|
|
|
1794
1806
|
}
|
|
1795
1807
|
//#endregion
|
|
1796
1808
|
//#region src/composables/useMemiBoardAuthorMemo.ts
|
|
1797
|
-
var
|
|
1809
|
+
var cr = 100, lr = /* @__PURE__ */ new Map();
|
|
1798
1810
|
function $(e) {
|
|
1799
|
-
let t =
|
|
1811
|
+
let t = lr.get(e);
|
|
1800
1812
|
return t || (t = he({
|
|
1801
1813
|
text: "",
|
|
1802
1814
|
sentiment: null,
|
|
@@ -1804,10 +1816,10 @@ function $(e) {
|
|
|
1804
1816
|
pending: !1,
|
|
1805
1817
|
saving: !1,
|
|
1806
1818
|
error: ""
|
|
1807
|
-
}),
|
|
1819
|
+
}), lr.set(e, t)), t;
|
|
1808
1820
|
}
|
|
1809
|
-
function
|
|
1810
|
-
let t = Z(), n = () => M(), { user: r } =
|
|
1821
|
+
function ur(e) {
|
|
1822
|
+
let t = Z(), n = () => M(), { user: r } = Ut(), { t: i, formatNumber: a } = j(), o = P(() => L(e)), s = P(() => `${r.value?.uid ?? ""}:${o.value}`), c = P(() => {
|
|
1811
1823
|
let e = r.value?.uid, i = o.value;
|
|
1812
1824
|
return !e || !i ? null : f(t, n(), e, i);
|
|
1813
1825
|
}), l = P({
|
|
@@ -1876,7 +1888,7 @@ function cr(e) {
|
|
|
1876
1888
|
}
|
|
1877
1889
|
//#endregion
|
|
1878
1890
|
//#region src/utils/subscribeCommentRange.ts
|
|
1879
|
-
function
|
|
1891
|
+
function dr(e, t, n, r, i, a) {
|
|
1880
1892
|
return W(K(g(e, t), X("postId", "==", n), ...r.rootId ? [X("rootId", "==", r.rootId), X("isReply", "==", !0)] : [X("parentId", "==", null)], G("createdAt", "asc"), G(B(), "asc"), ...r.hasMore && r.cursor ? [be(r.cursor)] : []), (e) => i(e.docs.map((e) => ({
|
|
1881
1893
|
...e.data(),
|
|
1882
1894
|
id: e.id
|
|
@@ -1884,15 +1896,15 @@ function lr(e, t, n, r, i, a) {
|
|
|
1884
1896
|
}
|
|
1885
1897
|
//#endregion
|
|
1886
1898
|
//#region src/composables/useMemiBoardComments.ts
|
|
1887
|
-
var
|
|
1888
|
-
function
|
|
1899
|
+
var fr = 10, pr = 5, mr = 1e3;
|
|
1900
|
+
function hr(e, { t, formatNumber: n }) {
|
|
1889
1901
|
let r = e.trim();
|
|
1890
1902
|
if (!r) throw Error(t("comments.bodyRequired"));
|
|
1891
|
-
if (r.length > 1e3) throw Error(t("comments.bodyTooLong", { max: n(
|
|
1903
|
+
if (r.length > 1e3) throw Error(t("comments.bodyTooLong", { max: n(mr) }));
|
|
1892
1904
|
return r;
|
|
1893
1905
|
}
|
|
1894
|
-
function
|
|
1895
|
-
let r = Z(), i = Me(), a = () => M(), { isBoardHidden: o } =
|
|
1906
|
+
function gr(e, t, n = {}) {
|
|
1907
|
+
let r = Z(), i = Me(), a = () => M(), { isBoardHidden: o } = qn(), s = j(), { t: c } = s, l = () => g(r, a()), u = P(() => L(e)), f = P(() => L(t)), p = () => !o(u.value), m = I([]), h = I(n.subscribe !== !1), _ = I([]), v = I(!1), b = I(n.subscribe !== !1), S = I(n.subscribe !== !1), C = null, w = 0, T = 0, E;
|
|
1896
1908
|
function D(e) {
|
|
1897
1909
|
return e.createdAt?.toMillis?.() ?? 0;
|
|
1898
1910
|
}
|
|
@@ -1923,7 +1935,7 @@ function mr(e, t, n = {}) {
|
|
|
1923
1935
|
function O() {
|
|
1924
1936
|
E?.();
|
|
1925
1937
|
let e = ++T;
|
|
1926
|
-
E =
|
|
1938
|
+
E = dr(r, a(), f.value, {
|
|
1927
1939
|
cursor: C,
|
|
1928
1940
|
hasMore: b.value
|
|
1929
1941
|
}, (t) => {
|
|
@@ -1946,11 +1958,11 @@ function mr(e, t, n = {}) {
|
|
|
1946
1958
|
U(11)
|
|
1947
1959
|
], n = await H(K(l(), ...t));
|
|
1948
1960
|
if (e !== w) return;
|
|
1949
|
-
let r = n.docs.slice(0,
|
|
1961
|
+
let r = n.docs.slice(0, fr), i = r.map((e) => ({
|
|
1950
1962
|
id: e.id,
|
|
1951
1963
|
...e.data()
|
|
1952
1964
|
})), a = new Set(_.value.map((e) => e.id));
|
|
1953
|
-
_.value.push(...i.filter((e) => !a.has(e.id))), C = r.at(-1) ?? C, b.value = n.size >
|
|
1965
|
+
_.value.push(...i.filter((e) => !a.has(e.id))), C = r.at(-1) ?? C, b.value = n.size > fr, O();
|
|
1954
1966
|
} finally {
|
|
1955
1967
|
e === w && (v.value = !1);
|
|
1956
1968
|
}
|
|
@@ -1976,7 +1988,7 @@ function mr(e, t, n = {}) {
|
|
|
1976
1988
|
postId: f.value,
|
|
1977
1989
|
boardId: u.value,
|
|
1978
1990
|
listed: p(),
|
|
1979
|
-
body:
|
|
1991
|
+
body: hr(e.body, s),
|
|
1980
1992
|
authorUid: e.authorUid,
|
|
1981
1993
|
authorName: e.authorName,
|
|
1982
1994
|
authorPhoto: e.authorPhoto,
|
|
@@ -2024,13 +2036,13 @@ function mr(e, t, n = {}) {
|
|
|
2024
2036
|
if (!e.id) return;
|
|
2025
2037
|
if (e.parentId == null && (e.replyCount ?? 0) > 0) throw Error(c("comments.hasRepliesCannotDelete"));
|
|
2026
2038
|
let t = ue(e.images, `${x(a(), f.value)}/comment-images/${e.id}/`);
|
|
2027
|
-
await
|
|
2039
|
+
await on(r, a(), e.id);
|
|
2028
2040
|
let n = Oe(r);
|
|
2029
2041
|
n.delete(d(r, a(), e.id)), e.parentId && e.rootId && n.update(d(r, a(), e.rootId), { replyCount: we(-1) }), n.update(y(r, a(), f.value), { commentCount: we(-1) }), await n.commit(), await ae(t), _.value = _.value.filter((t) => t.id !== e.id), m.value = m.value.filter((t) => t.id !== e.id);
|
|
2030
2042
|
}
|
|
2031
2043
|
async function ce(e, t, n) {
|
|
2032
2044
|
let i = {
|
|
2033
|
-
body:
|
|
2045
|
+
body: hr(t, s),
|
|
2034
2046
|
updatedAt: q()
|
|
2035
2047
|
};
|
|
2036
2048
|
if (n !== void 0) {
|
|
@@ -2060,7 +2072,7 @@ function mr(e, t, n = {}) {
|
|
|
2060
2072
|
deleteComment: se
|
|
2061
2073
|
};
|
|
2062
2074
|
}
|
|
2063
|
-
function
|
|
2075
|
+
function _r(e, t, n) {
|
|
2064
2076
|
let r = Z(), i = () => M(), a = () => g(r, i()), o = t.trim(), s = I([]), c = I(!1), l = I(!1), u = I(!0), d = null, f, p = 0, m = 0;
|
|
2065
2077
|
function h(e) {
|
|
2066
2078
|
return e.sort((e, t) => (e.createdAt?.toMillis?.() ?? 0) - (t.createdAt?.toMillis?.() ?? 0) || String(e.id).localeCompare(String(t.id)));
|
|
@@ -2068,7 +2080,7 @@ function hr(e, t, n) {
|
|
|
2068
2080
|
function _() {
|
|
2069
2081
|
f?.();
|
|
2070
2082
|
let e = ++m;
|
|
2071
|
-
f =
|
|
2083
|
+
f = dr(r, i(), o, {
|
|
2072
2084
|
rootId: n,
|
|
2073
2085
|
cursor: d,
|
|
2074
2086
|
hasMore: u.value
|
|
@@ -2093,11 +2105,11 @@ function hr(e, t, n) {
|
|
|
2093
2105
|
U(6)
|
|
2094
2106
|
], r = await H(K(a(), ...t));
|
|
2095
2107
|
if (e !== p) return;
|
|
2096
|
-
let i = new Set(s.value.map((e) => e.id)), c = r.docs.slice(0,
|
|
2108
|
+
let i = new Set(s.value.map((e) => e.id)), c = r.docs.slice(0, pr), f = c.map((e) => ({
|
|
2097
2109
|
id: e.id,
|
|
2098
2110
|
...e.data()
|
|
2099
2111
|
})).filter((e) => !i.has(e.id));
|
|
2100
|
-
s.value = h([...s.value, ...f]), d = c.at(-1) ?? d, l.value = !0, u.value = r.size >
|
|
2112
|
+
s.value = h([...s.value, ...f]), d = c.at(-1) ?? d, l.value = !0, u.value = r.size > pr, _();
|
|
2101
2113
|
} finally {
|
|
2102
2114
|
e === p && (c.value = !1);
|
|
2103
2115
|
}
|
|
@@ -2121,7 +2133,7 @@ function hr(e, t, n) {
|
|
|
2121
2133
|
}
|
|
2122
2134
|
//#endregion
|
|
2123
2135
|
//#region src/composables/useMemiBoardLikes.ts
|
|
2124
|
-
function
|
|
2136
|
+
function vr(e, t) {
|
|
2125
2137
|
let n = () => M(), r = Z(), i = Ae(), { t: a } = j(), o = I(!1), s = I(!1), c;
|
|
2126
2138
|
function l(e) {
|
|
2127
2139
|
return me(r, n(), t, e);
|
|
@@ -2160,8 +2172,8 @@ function gr(e, t) {
|
|
|
2160
2172
|
toggleLike: u
|
|
2161
2173
|
};
|
|
2162
2174
|
}
|
|
2163
|
-
var
|
|
2164
|
-
function
|
|
2175
|
+
var yr = Symbol.for("memi-board:comment-likes");
|
|
2176
|
+
function br(e, t) {
|
|
2165
2177
|
let n = () => M(), r = Z(), i = Ae(), { t: a } = j(), o = P(() => L(e)), s = P(() => L(t)), c = I(/* @__PURE__ */ new Set()), l = I(null), u;
|
|
2166
2178
|
R([i, s], ([e, t]) => {
|
|
2167
2179
|
u?.(), u = void 0, c.value = /* @__PURE__ */ new Set(), e && t && (u = W(K(b(r, n()), X("uid", "==", e.uid), X("postId", "==", t), X("target", "==", "comment")), (e) => {
|
|
@@ -2214,7 +2226,7 @@ function vr(e, t) {
|
|
|
2214
2226
|
}
|
|
2215
2227
|
//#endregion
|
|
2216
2228
|
//#region src/composables/useMemiBoardReports.ts
|
|
2217
|
-
var
|
|
2229
|
+
var xr = 200, Sr = [
|
|
2218
2230
|
{
|
|
2219
2231
|
value: "spam",
|
|
2220
2232
|
label: "스팸·광고"
|
|
@@ -2236,10 +2248,10 @@ var yr = 200, br = [
|
|
|
2236
2248
|
label: "기타"
|
|
2237
2249
|
}
|
|
2238
2250
|
];
|
|
2239
|
-
function
|
|
2240
|
-
return
|
|
2251
|
+
function Cr(e, t) {
|
|
2252
|
+
return Sr.some((t) => t.value === e) ? A(t, `reports.reason.${e}`) : e || A(t, "reports.fallbackLabel");
|
|
2241
2253
|
}
|
|
2242
|
-
function
|
|
2254
|
+
function wr(e, t) {
|
|
2243
2255
|
let n = () => M(), r = Z(), i = Ae(), { t: a } = j(), o = I(!1), s = I(!1), c;
|
|
2244
2256
|
R(i, (e) => {
|
|
2245
2257
|
if (c?.(), c = void 0, !e) {
|
|
@@ -2257,7 +2269,7 @@ function Sr(e, t) {
|
|
|
2257
2269
|
if (!l) throw Error(a("reports.signInRequired"));
|
|
2258
2270
|
if (s.value) return;
|
|
2259
2271
|
if (c.authorUid && c.authorUid === l) throw Error(a("reports.cannotReportOwn"));
|
|
2260
|
-
let d =
|
|
2272
|
+
let d = Sr.some((e) => e.value === c.reason) ? c.reason : null;
|
|
2261
2273
|
if (!d) throw Error(a("reports.reasonRequired"));
|
|
2262
2274
|
let f = (c.detail || "").trim();
|
|
2263
2275
|
if (f.length > 200) throw Error(a("reports.detailTooLong", { max: 200 }));
|
|
@@ -2286,9 +2298,9 @@ function Sr(e, t) {
|
|
|
2286
2298
|
submitReport: l
|
|
2287
2299
|
};
|
|
2288
2300
|
}
|
|
2289
|
-
var
|
|
2290
|
-
function
|
|
2291
|
-
let t = () => M(), n = Z(), { isAdmin: r, canManageContent: i, canManageBoard: a } =
|
|
2301
|
+
var Tr = 20;
|
|
2302
|
+
function Er(e = {}) {
|
|
2303
|
+
let t = () => M(), n = Z(), { isAdmin: r, canManageContent: i, canManageBoard: a } = Ut(), o = e.pageSize ?? Tr, s = P(() => L(e.boardId)?.trim() || ""), c = I([]), d = I(!0), f = I(!1), p = I(!1), m = I(""), h = null, g;
|
|
2292
2304
|
function _(e) {
|
|
2293
2305
|
return {
|
|
2294
2306
|
id: e.id,
|
|
@@ -2342,7 +2354,7 @@ function wr(e = {}) {
|
|
|
2342
2354
|
}
|
|
2343
2355
|
//#endregion
|
|
2344
2356
|
//#region src/composables/useMemiBoardViews.ts
|
|
2345
|
-
function
|
|
2357
|
+
function Dr() {
|
|
2346
2358
|
let e = () => M(), t = Z();
|
|
2347
2359
|
function n(t, n) {
|
|
2348
2360
|
let { postsCollection: r } = e();
|
|
@@ -2382,20 +2394,20 @@ function Tr() {
|
|
|
2382
2394
|
}
|
|
2383
2395
|
//#endregion
|
|
2384
2396
|
//#region src/composables/useMemiBoardModeration.ts
|
|
2385
|
-
var
|
|
2397
|
+
var Or = {
|
|
2386
2398
|
flagged: !1,
|
|
2387
2399
|
category: "none",
|
|
2388
2400
|
reason: "",
|
|
2389
2401
|
via: "empty"
|
|
2390
2402
|
};
|
|
2391
|
-
function
|
|
2392
|
-
let t = j(), n = P(() => N(L(e.locale)) ?? t.locale.value), r = v().moderation ?? {}, i = Me(), a = (e) => A(n.value, e), { isWriteRestricted: o, restrictedMessage: s, recordContentModerationBlock: c } =
|
|
2403
|
+
function kr(e = {}) {
|
|
2404
|
+
let t = j(), n = P(() => N(L(e.locale)) ?? t.locale.value), r = v().moderation ?? {}, i = Me(), a = (e) => A(n.value, e), { isWriteRestricted: o, restrictedMessage: s, recordContentModerationBlock: c } = Ut(), l = Mn([...jn, ...r.localBlocklist ?? []]);
|
|
2393
2405
|
function u(e) {
|
|
2394
2406
|
let t = e.match(l);
|
|
2395
2407
|
return t ? {
|
|
2396
2408
|
flagged: !0,
|
|
2397
2409
|
category: "abuse",
|
|
2398
|
-
reason:
|
|
2410
|
+
reason: An({
|
|
2399
2411
|
via: "local",
|
|
2400
2412
|
localHit: t[0] || "",
|
|
2401
2413
|
category: "abuse"
|
|
@@ -2410,7 +2422,7 @@ function Dr(e = {}) {
|
|
|
2410
2422
|
});
|
|
2411
2423
|
return Ze(a, {
|
|
2412
2424
|
model: e,
|
|
2413
|
-
systemInstruction:
|
|
2425
|
+
systemInstruction: Tn(n.value),
|
|
2414
2426
|
generationConfig: {
|
|
2415
2427
|
temperature: 0,
|
|
2416
2428
|
maxOutputTokens: 256,
|
|
@@ -2439,7 +2451,7 @@ function Dr(e = {}) {
|
|
|
2439
2451
|
};
|
|
2440
2452
|
}
|
|
2441
2453
|
function p(e, t = "") {
|
|
2442
|
-
let n =
|
|
2454
|
+
let n = An({
|
|
2443
2455
|
reason: e.reason,
|
|
2444
2456
|
category: e.category,
|
|
2445
2457
|
via: e.via
|
|
@@ -2467,26 +2479,26 @@ function Dr(e = {}) {
|
|
|
2467
2479
|
};
|
|
2468
2480
|
let i = e.trim();
|
|
2469
2481
|
if (!i) return {
|
|
2470
|
-
...
|
|
2482
|
+
...Or,
|
|
2471
2483
|
via: "empty"
|
|
2472
2484
|
};
|
|
2473
2485
|
if (t?.skipFilter) return {
|
|
2474
|
-
...
|
|
2486
|
+
...Or,
|
|
2475
2487
|
via: "admin-override"
|
|
2476
2488
|
};
|
|
2477
2489
|
let c = u(i);
|
|
2478
2490
|
if (c) return m(c);
|
|
2479
2491
|
if (r.enabled === !1) return {
|
|
2480
|
-
...
|
|
2492
|
+
...Or,
|
|
2481
2493
|
via: "disabled"
|
|
2482
2494
|
};
|
|
2483
2495
|
try {
|
|
2484
|
-
let e =
|
|
2496
|
+
let e = Pn((await d().generateContent(En(i, n.value))).response.text());
|
|
2485
2497
|
if (!e) return f();
|
|
2486
2498
|
let t = {
|
|
2487
2499
|
flagged: e.flagged,
|
|
2488
2500
|
category: e.category,
|
|
2489
|
-
reason:
|
|
2501
|
+
reason: An({
|
|
2490
2502
|
reason: e.reason,
|
|
2491
2503
|
category: e.category,
|
|
2492
2504
|
via: "ai"
|
|
@@ -2506,22 +2518,22 @@ function Dr(e = {}) {
|
|
|
2506
2518
|
via: "restricted"
|
|
2507
2519
|
};
|
|
2508
2520
|
if (r.enabled === !1 || !r.moderateImages) return {
|
|
2509
|
-
...
|
|
2521
|
+
...Or,
|
|
2510
2522
|
via: "disabled"
|
|
2511
2523
|
};
|
|
2512
2524
|
try {
|
|
2513
2525
|
let t = await new Promise((t, n) => {
|
|
2514
2526
|
let r = new FileReader();
|
|
2515
2527
|
r.onload = () => t(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(e);
|
|
2516
|
-
}), r =
|
|
2528
|
+
}), r = Pn((await d().generateContent([{ inlineData: {
|
|
2517
2529
|
mimeType: e.type || "image/jpeg",
|
|
2518
2530
|
data: t
|
|
2519
|
-
} },
|
|
2531
|
+
} }, Dn(n.value)])).response.text());
|
|
2520
2532
|
if (!r) return f();
|
|
2521
2533
|
let i = {
|
|
2522
2534
|
flagged: r.flagged,
|
|
2523
2535
|
category: r.category,
|
|
2524
|
-
reason:
|
|
2536
|
+
reason: An({
|
|
2525
2537
|
reason: r.reason,
|
|
2526
2538
|
category: r.category,
|
|
2527
2539
|
via: "ai"
|
|
@@ -2540,7 +2552,7 @@ function Dr(e = {}) {
|
|
|
2540
2552
|
}
|
|
2541
2553
|
//#endregion
|
|
2542
2554
|
//#region src/composables/useMemiBoardUsers.ts
|
|
2543
|
-
var
|
|
2555
|
+
var Ar = [
|
|
2544
2556
|
"owner",
|
|
2545
2557
|
"admin",
|
|
2546
2558
|
"manager",
|
|
@@ -2551,10 +2563,10 @@ var Or = [
|
|
|
2551
2563
|
label: A("ko", `common.role.${e}`),
|
|
2552
2564
|
description: A("ko", `boardUsers.roleDescription.${e}`)
|
|
2553
2565
|
}));
|
|
2554
|
-
function
|
|
2566
|
+
function jr(e = {}) {
|
|
2555
2567
|
let t = () => M(), n = Z(), { t: r } = j(), i = P(() => L(e.enabled) ?? !0 ? c(n, t()) : null), a = ke(i, { ssrKey: p(t(), "users-management") });
|
|
2556
2568
|
async function o(e, i) {
|
|
2557
|
-
if (!
|
|
2569
|
+
if (!Ar.some((e) => e.value === i)) throw Error(r("boardUsers.unsupportedRole"));
|
|
2558
2570
|
await Y(h(n, t(), e), {
|
|
2559
2571
|
role: i,
|
|
2560
2572
|
updatedAt: q()
|
|
@@ -2568,7 +2580,7 @@ function kr(e = {}) {
|
|
|
2568
2580
|
}
|
|
2569
2581
|
//#endregion
|
|
2570
2582
|
//#region src/composables/useMemiBoardUserProfile.ts
|
|
2571
|
-
function
|
|
2583
|
+
function Mr(e) {
|
|
2572
2584
|
let t = Z(), n = () => M(), r = P(() => L(e)), i = P(() => {
|
|
2573
2585
|
let e = r.value;
|
|
2574
2586
|
return e ? h(t, n(), e) : null;
|
|
@@ -2580,7 +2592,7 @@ function Ar(e) {
|
|
|
2580
2592
|
}
|
|
2581
2593
|
//#endregion
|
|
2582
2594
|
//#region src/utils/section.ts
|
|
2583
|
-
var
|
|
2595
|
+
var Nr = [
|
|
2584
2596
|
3,
|
|
2585
2597
|
4,
|
|
2586
2598
|
5,
|
|
@@ -2590,7 +2602,7 @@ var jr = [
|
|
|
2590
2602
|
9,
|
|
2591
2603
|
12
|
|
2592
2604
|
];
|
|
2593
|
-
function
|
|
2605
|
+
function Pr(e) {
|
|
2594
2606
|
return [
|
|
2595
2607
|
{
|
|
2596
2608
|
value: "list",
|
|
@@ -2606,7 +2618,7 @@ function Mr(e) {
|
|
|
2606
2618
|
}
|
|
2607
2619
|
];
|
|
2608
2620
|
}
|
|
2609
|
-
function
|
|
2621
|
+
function Fr(e) {
|
|
2610
2622
|
return [{
|
|
2611
2623
|
value: "latest",
|
|
2612
2624
|
label: A(e, "sectionUtil.sort.latest")
|
|
@@ -2615,18 +2627,18 @@ function Nr(e) {
|
|
|
2615
2627
|
label: A(e, "sectionUtil.sort.likes")
|
|
2616
2628
|
}];
|
|
2617
2629
|
}
|
|
2618
|
-
var
|
|
2619
|
-
function
|
|
2630
|
+
var Ir = Pr("ko"), Lr = Fr("ko");
|
|
2631
|
+
function Rr(e) {
|
|
2620
2632
|
return e === "list" ? 5 : 4;
|
|
2621
2633
|
}
|
|
2622
|
-
function
|
|
2634
|
+
function zr(e, t) {
|
|
2623
2635
|
let n = Math.floor(Number(t));
|
|
2624
|
-
return Number.isFinite(n) ? Math.min(e === "list" ? 20 : 10, Math.max(1, n)) :
|
|
2636
|
+
return Number.isFinite(n) ? Math.min(e === "list" ? 20 : 10, Math.max(1, n)) : Rr(e);
|
|
2625
2637
|
}
|
|
2626
|
-
function
|
|
2638
|
+
function Br(e) {
|
|
2627
2639
|
return e === 12 ? null : 12 - e;
|
|
2628
2640
|
}
|
|
2629
|
-
function
|
|
2641
|
+
function Vr(e) {
|
|
2630
2642
|
return {
|
|
2631
2643
|
3: "col-span-12 md:col-span-3",
|
|
2632
2644
|
4: "col-span-12 md:col-span-4",
|
|
@@ -2638,36 +2650,36 @@ function zr(e) {
|
|
|
2638
2650
|
12: "col-span-12"
|
|
2639
2651
|
}[e];
|
|
2640
2652
|
}
|
|
2641
|
-
function
|
|
2653
|
+
function Hr(e) {
|
|
2642
2654
|
return e === "sm" ? "min-h-40" : e === "lg" ? "min-h-80" : "min-h-56";
|
|
2643
2655
|
}
|
|
2644
|
-
function
|
|
2656
|
+
function Ur(e) {
|
|
2645
2657
|
let t = e.boardId || e.category || "free";
|
|
2646
2658
|
return e.slug ? `/board/${t}/${e.slug}` : `/board/${t}`;
|
|
2647
2659
|
}
|
|
2648
|
-
function
|
|
2660
|
+
function Wr(e) {
|
|
2649
2661
|
return e.kind === "comments" ? e.sort === "likes" ? "/boardComments?sort=likes" : "/boardComments" : e.kind === "list" && e.boardId ? `/board/${e.boardId}` : "/board";
|
|
2650
2662
|
}
|
|
2651
|
-
function
|
|
2663
|
+
function Gr(e) {
|
|
2652
2664
|
if (e.boardId && e.postId) return `/board/${e.boardId}/post/${e.postId}`;
|
|
2653
2665
|
}
|
|
2654
|
-
function
|
|
2666
|
+
function Kr(e) {
|
|
2655
2667
|
return e.previewImage || e.attachments?.find((e) => e.type.startsWith("image/"))?.url;
|
|
2656
2668
|
}
|
|
2657
|
-
function
|
|
2658
|
-
return
|
|
2669
|
+
function qr(e, t) {
|
|
2670
|
+
return vt(e, t).trim() || yt(e, t).trim() || A(t, "sectionUtil.untitledPost");
|
|
2659
2671
|
}
|
|
2660
|
-
function
|
|
2672
|
+
function Jr(e) {
|
|
2661
2673
|
return e === "image" || e === "video" ? "media" : "list";
|
|
2662
2674
|
}
|
|
2663
|
-
function
|
|
2664
|
-
return A(t,
|
|
2675
|
+
function Yr(e, t) {
|
|
2676
|
+
return A(t, Jr(e) === "media" ? "sectionUtil.view.media" : "sectionUtil.view.list");
|
|
2665
2677
|
}
|
|
2666
|
-
function
|
|
2678
|
+
function Xr(e, t) {
|
|
2667
2679
|
return e ? t(e)?.listView ?? "default" : "default";
|
|
2668
2680
|
}
|
|
2669
|
-
function
|
|
2670
|
-
let n = e.map((e) =>
|
|
2681
|
+
function Zr(e, t) {
|
|
2682
|
+
let n = e.map((e) => Xr(e.boardId, t));
|
|
2671
2683
|
if (!n.length) return "default";
|
|
2672
2684
|
if (n.every((e) => e === n[0])) return n[0];
|
|
2673
2685
|
let r = n.filter((e) => e === "image").length, i = n.filter((e) => e === "video").length;
|
|
@@ -2675,27 +2687,27 @@ function Yr(e, t) {
|
|
|
2675
2687
|
}
|
|
2676
2688
|
//#endregion
|
|
2677
2689
|
//#region src/composables/useMemiBoardSections.ts
|
|
2678
|
-
var
|
|
2679
|
-
function
|
|
2690
|
+
var Qr = [];
|
|
2691
|
+
function $r(e) {
|
|
2680
2692
|
let t = Number(e.cols);
|
|
2681
|
-
if (
|
|
2693
|
+
if (Nr.includes(t)) return t;
|
|
2682
2694
|
let n = e.width;
|
|
2683
2695
|
return n === "xs" ? 3 : n === "sm" ? 4 : n === "lg" || Number(e.span) === 2 ? 12 : 6;
|
|
2684
2696
|
}
|
|
2685
|
-
function
|
|
2697
|
+
function ei(e) {
|
|
2686
2698
|
let t = e.height ?? e.size;
|
|
2687
2699
|
return t === "sm" || t === "lg" ? t : "md";
|
|
2688
2700
|
}
|
|
2689
|
-
function
|
|
2701
|
+
function ti(e) {
|
|
2690
2702
|
return e.kind === "post" || e.kind === "comments" ? e.kind : "list";
|
|
2691
2703
|
}
|
|
2692
|
-
function
|
|
2704
|
+
function ni(e) {
|
|
2693
2705
|
return e.sort === "likes" ? "likes" : "latest";
|
|
2694
2706
|
}
|
|
2695
|
-
function
|
|
2707
|
+
function ri(e) {
|
|
2696
2708
|
return (typeof e.boardId == "string" ? e.boardId.trim() : "") || null;
|
|
2697
2709
|
}
|
|
2698
|
-
function
|
|
2710
|
+
function ii(e, t) {
|
|
2699
2711
|
let n = Array.isArray(e.postIds) ? e.postIds : [], r = [];
|
|
2700
2712
|
for (let e of n) {
|
|
2701
2713
|
let n = String(e || "").trim();
|
|
@@ -2703,34 +2715,34 @@ function ni(e, t) {
|
|
|
2703
2715
|
}
|
|
2704
2716
|
return r;
|
|
2705
2717
|
}
|
|
2706
|
-
function
|
|
2718
|
+
function ai(e, t) {
|
|
2707
2719
|
return A(t, "sections.defaultTitle", { n: e + 1 });
|
|
2708
2720
|
}
|
|
2709
|
-
function
|
|
2710
|
-
let r =
|
|
2721
|
+
function oi(e, t, n) {
|
|
2722
|
+
let r = ti(e), i = zr(r, e.count), a = Q(e.i18n), o = ut(e.i18nSource);
|
|
2711
2723
|
return {
|
|
2712
2724
|
id: String(e.id || `box-${t + 1}`),
|
|
2713
|
-
title: String(e.title ||
|
|
2725
|
+
title: String(e.title || ai(t, n)),
|
|
2714
2726
|
showTitle: e.showTitle !== !1,
|
|
2715
2727
|
kind: r,
|
|
2716
|
-
boardId:
|
|
2728
|
+
boardId: ri(e),
|
|
2717
2729
|
count: i,
|
|
2718
|
-
postIds:
|
|
2719
|
-
sort:
|
|
2720
|
-
cols:
|
|
2721
|
-
height:
|
|
2730
|
+
postIds: ii(e, i),
|
|
2731
|
+
sort: ni(e),
|
|
2732
|
+
cols: $r(e),
|
|
2733
|
+
height: ei(e),
|
|
2722
2734
|
order: t,
|
|
2723
2735
|
...a ? { i18n: a } : {},
|
|
2724
2736
|
...o ? { i18nSource: o } : {}
|
|
2725
2737
|
};
|
|
2726
2738
|
}
|
|
2727
|
-
function
|
|
2728
|
-
let { locale: e } = j(), { translateEntries: t } =
|
|
2739
|
+
function si() {
|
|
2740
|
+
let { locale: e } = j(), { translateEntries: t } = Hn(), r = Z(), i = z(r, "settings", "homeLayout"), { data: a, pending: o } = je(i), s = P(() => (Array.isArray(a.value?.blocks) ? a.value.blocks : Qr).map((t, n) => oi(t, n, e.value)));
|
|
2729
2741
|
async function c(r) {
|
|
2730
2742
|
if (!n()) return r;
|
|
2731
2743
|
let i = e.value, a = r.filter((e) => {
|
|
2732
2744
|
let t = e.title.trim();
|
|
2733
|
-
return t ? !(e.i18nSource?.title === t &&
|
|
2745
|
+
return t ? !(e.i18nSource?.title === t && ht(e.i18n, ["title"])) : !1;
|
|
2734
2746
|
});
|
|
2735
2747
|
if (!a.length) return r;
|
|
2736
2748
|
let o = await t(a.map((e) => ({
|
|
@@ -2745,7 +2757,7 @@ function ai() {
|
|
|
2745
2757
|
let t = o[e.id];
|
|
2746
2758
|
if (!t) return e;
|
|
2747
2759
|
let n = e.title.trim(), r = ft(i, { title: n }, t);
|
|
2748
|
-
return
|
|
2760
|
+
return ht(r, ["title"]) ? {
|
|
2749
2761
|
...e,
|
|
2750
2762
|
title: n,
|
|
2751
2763
|
i18n: r,
|
|
@@ -2757,18 +2769,18 @@ function ai() {
|
|
|
2757
2769
|
});
|
|
2758
2770
|
}
|
|
2759
2771
|
function l(t, n) {
|
|
2760
|
-
let r = t, i =
|
|
2772
|
+
let r = t, i = ti(r), a = zr(i, t.count), o = t.title.trim() || ai(n, e.value);
|
|
2761
2773
|
return {
|
|
2762
2774
|
id: t.id,
|
|
2763
2775
|
title: o,
|
|
2764
2776
|
showTitle: t.showTitle !== !1,
|
|
2765
2777
|
kind: i,
|
|
2766
|
-
boardId:
|
|
2778
|
+
boardId: ri(r),
|
|
2767
2779
|
count: a,
|
|
2768
|
-
postIds:
|
|
2769
|
-
sort:
|
|
2770
|
-
cols:
|
|
2771
|
-
height:
|
|
2780
|
+
postIds: ii(r, a),
|
|
2781
|
+
sort: ni(r),
|
|
2782
|
+
cols: $r(r),
|
|
2783
|
+
height: ei(r),
|
|
2772
2784
|
order: n,
|
|
2773
2785
|
...t.i18n ? { i18n: t.i18n } : {},
|
|
2774
2786
|
...t.i18nSource?.title === o ? { i18nSource: t.i18nSource } : {}
|
|
@@ -2799,7 +2811,7 @@ function ai() {
|
|
|
2799
2811
|
}
|
|
2800
2812
|
//#endregion
|
|
2801
2813
|
//#region src/composables/useMemiBoardMiniPosts.ts
|
|
2802
|
-
function
|
|
2814
|
+
function ci(e, t) {
|
|
2803
2815
|
let n = Z(), r = () => M(), i = L(e)?.trim() || "all", a = P(() => {
|
|
2804
2816
|
let i = Math.max(1, Math.floor(Number(L(t)) || 5)), a = L(e)?.trim() || "", s = [X("isPublished", "==", !0), X("listed", "==", !0)];
|
|
2805
2817
|
return a && s.unshift(X("boardId", "==", a)), K(o(n, r()), ...s, G("createdAt", "desc"), U(i));
|
|
@@ -2810,7 +2822,7 @@ function oi(e, t) {
|
|
|
2810
2822
|
error: l
|
|
2811
2823
|
};
|
|
2812
2824
|
}
|
|
2813
|
-
function
|
|
2825
|
+
function li(e) {
|
|
2814
2826
|
let t = Z(), n = () => M(), r = P(() => [...new Set(L(e).map((e) => e.trim()).filter(Boolean))].slice(0, 10)), i = r.value.join(",") || "none", a = P(() => r.value.length ? K(o(t, n()), X(B(), "in", r.value)) : null), { data: s, pending: c, error: l } = ke(a, { ssrKey: p(n(), `mini-picked/${i}`) });
|
|
2815
2827
|
return {
|
|
2816
2828
|
posts: P(() => {
|
|
@@ -2827,22 +2839,42 @@ function si(e) {
|
|
|
2827
2839
|
}
|
|
2828
2840
|
//#endregion
|
|
2829
2841
|
//#region src/data/versionHistory.ts
|
|
2830
|
-
var
|
|
2842
|
+
var ui = [
|
|
2843
|
+
{
|
|
2844
|
+
version: "0.52.0",
|
|
2845
|
+
date: "2026-10-29",
|
|
2846
|
+
highlights: [
|
|
2847
|
+
"게시판마다 번역할 언어를 골라 원하는 언어로만 번역할 수 있어요",
|
|
2848
|
+
"전체 게시판 설정에서 번역 언어·자동 번역·게시판 언어를 모든 게시판에 한 번에 적용할 수 있어요",
|
|
2849
|
+
"번역이 안 된 게시판 이름·설명을 한 번에 번역할 수 있어요",
|
|
2850
|
+
"게시판 소개 문장이 10개 언어로 보이고, 버전 기록은 영어로도 볼 수 있어요"
|
|
2851
|
+
],
|
|
2852
|
+
highlightsEn: [
|
|
2853
|
+
"Choose which languages each board is translated into",
|
|
2854
|
+
"Apply translation languages, auto-translation, and board language to all boards at once",
|
|
2855
|
+
"Translate the names and descriptions of untranslated boards in one go",
|
|
2856
|
+
"The board intro now appears in 10 languages, and the version history is available in English"
|
|
2857
|
+
]
|
|
2858
|
+
},
|
|
2831
2859
|
{
|
|
2832
2860
|
version: "0.51.2",
|
|
2833
2861
|
date: "2026-10-29",
|
|
2834
|
-
highlights: ["목록 카드의 작성자 이름이 오른쪽에 맞춰져요", "작성자 메모는 작성자 메뉴에서 열 수 있어요"]
|
|
2862
|
+
highlights: ["목록 카드의 작성자 이름이 오른쪽에 맞춰져요", "작성자 메모는 작성자 메뉴에서 열 수 있어요"],
|
|
2863
|
+
highlightsEn: ["Author names on list cards are right-aligned", "Open author memos from the author menu"]
|
|
2835
2864
|
},
|
|
2836
2865
|
{
|
|
2837
2866
|
version: "0.51.1",
|
|
2838
2867
|
date: "2026-10-29",
|
|
2839
|
-
highlights: ["글 수정 화면에서도 공지로 등록하거나 해제할 수 있어요"]
|
|
2868
|
+
highlights: ["글 수정 화면에서도 공지로 등록하거나 해제할 수 있어요"],
|
|
2869
|
+
highlightsEn: ["Pin or unpin a notice from the post edit screen too"]
|
|
2840
2870
|
},
|
|
2841
2871
|
{
|
|
2842
2872
|
version: "0.51.0",
|
|
2843
2873
|
date: "2026-10-29",
|
|
2844
2874
|
highlights: ["게시판 매니저가 글을 공지로 지정하거나 해제할 수 있어요", "공지는 목록 맨 위에 따로 모여 보여요"],
|
|
2845
|
-
|
|
2875
|
+
highlightsEn: ["Board managers can pin posts as notices or unpin them", "Notices are grouped at the top of the list"],
|
|
2876
|
+
note: "호스트 프로젝트의 Firestore 규칙과 인덱스도 함께 갱신해야 합니다.",
|
|
2877
|
+
noteEn: "Hosts must also update their Firestore rules and indexes."
|
|
2846
2878
|
},
|
|
2847
2879
|
{
|
|
2848
2880
|
version: "0.50.0",
|
|
@@ -2852,17 +2884,25 @@ var ci = [
|
|
|
2852
2884
|
"최근 업로드를 보호하고, 삭제 전에 연결과 관리자 권한을 다시 확인해요",
|
|
2853
2885
|
"파일 목록과 비공개 댓글 조회의 보안 규칙을 강화했어요"
|
|
2854
2886
|
],
|
|
2855
|
-
|
|
2887
|
+
highlightsEn: [
|
|
2888
|
+
"Find orphaned files, comments and likes in board settings and delete the ones you pick",
|
|
2889
|
+
"Recent uploads are protected, and links and admin rights are checked again before deleting",
|
|
2890
|
+
"Tightened security rules for file listing and private comment reads"
|
|
2891
|
+
],
|
|
2892
|
+
note: "호스트 프로젝트의 Firestore·Storage 규칙도 함께 갱신해야 합니다.",
|
|
2893
|
+
noteEn: "Hosts must also update their Firestore and Storage rules."
|
|
2856
2894
|
},
|
|
2857
2895
|
{
|
|
2858
2896
|
version: "0.49.0",
|
|
2859
2897
|
date: "2026-10-29",
|
|
2860
|
-
highlights: ["보드 설정에서 글 작성 시 자동 번역을 켤 수 있어요. 기본값은 꺼짐이에요", "새 글의 제목·요약·본문을 10개 언어로 번역해 함께 저장해요"]
|
|
2898
|
+
highlights: ["보드 설정에서 글 작성 시 자동 번역을 켤 수 있어요. 기본값은 꺼짐이에요", "새 글의 제목·요약·본문을 10개 언어로 번역해 함께 저장해요"],
|
|
2899
|
+
highlightsEn: ["Turn on automatic translation for new posts in board settings. It is off by default", "New posts save their title, summary and body in 10 languages"]
|
|
2861
2900
|
},
|
|
2862
2901
|
{
|
|
2863
2902
|
version: "0.48.1",
|
|
2864
2903
|
date: "2026-10-29",
|
|
2865
|
-
highlights: ["댓글·답글·댓글 수정 입력창에 이미지를 붙여넣어 첨부할 수 있어요"]
|
|
2904
|
+
highlights: ["댓글·답글·댓글 수정 입력창에 이미지를 붙여넣어 첨부할 수 있어요"],
|
|
2905
|
+
highlightsEn: ["Paste images into the comment, reply and comment edit boxes to attach them"]
|
|
2866
2906
|
},
|
|
2867
2907
|
{
|
|
2868
2908
|
version: "0.48.0",
|
|
@@ -2871,12 +2911,18 @@ var ci = [
|
|
|
2871
2911
|
"댓글과 답글에 이미지를 한 장 첨부할 수 있어요",
|
|
2872
2912
|
"큰 이미지는 올리기 전에 5MB 이하로 줄여요",
|
|
2873
2913
|
"글을 지우면 댓글 이미지도 함께 지워요"
|
|
2914
|
+
],
|
|
2915
|
+
highlightsEn: [
|
|
2916
|
+
"Attach one image to comments and replies",
|
|
2917
|
+
"Large images are shrunk to 5MB or less before upload",
|
|
2918
|
+
"Deleting a post also deletes its comment images"
|
|
2874
2919
|
]
|
|
2875
2920
|
},
|
|
2876
2921
|
{
|
|
2877
2922
|
version: "0.47.1",
|
|
2878
2923
|
date: "2026-10-29",
|
|
2879
|
-
highlights: ["AI 글쓰기 도우미가 실패하면 그 시도는 하루 횟수에서 빠져요", "App Check 연결 안내를 한 번용 토큰과 SPA에 맞게 고쳤어요"]
|
|
2924
|
+
highlights: ["AI 글쓰기 도우미가 실패하면 그 시도는 하루 횟수에서 빠져요", "App Check 연결 안내를 한 번용 토큰과 SPA에 맞게 고쳤어요"],
|
|
2925
|
+
highlightsEn: ["Failed AI writing assistant attempts no longer count toward the daily limit", "Updated the App Check setup guide for one-time tokens and SPAs"]
|
|
2880
2926
|
},
|
|
2881
2927
|
{
|
|
2882
2928
|
version: "0.47.0",
|
|
@@ -2885,12 +2931,18 @@ var ci = [
|
|
|
2885
2931
|
"서비스에 따라 국제화를 끄고 한 언어로만 게시판을 사용할 수 있어요",
|
|
2886
2932
|
"화면 언어 전환은 유지하면서 콘텐츠 번역만 끌 수도 있어요",
|
|
2887
2933
|
"다른 사이트에 설치하는 방법과 번역 설정 안내를 정리했어요"
|
|
2934
|
+
],
|
|
2935
|
+
highlightsEn: [
|
|
2936
|
+
"Turn off internationalization to run the board in a single language",
|
|
2937
|
+
"Or keep the language switcher and turn off only content translation",
|
|
2938
|
+
"Rewrote the guides for installing on other sites and setting up translation"
|
|
2888
2939
|
]
|
|
2889
2940
|
},
|
|
2890
2941
|
{
|
|
2891
2942
|
version: "0.46.2",
|
|
2892
2943
|
date: "2026-10-23",
|
|
2893
|
-
highlights: ["게시판 표시 라벨 옆 버튼으로 이름과 설명을 10개 언어로 번역할 수 있어요", "저장할 때 자동으로 번역할지도 선택할 수 있어요"]
|
|
2944
|
+
highlights: ["게시판 표시 라벨 옆 버튼으로 이름과 설명을 10개 언어로 번역할 수 있어요", "저장할 때 자동으로 번역할지도 선택할 수 있어요"],
|
|
2945
|
+
highlightsEn: ["Translate board names and descriptions into 10 languages with the button next to the display label", "Choose whether to translate automatically on save"]
|
|
2894
2946
|
},
|
|
2895
2947
|
{
|
|
2896
2948
|
version: "0.46.1",
|
|
@@ -2899,6 +2951,11 @@ var ci = [
|
|
|
2899
2951
|
"게시판 이름과 설명을 저장하면 10개 언어로 번역해 둬요",
|
|
2900
2952
|
"글쓰기 AI에서 한 번에 10개 언어로 번역해 저장할 수 있어요",
|
|
2901
2953
|
"보는 언어로 제목과 본문이 보이고, 원문을 고치면 번역 대신 원문이 보여요"
|
|
2954
|
+
],
|
|
2955
|
+
highlightsEn: [
|
|
2956
|
+
"Board names and descriptions are translated into 10 languages when saved",
|
|
2957
|
+
"The AI writing assistant can translate and save into 10 languages at once",
|
|
2958
|
+
"Titles and bodies show in the reader's language, and the original shows instead once it is edited"
|
|
2902
2959
|
]
|
|
2903
2960
|
},
|
|
2904
2961
|
{
|
|
@@ -2908,6 +2965,11 @@ var ci = [
|
|
|
2908
2965
|
"일반 사용자·스태프·매니저·관리자·최고 관리자, 다섯 가지 역할을 지원해요",
|
|
2909
2966
|
"게시판별 매니저가 설정과 스태프 배정을 관리할 수 있어요",
|
|
2910
2967
|
"서비스에 맞게 역할 이름을 바꿀 수 있어요"
|
|
2968
|
+
],
|
|
2969
|
+
highlightsEn: [
|
|
2970
|
+
"Five roles are supported: user, staff, manager, admin and owner",
|
|
2971
|
+
"Per-board managers can manage settings and staff assignments",
|
|
2972
|
+
"Rename roles to fit your service"
|
|
2911
2973
|
]
|
|
2912
2974
|
},
|
|
2913
2975
|
{
|
|
@@ -2917,93 +2979,120 @@ var ci = [
|
|
|
2917
2979
|
"일반 계정 글쓰기·초안 조회와 태그 입력을 고쳤어요",
|
|
2918
2980
|
"댓글·답글 수정과 좋아요가 다른 사용자에게 바로 반영돼요",
|
|
2919
2981
|
"글 삭제·첨부파일 보호·게시판 공개 범위 변경을 보강했어요"
|
|
2982
|
+
],
|
|
2983
|
+
highlightsEn: [
|
|
2984
|
+
"Fixed posting, draft viewing and tag input for regular accounts",
|
|
2985
|
+
"Comment and reply edits and likes show up for other users right away",
|
|
2986
|
+
"Strengthened post deletion, attachment protection and board visibility changes"
|
|
2920
2987
|
]
|
|
2921
2988
|
},
|
|
2922
2989
|
{
|
|
2923
2990
|
version: "0.45.0",
|
|
2924
2991
|
date: "2026-10-01",
|
|
2925
|
-
highlights: ["게시판을 10개 언어로 볼 수 있어요 (한국어·영어·일본어·독일어·프랑스어·스페인어·포르투갈어·중국어·아랍어·인도네시아어)", "게시판 설정에서 게시판마다 언어를 고정하거나 사이트 언어를 따르게 할 수 있어요"]
|
|
2992
|
+
highlights: ["게시판을 10개 언어로 볼 수 있어요 (한국어·영어·일본어·독일어·프랑스어·스페인어·포르투갈어·중국어·아랍어·인도네시아어)", "게시판 설정에서 게시판마다 언어를 고정하거나 사이트 언어를 따르게 할 수 있어요"],
|
|
2993
|
+
highlightsEn: ["View the board in 10 languages (Korean, English, Japanese, German, French, Spanish, Portuguese, Chinese, Arabic and Indonesian)", "In board settings, fix each board's language or follow the site language"]
|
|
2926
2994
|
},
|
|
2927
2995
|
{
|
|
2928
2996
|
version: "0.44.1",
|
|
2929
2997
|
date: "2026-10-01",
|
|
2930
|
-
highlights: ["Mac Safari에서 한글을 입력하고 바로 등록 버튼을 누를 때 두 번 눌러야 하던 문제를 고쳤어요"]
|
|
2998
|
+
highlights: ["Mac Safari에서 한글을 입력하고 바로 등록 버튼을 누를 때 두 번 눌러야 하던 문제를 고쳤어요"],
|
|
2999
|
+
highlightsEn: ["Fixed having to press submit twice after typing Korean in Mac Safari"]
|
|
2931
3000
|
},
|
|
2932
3001
|
{
|
|
2933
3002
|
version: "0.44.0",
|
|
2934
3003
|
date: "2026-10-01",
|
|
2935
|
-
highlights: ["글을 저장하고 미리보기가 뜨기까지 기다리는 시간이 줄었어요 (특히 iPhone·Mac Safari)"]
|
|
3004
|
+
highlights: ["글을 저장하고 미리보기가 뜨기까지 기다리는 시간이 줄었어요 (특히 iPhone·Mac Safari)"],
|
|
3005
|
+
highlightsEn: ["Less waiting between saving a post and seeing the preview (especially on iPhone and Mac Safari)"]
|
|
2936
3006
|
},
|
|
2937
3007
|
{
|
|
2938
3008
|
version: "0.42.0",
|
|
2939
3009
|
date: "2026-08-27",
|
|
2940
|
-
highlights: ["글쓰기에서 문단 옆 + 버튼이나 / 입력으로 제목·목록·이미지 같은 블록을 빠르게 넣을 수 있어요", "문단 메뉴에서 블록을 다른 형식으로 바꾸거나 복제·이동·삭제할 수 있어요"]
|
|
3010
|
+
highlights: ["글쓰기에서 문단 옆 + 버튼이나 / 입력으로 제목·목록·이미지 같은 블록을 빠르게 넣을 수 있어요", "문단 메뉴에서 블록을 다른 형식으로 바꾸거나 복제·이동·삭제할 수 있어요"],
|
|
3011
|
+
highlightsEn: ["Quickly insert blocks such as headings, lists and images with the + button next to a paragraph or by typing /", "Convert, duplicate, move or delete blocks from the paragraph menu"]
|
|
2941
3012
|
},
|
|
2942
3013
|
{
|
|
2943
3014
|
version: "0.41.0",
|
|
2944
3015
|
date: "2026-08-27",
|
|
2945
|
-
highlights: ["AI 도우미 결과를 바로 적용하지 않고 이전/이후를 비교해보고 적용할 수 있어요"]
|
|
3016
|
+
highlights: ["AI 도우미 결과를 바로 적용하지 않고 이전/이후를 비교해보고 적용할 수 있어요"],
|
|
3017
|
+
highlightsEn: ["Compare before and after AI assistant results before applying them"]
|
|
2946
3018
|
},
|
|
2947
3019
|
{
|
|
2948
3020
|
version: "0.40.0",
|
|
2949
3021
|
date: "2026-08-27",
|
|
2950
|
-
highlights: ["AI 글쓰기 도우미에서 원하는 문체를 직접 입력해 바꿀 수 있어요 (예: 경상도 사투리 스타일로)"]
|
|
3022
|
+
highlights: ["AI 글쓰기 도우미에서 원하는 문체를 직접 입력해 바꿀 수 있어요 (예: 경상도 사투리 스타일로)"],
|
|
3023
|
+
highlightsEn: ["Type any writing style you like into the AI writing assistant (e.g. in a Gyeongsang dialect style)"]
|
|
2951
3024
|
},
|
|
2952
3025
|
{
|
|
2953
3026
|
version: "0.39.1",
|
|
2954
3027
|
date: "2026-08-27",
|
|
2955
|
-
highlights: ["관리자는 글쓰기 하단 스위치로 비속어 필터를 끄고 글을 올릴 수 있어요"]
|
|
3028
|
+
highlights: ["관리자는 글쓰기 하단 스위치로 비속어 필터를 끄고 글을 올릴 수 있어요"],
|
|
3029
|
+
highlightsEn: ["Admins can turn off the profanity filter with a switch at the bottom of the editor"]
|
|
2956
3030
|
},
|
|
2957
3031
|
{
|
|
2958
3032
|
version: "0.39.0",
|
|
2959
3033
|
date: "2026-08-21",
|
|
2960
|
-
highlights: ["사진 글에서 원하는 사진을 대표로 고를 수 있어요", "목록 모양과 글쓰기 폼을 각각 선택할 수 있어요"]
|
|
3034
|
+
highlights: ["사진 글에서 원하는 사진을 대표로 고를 수 있어요", "목록 모양과 글쓰기 폼을 각각 선택할 수 있어요"],
|
|
3035
|
+
highlightsEn: ["Pick which photo represents a photo post", "Choose the list layout and the editor form separately"]
|
|
2961
3036
|
},
|
|
2962
3037
|
{
|
|
2963
3038
|
version: "0.38.1",
|
|
2964
3039
|
date: "2026-08-21",
|
|
2965
3040
|
highlights: ["내가 쓴 글을 지울 때 첨부 사진 때문에 막히던 문제를 고쳤어요"],
|
|
2966
|
-
|
|
3041
|
+
highlightsEn: ["Fixed deleting your own post being blocked by attached photos"],
|
|
3042
|
+
note: "호스트는 Storage 예시 규칙의 memiBoardPosts/{postId}/{allPaths=**} 목록 블록을 넣고 배포해야 해요.",
|
|
3043
|
+
noteEn: "Hosts must add and deploy the memiBoardPosts/{postId}/{allPaths=**} list block from the example Storage rules."
|
|
2967
3044
|
},
|
|
2968
3045
|
{
|
|
2969
3046
|
version: "0.38.0",
|
|
2970
3047
|
date: "2026-08-21",
|
|
2971
3048
|
highlights: ["숨김 게시판에는 댓글을 거의 쓰지 않아요. 필요하면 담당 스태프만 남길 수 있어요", "숨김 게시판 댓글은 공개 댓글 모음에 나오지 않아요"],
|
|
2972
|
-
|
|
3049
|
+
highlightsEn: ["Hidden boards rarely take comments. Only assigned staff can leave them when needed", "Comments on hidden boards don't appear in the public comment feed"],
|
|
3050
|
+
note: "호스트는 Firestore 규칙·댓글 listed 인덱스를 다시 배포해야 해요. 공개 보드는 한 번 저장하면 예전 댓글이 목록에 다시 채워져요.",
|
|
3051
|
+
noteEn: "Hosts must redeploy the Firestore rules and the comment listed index. Saving a public board once refills its older comments in the feed."
|
|
2973
3052
|
},
|
|
2974
3053
|
{
|
|
2975
3054
|
version: "0.36.0",
|
|
2976
3055
|
date: "2026-08-19",
|
|
2977
3056
|
highlights: ["게시물을 신고할 수 있어요. 담당 스태프가 내용을 확인해요", "스태프는 지정된 게시판만 관리해요"],
|
|
2978
|
-
|
|
3057
|
+
highlightsEn: ["Report posts. Assigned staff review them", "Staff manage only the boards assigned to them"],
|
|
3058
|
+
note: "호스트는 Firestore 신고 규칙·인덱스를 배포해야 해요.",
|
|
3059
|
+
noteEn: "Hosts must deploy the Firestore report rules and indexes."
|
|
2979
3060
|
},
|
|
2980
3061
|
{
|
|
2981
3062
|
version: "0.35.0",
|
|
2982
3063
|
date: "2026-08-19",
|
|
2983
|
-
highlights: ["메인에 게시목록·게시물·댓글모음 섹션을 올릴 수 있어요", "댓글모음은 최신순이나 좋아요순으로 네 개부터 보여요"]
|
|
3064
|
+
highlights: ["메인에 게시목록·게시물·댓글모음 섹션을 올릴 수 있어요", "댓글모음은 최신순이나 좋아요순으로 네 개부터 보여요"],
|
|
3065
|
+
highlightsEn: ["Put post list, post and comment feed sections on the main page", "The comment feed starts with four comments, newest or most liked first"]
|
|
2984
3066
|
},
|
|
2985
3067
|
{
|
|
2986
3068
|
version: "0.34.0",
|
|
2987
3069
|
date: "2026-08-19",
|
|
2988
3070
|
highlights: ["댓글을 최신순이나 좋아요순으로 모아 볼 수 있어요"],
|
|
2989
|
-
|
|
3071
|
+
highlightsEn: ["Browse comments newest first or most liked first"],
|
|
3072
|
+
note: "호스트는 좋아요순 목록용 Firestore 인덱스(likeCount + createdAt)를 배포해야 해요.",
|
|
3073
|
+
noteEn: "Hosts must deploy the Firestore index for the most-liked list (likeCount + createdAt)."
|
|
2990
3074
|
},
|
|
2991
3075
|
{
|
|
2992
3076
|
version: "0.33.0",
|
|
2993
3077
|
date: "2026-08-19",
|
|
2994
3078
|
highlights: ["댓글과 답글에도 좋아요를 누를 수 있어요", "글을 지우면 댓글 좋아요도 함께 삭제돼요"],
|
|
2995
|
-
|
|
3079
|
+
highlightsEn: ["Like comments and replies too", "Deleting a post also deletes its comment likes"],
|
|
3080
|
+
note: "호스트는 Firestore 규칙과 좋아요 인덱스를 병합·배포해야 댓글 좋아요가 동작해요.",
|
|
3081
|
+
noteEn: "Hosts must merge and deploy the Firestore rules and like indexes for comment likes to work."
|
|
2996
3082
|
},
|
|
2997
3083
|
{
|
|
2998
3084
|
version: "0.32.0",
|
|
2999
3085
|
date: "2026-08-14",
|
|
3000
|
-
highlights: ["글쓰기에서 사진 파일을 올리거나 외부 이미지 주소를 넣을 수 있어요", "게시판을 지우면 그 안의 글·댓글·이미지도 함께 삭제돼요"]
|
|
3086
|
+
highlights: ["글쓰기에서 사진 파일을 올리거나 외부 이미지 주소를 넣을 수 있어요", "게시판을 지우면 그 안의 글·댓글·이미지도 함께 삭제돼요"],
|
|
3087
|
+
highlightsEn: ["Upload photo files or add external image URLs while writing", "Deleting a board also deletes its posts, comments and images"]
|
|
3001
3088
|
},
|
|
3002
3089
|
{
|
|
3003
3090
|
version: "0.31.0",
|
|
3004
3091
|
date: "2026-08-14",
|
|
3005
3092
|
highlights: ["글쓰기 화면에 AI 도우미가 생겼어요. 맞춤법, 다듬기, 이어서 쓰기, 요약, 번역, 제목을 도와줘요", "AI 도우미는 하루 10번까지 사용할 수 있어요"],
|
|
3006
|
-
|
|
3093
|
+
highlightsEn: ["An AI assistant joins the editor. It helps with spelling, polishing, continuing, summarizing, translating and titles", "Use the AI assistant up to 10 times a day"],
|
|
3094
|
+
note: "호스트는 Firestore 규칙에 AI 글쓰기 횟수 필드를 병합·배포해야 제한이 지켜져요.",
|
|
3095
|
+
noteEn: "Hosts must merge and deploy the AI writing count fields in their Firestore rules for the limit to apply."
|
|
3007
3096
|
},
|
|
3008
3097
|
{
|
|
3009
3098
|
version: "0.30.2",
|
|
@@ -3012,12 +3101,18 @@ var ci = [
|
|
|
3012
3101
|
"일반 목록 썸네일을 조금 작은 라운드 사각으로 바꿨어요",
|
|
3013
3102
|
"글쓰기 도구 모음을 서식 메뉴로 줄여 좁은 화면에서도 잘리지 않아요",
|
|
3014
3103
|
"다시 로그인해도 바꿔 둔 프로필 이름과 사진이 유지돼요"
|
|
3104
|
+
],
|
|
3105
|
+
highlightsEn: [
|
|
3106
|
+
"List thumbnails are now slightly smaller rounded squares",
|
|
3107
|
+
"The editor toolbar folds into a format menu so it fits on narrow screens",
|
|
3108
|
+
"Your changed profile name and photo stay after signing in again"
|
|
3015
3109
|
]
|
|
3016
3110
|
},
|
|
3017
3111
|
{
|
|
3018
3112
|
version: "0.30.1",
|
|
3019
3113
|
date: "2026-08-13",
|
|
3020
|
-
highlights: ["게시물 상단의 목록 버튼을 간결한 왼쪽 화살표로 바꿨어요", "이제 main 브랜치에 push하면 npm 배포가 자동으로 진행돼요"]
|
|
3114
|
+
highlights: ["게시물 상단의 목록 버튼을 간결한 왼쪽 화살표로 바꿨어요", "이제 main 브랜치에 push하면 npm 배포가 자동으로 진행돼요"],
|
|
3115
|
+
highlightsEn: ["The list button at the top of a post is now a simple left arrow", "Pushing to the main branch now publishes to npm automatically"]
|
|
3021
3116
|
},
|
|
3022
3117
|
{
|
|
3023
3118
|
version: "0.30.0",
|
|
@@ -3027,13 +3122,21 @@ var ci = [
|
|
|
3027
3122
|
"목록은 게시판 설정에서 정한 보기 방식을 사용하고, 테스트용 보기 전환 버튼은 숨겼어요",
|
|
3028
3123
|
"글쓰기 중 사진 업로드 권한과 Storage 버킷 설정 안내를 보강했어요"
|
|
3029
3124
|
],
|
|
3030
|
-
|
|
3125
|
+
highlightsEn: [
|
|
3126
|
+
"Post with only attached photos and no body text",
|
|
3127
|
+
"Lists use the view chosen in board settings, and the test view switcher is hidden",
|
|
3128
|
+
"Improved guidance on photo upload permissions and Storage bucket setup"
|
|
3129
|
+
],
|
|
3130
|
+
note: "이미지 업로드가 거부되면 호스트의 Storage 규칙과 firebase.json 버킷 설정을 최신 예시로 확인해 주세요.",
|
|
3131
|
+
noteEn: "If image uploads are rejected, check the host's Storage rules and firebase.json bucket settings against the latest example."
|
|
3031
3132
|
},
|
|
3032
3133
|
{
|
|
3033
3134
|
version: "0.29.1",
|
|
3034
3135
|
date: "2026-08-13",
|
|
3035
3136
|
highlights: ["프로필 이름이나 사진을 바꾸면 예전에 작성한 글과 댓글의 작성자 정보도 함께 바뀌어요", "나를 대상으로 작성된 답글의 표시 이름도 새 프로필 이름으로 맞춰져요"],
|
|
3036
|
-
|
|
3137
|
+
highlightsEn: ["Changing your profile name or photo also updates the author info on your earlier posts and comments", "Replies addressed to you show your new profile name too"],
|
|
3138
|
+
note: "이 기능을 쓰려면 호스트가 Firestore 보안 규칙을 최신 예시로 갱신해야 해요.",
|
|
3139
|
+
noteEn: "Hosts must update their Firestore security rules to the latest example to use this."
|
|
3037
3140
|
},
|
|
3038
3141
|
{
|
|
3039
3142
|
version: "0.27.0",
|
|
@@ -3042,23 +3145,32 @@ var ci = [
|
|
|
3042
3145
|
"일반 목록 디자인을 다듬었어요 — 사진이 있는 글은 제목과 작성자 사이에 동그란 미리보기로 보여요",
|
|
3043
3146
|
"조밀 보기를 커뮤니티 게시판처럼 한 줄로 다시 짰어요 — 시간·작성자·조회·추천·게시판을 한 줄에, 사진 있으면 오른쪽에 작은 미리보기가 붙어요",
|
|
3044
3147
|
"두 줄 이상으로 쓴 글은 목록 요약에서도 줄바꿈이 그대로 보여요"
|
|
3148
|
+
],
|
|
3149
|
+
highlightsEn: [
|
|
3150
|
+
"Refined the default list. Posts with photos show a round preview between the title and author",
|
|
3151
|
+
"Rebuilt the dense view as a single line like a community board, with time, author, views, likes and board on one line and a small preview on the right when there is a photo",
|
|
3152
|
+
"Line breaks in multi-line posts are kept in list summaries"
|
|
3045
3153
|
]
|
|
3046
3154
|
},
|
|
3047
3155
|
{
|
|
3048
3156
|
version: "0.26.0",
|
|
3049
3157
|
date: "2026-08-12",
|
|
3050
|
-
highlights: ["다른 사람 글을 보다가 그 사람에 대해 나만 보는 메모를 남길 수 있어요 — 좋음/나쁨 표시도 같이 남겨요", "메모는 작성자 이름 옆 아이콘을 눌러서 쓰고, 다른 사람은(관리자 포함) 절대 못 봐요"]
|
|
3158
|
+
highlights: ["다른 사람 글을 보다가 그 사람에 대해 나만 보는 메모를 남길 수 있어요 — 좋음/나쁨 표시도 같이 남겨요", "메모는 작성자 이름 옆 아이콘을 눌러서 쓰고, 다른 사람은(관리자 포함) 절대 못 봐요"],
|
|
3159
|
+
highlightsEn: ["Leave a private memo about another author while reading their post, with a good or bad mark", "Write memos from the icon next to the author name. Nobody else, not even admins, can see them"]
|
|
3051
3160
|
},
|
|
3052
3161
|
{
|
|
3053
3162
|
version: "0.25.0",
|
|
3054
3163
|
date: "2026-08-12",
|
|
3055
3164
|
highlights: ["\"작성글 보기\"처럼 \"작성한 댓글 보기\"도 생겼어요 — 여러 게시판에 흩어진 내 댓글을 한 곳에서 모아 봐요"],
|
|
3056
|
-
|
|
3165
|
+
highlightsEn: ["Like \"View posts\", there is now \"View comments\" to gather your comments from every board in one place"],
|
|
3166
|
+
note: "이 기능을 쓰려면 호스트가 Firestore 보안 규칙을 최신 예시로 갱신해야 해요.",
|
|
3167
|
+
noteEn: "Hosts must update their Firestore security rules to the latest example to use this."
|
|
3057
3168
|
},
|
|
3058
3169
|
{
|
|
3059
3170
|
version: "0.24.0",
|
|
3060
3171
|
date: "2026-08-11",
|
|
3061
|
-
highlights: ["사진 올리는 코드를 분리해서, 게시판이 더 안정적으로 빌드되고 열려요"]
|
|
3172
|
+
highlights: ["사진 올리는 코드를 분리해서, 게시판이 더 안정적으로 빌드되고 열려요"],
|
|
3173
|
+
highlightsEn: ["Split out the photo upload code so the board builds and loads more reliably"]
|
|
3062
3174
|
},
|
|
3063
3175
|
{
|
|
3064
3176
|
version: "0.23.0",
|
|
@@ -3069,42 +3181,56 @@ var ci = [
|
|
|
3069
3181
|
"\"내가 좋아요한 글\" 같은 기능을 나중에 붙이기 쉬워졌어요",
|
|
3070
3182
|
"날짜에 마우스를 올려야 보이던 작성 시각을 이제 눌러서 볼 수 있어요 — 폰에서도 똑같이 동작해요"
|
|
3071
3183
|
],
|
|
3072
|
-
|
|
3184
|
+
highlightsEn: [
|
|
3185
|
+
"Posts, comments and likes now live in separate memiBoardPosts, memiBoardComments and memiBoardLikes collections instead of under each board. The board ID is a field on each post",
|
|
3186
|
+
"Per-author post lists are lighter, found in one query instead of searching every board",
|
|
3187
|
+
"Features like \"posts I liked\" are easier to add later",
|
|
3188
|
+
"Tap a date to see the exact time instead of hovering, which also works on phones"
|
|
3189
|
+
],
|
|
3190
|
+
note: "기존 게시판 데이터와 호환되지 않아요 — 새로 설치하는 것처럼 다시 시작해야 해요.",
|
|
3191
|
+
noteEn: "Not compatible with existing board data. Start over as if installing fresh."
|
|
3073
3192
|
},
|
|
3074
3193
|
{
|
|
3075
3194
|
version: "0.21.1",
|
|
3076
3195
|
date: "2026-08-07",
|
|
3077
|
-
highlights: ["관리자가 쓴 댓글에 답글을 달 때 오류가 나던 문제를 고쳤어요"]
|
|
3196
|
+
highlights: ["관리자가 쓴 댓글에 답글을 달 때 오류가 나던 문제를 고쳤어요"],
|
|
3197
|
+
highlightsEn: ["Fixed an error when replying to a comment written by an admin"]
|
|
3078
3198
|
},
|
|
3079
3199
|
{
|
|
3080
3200
|
version: "0.21.0",
|
|
3081
3201
|
date: "2026-08-06",
|
|
3082
|
-
highlights: ["카테고리 대신 게시판(보드)만 있어요 — 예전 카테고리 이름이 곧 보드 ID예요", "글은 memiBoards/{보드}/posts, 설정은 settings, 회원 역할은 memiBoardUsers 에 모아요"]
|
|
3202
|
+
highlights: ["카테고리 대신 게시판(보드)만 있어요 — 예전 카테고리 이름이 곧 보드 ID예요", "글은 memiBoards/{보드}/posts, 설정은 settings, 회원 역할은 memiBoardUsers 에 모아요"],
|
|
3203
|
+
highlightsEn: ["Boards replace categories. The old category name is now the board ID", "Posts live in memiBoards/{board}/posts, settings in settings, and member roles in memiBoardUsers"]
|
|
3083
3204
|
},
|
|
3084
3205
|
{
|
|
3085
3206
|
version: "0.20.0",
|
|
3086
3207
|
date: "2026-08-06",
|
|
3087
|
-
highlights: ["게시판 데이터를 memiBoards/{게시판ID}/… 아래에 모았어요 (여러 게시판을 같은 프로젝트에 둘 수 있어요)", "설정·글·사용자 경로가 게시판마다 분리돼요 — 호스트는 boardId만 맞추면 돼요"]
|
|
3208
|
+
highlights: ["게시판 데이터를 memiBoards/{게시판ID}/… 아래에 모았어요 (여러 게시판을 같은 프로젝트에 둘 수 있어요)", "설정·글·사용자 경로가 게시판마다 분리돼요 — 호스트는 boardId만 맞추면 돼요"],
|
|
3209
|
+
highlightsEn: ["Board data now lives under memiBoards/{boardId}/… (so several boards can share one project)", "Settings, post and user paths are separated per board. Hosts only need to match the boardId"]
|
|
3088
3210
|
},
|
|
3089
3211
|
{
|
|
3090
3212
|
version: "0.19.3",
|
|
3091
3213
|
date: "2026-08-06",
|
|
3092
|
-
highlights: ["이미지 목록 게시판은 인스타처럼 제목 없이 사진+내용만 올리면 돼요", "사진을 여러 장 올리고 끌어 순서를 바꿀 수 있어요 (첫 장이 대표)"]
|
|
3214
|
+
highlights: ["이미지 목록 게시판은 인스타처럼 제목 없이 사진+내용만 올리면 돼요", "사진을 여러 장 올리고 끌어 순서를 바꿀 수 있어요 (첫 장이 대표)"],
|
|
3215
|
+
highlightsEn: ["Image list boards work like Instagram, with just photos and text and no title", "Upload several photos and drag to reorder them (the first one is the cover)"]
|
|
3093
3216
|
},
|
|
3094
3217
|
{
|
|
3095
3218
|
version: "0.19.2",
|
|
3096
3219
|
date: "2026-08-06",
|
|
3097
|
-
highlights: ["이미지만 넣고 글 없이 올리는 건 막았어요 — 본문에 글자를 넣어야 해요"]
|
|
3220
|
+
highlights: ["이미지만 넣고 글 없이 올리는 건 막았어요 — 본문에 글자를 넣어야 해요"],
|
|
3221
|
+
highlightsEn: ["Posting images without any text is blocked. The body needs some text"]
|
|
3098
3222
|
},
|
|
3099
3223
|
{
|
|
3100
3224
|
version: "0.19.1",
|
|
3101
3225
|
date: "2026-08-06",
|
|
3102
|
-
highlights: ["공유 미리보기 이미지를 가벼운 썸네일로 쓰도록 바꿨어요 (카톡 등에 더 잘 맞아요)"]
|
|
3226
|
+
highlights: ["공유 미리보기 이미지를 가벼운 썸네일로 쓰도록 바꿨어요 (카톡 등에 더 잘 맞아요)"],
|
|
3227
|
+
highlightsEn: ["Share previews now use a lighter thumbnail image (better for messengers like KakaoTalk)"]
|
|
3103
3228
|
},
|
|
3104
3229
|
{
|
|
3105
3230
|
version: "0.19.0",
|
|
3106
3231
|
date: "2026-08-06",
|
|
3107
|
-
highlights: ["게시판 목록·글 공유 미리보기(제목·설명·대표 이미지)를 모듈만으로 붙일 수 있어요", "호스트가 따로 SEO API를 만들 필요 없이, 공개 글 기준으로 미리보기가 채워져요"]
|
|
3232
|
+
highlights: ["게시판 목록·글 공유 미리보기(제목·설명·대표 이미지)를 모듈만으로 붙일 수 있어요", "호스트가 따로 SEO API를 만들 필요 없이, 공개 글 기준으로 미리보기가 채워져요"],
|
|
3233
|
+
highlightsEn: ["Add share previews (title, description and cover image) for board lists and posts with just the module", "Previews are filled from public posts without the host building its own SEO API"]
|
|
3108
3234
|
},
|
|
3109
3235
|
{
|
|
3110
3236
|
version: "0.18.0",
|
|
@@ -3113,6 +3239,11 @@ var ci = [
|
|
|
3113
3239
|
"게시판을 보임/숨김으로 나눌 수 있어요 — 숨김은 일기장처럼 권한이 있는 사람만 볼 수 있어요",
|
|
3114
3240
|
"권한이 없는 글·게시판은 로딩에 멈추지 않고 안내 메시지로 알려 줘요",
|
|
3115
3241
|
"글 붙여넣기로 넣은 이미지가 파일명과 함께 두 번 들어가던 문제를 고쳤어요"
|
|
3242
|
+
],
|
|
3243
|
+
highlightsEn: [
|
|
3244
|
+
"Boards can be visible or hidden. Hidden boards are like a diary that only people with access can see",
|
|
3245
|
+
"Posts and boards you can't access show a message instead of loading forever",
|
|
3246
|
+
"Fixed pasted images being inserted twice along with their file name"
|
|
3116
3247
|
]
|
|
3117
3248
|
},
|
|
3118
3249
|
{
|
|
@@ -3122,6 +3253,11 @@ var ci = [
|
|
|
3122
3253
|
"게시글 조회수가 생겼어요 — 로그인하지 않아도 조회가 쌓여요",
|
|
3123
3254
|
"목록과 상세에서 조회수를 바로 확인할 수 있어요",
|
|
3124
3255
|
"댓글 수는 모든 목록 보기에서 제목 옆에 같은 방식으로 보여요"
|
|
3256
|
+
],
|
|
3257
|
+
highlightsEn: [
|
|
3258
|
+
"Posts now have view counts, which count even without signing in",
|
|
3259
|
+
"See view counts right in the list and on the post",
|
|
3260
|
+
"Comment counts show next to the title the same way in every list view"
|
|
3125
3261
|
]
|
|
3126
3262
|
},
|
|
3127
3263
|
{
|
|
@@ -3132,22 +3268,31 @@ var ci = [
|
|
|
3132
3268
|
"이미지 목록에서는 사진 위에 제목과 좋아요가 겹쳐 보여요",
|
|
3133
3269
|
"영상 목록에서는 유튜브가 있으면 유튜브 썸네일을 먼저 보여 줘요",
|
|
3134
3270
|
"시간·설명 툴팁이 마우스만 올려도 바로 떠요"
|
|
3271
|
+
],
|
|
3272
|
+
highlightsEn: [
|
|
3273
|
+
"A new dense list view lets you scan posts one line at a time",
|
|
3274
|
+
"The image list overlays the title and likes on the photo",
|
|
3275
|
+
"The video list shows the YouTube thumbnail first when there is one",
|
|
3276
|
+
"Time and description tooltips appear as soon as you hover"
|
|
3135
3277
|
]
|
|
3136
3278
|
},
|
|
3137
3279
|
{
|
|
3138
3280
|
version: "0.15.0",
|
|
3139
3281
|
date: "2026-08-06",
|
|
3140
|
-
highlights: ["게시판마다 짧은 설명을 적어 두고, 제목에 마우스를 올리면 볼 수 있어요", "일반 목록 카드 여백과 썸네일 배치를 더 단정하게 다듬었어요"]
|
|
3282
|
+
highlights: ["게시판마다 짧은 설명을 적어 두고, 제목에 마우스를 올리면 볼 수 있어요", "일반 목록 카드 여백과 썸네일 배치를 더 단정하게 다듬었어요"],
|
|
3283
|
+
highlightsEn: ["Give each board a short description, shown when you hover over its title", "Tidied up the spacing and thumbnail layout of default list cards"]
|
|
3141
3284
|
},
|
|
3142
3285
|
{
|
|
3143
3286
|
version: "0.14.1",
|
|
3144
3287
|
date: "2026-08-06",
|
|
3145
|
-
highlights: ["일반 리스트뷰에 정사각형 썸네일이 생기고, 작성자·시간·좋아요·댓글수를 오른쪽에 보기 좋게 정리했어요", "카테고리 안에서는 같은 카테고리 뱃지가 매 글마다 반복 표시되지 않아요"]
|
|
3288
|
+
highlights: ["일반 리스트뷰에 정사각형 썸네일이 생기고, 작성자·시간·좋아요·댓글수를 오른쪽에 보기 좋게 정리했어요", "카테고리 안에서는 같은 카테고리 뱃지가 매 글마다 반복 표시되지 않아요"],
|
|
3289
|
+
highlightsEn: ["The default list view gains square thumbnails, with author, time, likes and comment count neatly on the right", "Inside a category, the same category badge no longer repeats on every post"]
|
|
3146
3290
|
},
|
|
3147
3291
|
{
|
|
3148
3292
|
version: "0.14.0",
|
|
3149
3293
|
date: "2026-08-06",
|
|
3150
|
-
highlights: ["게시판 설정에서 글쓰기·댓글쓰기 권한과 리스트뷰를 카드로 골라요", "목록 위에서 일반·이미지·영상 보기를 아이콘으로 바로 바꿔볼 수 있어요"]
|
|
3294
|
+
highlights: ["게시판 설정에서 글쓰기·댓글쓰기 권한과 리스트뷰를 카드로 골라요", "목록 위에서 일반·이미지·영상 보기를 아이콘으로 바로 바꿔볼 수 있어요"],
|
|
3295
|
+
highlightsEn: ["Pick write and comment permissions and the list view as cards in board settings", "Switch between default, image and video views with icons above the list"]
|
|
3151
3296
|
},
|
|
3152
3297
|
{
|
|
3153
3298
|
version: "0.13.0",
|
|
@@ -3156,6 +3301,11 @@ var ci = [
|
|
|
3156
3301
|
"게시판마다 담당 스태프를 따로 지정할 수 있어요",
|
|
3157
3302
|
"댓글쓰기 권한도 게시판마다 다르게 정할 수 있어요",
|
|
3158
3303
|
"유튜브 영상이 모바일과 여러 주소 형식에서도 잘 보여요"
|
|
3304
|
+
],
|
|
3305
|
+
highlightsEn: [
|
|
3306
|
+
"Assign staff to each board separately",
|
|
3307
|
+
"Set comment permissions per board too",
|
|
3308
|
+
"YouTube videos play properly on mobile and with more URL formats"
|
|
3159
3309
|
]
|
|
3160
3310
|
},
|
|
3161
3311
|
{
|
|
@@ -3165,45 +3315,58 @@ var ci = [
|
|
|
3165
3315
|
"게시글에 좋아요 버튼이 생겼어요",
|
|
3166
3316
|
"글을 보는 동안 다른 사람의 좋아요·댓글 수가 화면에 바로 나타나요",
|
|
3167
3317
|
"작성 시간을 몇 분 전처럼 쉽게 보고, 올리면 정확한 시각을 확인할 수 있어요"
|
|
3318
|
+
],
|
|
3319
|
+
highlightsEn: [
|
|
3320
|
+
"Posts have a like button",
|
|
3321
|
+
"Other people's likes and comment counts update on screen while you read",
|
|
3322
|
+
"See times like \"5 minutes ago\" and hover for the exact time"
|
|
3168
3323
|
]
|
|
3169
3324
|
},
|
|
3170
3325
|
{
|
|
3171
3326
|
version: "0.10.0",
|
|
3172
3327
|
date: "2026-08-05",
|
|
3173
|
-
highlights: ["게시판 설치와 연결 설정이 한결 단순해졌어요", "게시판 아래에서 제작 정보와 지금까지의 버전 이야기를 볼 수 있어요"]
|
|
3328
|
+
highlights: ["게시판 설치와 연결 설정이 한결 단순해졌어요", "게시판 아래에서 제작 정보와 지금까지의 버전 이야기를 볼 수 있어요"],
|
|
3329
|
+
highlightsEn: ["Installing and connecting the board is much simpler", "See credits and the version story at the bottom of the board"]
|
|
3174
3330
|
},
|
|
3175
3331
|
{
|
|
3176
3332
|
version: "0.9.0",
|
|
3177
3333
|
date: "2026-08-04",
|
|
3178
|
-
highlights: ["댓글에 답글을 달고 대화 흐름을 볼 수 있어요", "댓글을 이어서 불러오고, 새 댓글은 실시간으로 받아요"]
|
|
3334
|
+
highlights: ["댓글에 답글을 달고 대화 흐름을 볼 수 있어요", "댓글을 이어서 불러오고, 새 댓글은 실시간으로 받아요"],
|
|
3335
|
+
highlightsEn: ["Reply to comments and follow the conversation", "Load more comments as you go and get new ones in real time"]
|
|
3179
3336
|
},
|
|
3180
3337
|
{
|
|
3181
3338
|
version: "0.8.0",
|
|
3182
3339
|
date: "2026-08-04",
|
|
3183
|
-
highlights: ["최근 댓글은 실시간으로 보고 이전 댓글은 이어서 불러올 수 있어요", "댓글 작성 영역을 대화 흐름의 맨 아래로 옮겼어요"]
|
|
3340
|
+
highlights: ["최근 댓글은 실시간으로 보고 이전 댓글은 이어서 불러올 수 있어요", "댓글 작성 영역을 대화 흐름의 맨 아래로 옮겼어요"],
|
|
3341
|
+
highlightsEn: ["See recent comments in real time and load older ones as you go", "Moved the comment box to the bottom of the conversation"]
|
|
3184
3342
|
},
|
|
3185
3343
|
{
|
|
3186
3344
|
version: "0.7.0",
|
|
3187
3345
|
date: "2026-08-04",
|
|
3188
|
-
highlights: ["카테고리마다 글쓰기 권한을 일반 이상·스태프 이상·관리자만으로 설정할 수 있어요", "게시판 설정 화면의 SSR hydration 경고를 해결했어요"]
|
|
3346
|
+
highlights: ["카테고리마다 글쓰기 권한을 일반 이상·스태프 이상·관리자만으로 설정할 수 있어요", "게시판 설정 화면의 SSR hydration 경고를 해결했어요"],
|
|
3347
|
+
highlightsEn: ["Set write permission per category to users and up, staff and up, or admins only", "Fixed the SSR hydration warning on the board settings screen"]
|
|
3189
3348
|
},
|
|
3190
3349
|
{
|
|
3191
3350
|
version: "0.6.0",
|
|
3192
3351
|
date: "2026-08-04",
|
|
3193
|
-
highlights: ["관리자가 게시판 사용자를 확인하고 관리자·스태프·사용자 역할을 설정할 수 있어요", "스태프는 게시판 설정이나 사용자 역할은 바꾸지 않고 게시글과 댓글을 관리할 수 있어요"]
|
|
3352
|
+
highlights: ["관리자가 게시판 사용자를 확인하고 관리자·스태프·사용자 역할을 설정할 수 있어요", "스태프는 게시판 설정이나 사용자 역할은 바꾸지 않고 게시글과 댓글을 관리할 수 있어요"],
|
|
3353
|
+
highlightsEn: ["Admins can review board users and set admin, staff and user roles", "Staff can manage posts and comments without changing board settings or user roles"]
|
|
3194
3354
|
},
|
|
3195
3355
|
{
|
|
3196
3356
|
version: "0.5.0",
|
|
3197
3357
|
date: "2026-08-04",
|
|
3198
|
-
highlights: ["게시판마다 일반·이미지·영상 목록을 고르고, 유튜브를 본문에 넣을 수 있어요", "이전 글·목록·다음 글 이동과 카테고리 관리를 지원해요"]
|
|
3358
|
+
highlights: ["게시판마다 일반·이미지·영상 목록을 고르고, 유튜브를 본문에 넣을 수 있어요", "이전 글·목록·다음 글 이동과 카테고리 관리를 지원해요"],
|
|
3359
|
+
highlightsEn: ["Choose a default, image or video list per board, and embed YouTube in posts", "Move to the previous post, the list or the next post, and manage categories"]
|
|
3199
3360
|
},
|
|
3200
3361
|
{
|
|
3201
3362
|
version: "0.2.0",
|
|
3202
3363
|
date: "2026-07-31",
|
|
3203
3364
|
highlights: ["글쓰기·댓글·첨부·로그인 권한으로 게시판을 바로 시작할 수 있어요", "부적절한 글·댓글은 AI 검열로 막을 수 있어요"],
|
|
3204
|
-
|
|
3365
|
+
highlightsEn: ["Start a board right away with posting, comments, attachments and sign-in permissions", "Block inappropriate posts and comments with AI moderation"],
|
|
3366
|
+
note: "실제 보안은 호스트 Firebase 규칙 등록이 필요합니다.",
|
|
3367
|
+
noteEn: "Real security requires registering the host's Firebase rules."
|
|
3205
3368
|
}
|
|
3206
|
-
],
|
|
3369
|
+
], di = 10, fi = 864e5, pi = 200, mi = {
|
|
3207
3370
|
proofread: "맞춤법, 띄어쓰기, 문법 오류만 고쳐라. 의미와 말투는 바꾸지 마라.",
|
|
3208
3371
|
polish: "뜻과 작성자의 말투를 유지하면서 더 자연스럽고 읽기 좋은 문장으로 다듬어라.",
|
|
3209
3372
|
continue: "앞의 맥락과 말투에 맞는 내용을 과장 없이 한두 문단 이어서 작성하라.",
|
|
@@ -3212,7 +3375,7 @@ var ci = [
|
|
|
3212
3375
|
"translate-ko": "자연스러운 한국어로 번역하라. 고유명사와 기술 용어는 정확히 유지하라.",
|
|
3213
3376
|
title: "본문을 대표하는 간결한 한국어 제목 하나를 만들어라. 따옴표나 설명 없이 제목만 출력하라."
|
|
3214
3377
|
};
|
|
3215
|
-
function
|
|
3378
|
+
function hi(e) {
|
|
3216
3379
|
return {
|
|
3217
3380
|
proofread: "Fix only spelling, spacing, and grammar errors. Do not change the meaning or tone.",
|
|
3218
3381
|
polish: "Refine the text into more natural, readable sentences while keeping the meaning and the author's tone.",
|
|
@@ -3223,27 +3386,27 @@ function pi(e) {
|
|
|
3223
3386
|
title: `Create one concise title in ${e} that represents the body. Output only the title, without quotes or explanation.`
|
|
3224
3387
|
};
|
|
3225
3388
|
}
|
|
3226
|
-
function
|
|
3227
|
-
let r = t === "ko", i =
|
|
3389
|
+
function gi(e, t, n) {
|
|
3390
|
+
let r = t === "ko", i = Cn(t);
|
|
3228
3391
|
if (e.action !== "custom") {
|
|
3229
|
-
if (r) return
|
|
3230
|
-
let t =
|
|
3392
|
+
if (r) return mi[e.action];
|
|
3393
|
+
let t = hi(i)[e.action];
|
|
3231
3394
|
return e.action === "translate-en" || e.action === "translate-ko" || e.action === "title" ? t : `${t} Write the result in the same language as the original text; if unclear, write in ${i}.`;
|
|
3232
3395
|
}
|
|
3233
|
-
let a = e.customInstruction?.trim().slice(0,
|
|
3396
|
+
let a = e.customInstruction?.trim().slice(0, pi);
|
|
3234
3397
|
if (!a) throw Error(n("writingAssistant.customRequired"));
|
|
3235
3398
|
return r ? `뜻은 유지하면서 다음 스타일 지시를 반영해 다시 써라: ${a}` : `Rewrite the text applying the following style instruction while keeping the meaning: ${a}`;
|
|
3236
3399
|
}
|
|
3237
|
-
function
|
|
3400
|
+
function _i(e) {
|
|
3238
3401
|
return e.trim().replace(/^```(?:html|text)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
3239
3402
|
}
|
|
3240
|
-
function
|
|
3403
|
+
function vi(e) {
|
|
3241
3404
|
return !e || typeof e != "object" ? null : "toMillis" in e && typeof e.toMillis == "function" ? e.toMillis() : null;
|
|
3242
3405
|
}
|
|
3243
|
-
function
|
|
3244
|
-
let t = j(), n = P(() => N(L(e.locale)) ?? t.locale.value), r = (e, t) => A(n.value, e, t), i = Me(), a = Z(), o = Ae(), s = v(), { translateEntries: c, translateHtml: l } =
|
|
3406
|
+
function yi(e = {}) {
|
|
3407
|
+
let t = j(), n = P(() => N(L(e.locale)) ?? t.locale.value), r = (e, t) => A(n.value, e, t), i = Me(), a = Z(), o = Ae(), s = v(), { translateEntries: c, translateHtml: l } = Hn();
|
|
3245
3408
|
function u(e, t) {
|
|
3246
|
-
let n =
|
|
3409
|
+
let n = vi(e.aiWritingWindowAt), r = n != null && t - n < fi, i = Math.max(0, Math.floor(Number(e.aiWritingCount) || 0)), a = Math.max(0, Math.floor(Number(e.aiWritingPending) || 0));
|
|
3247
3410
|
return {
|
|
3248
3411
|
active: r,
|
|
3249
3412
|
windowAt: n,
|
|
@@ -3252,7 +3415,7 @@ function _i(e = {}) {
|
|
|
3252
3415
|
};
|
|
3253
3416
|
}
|
|
3254
3417
|
function d(e, t) {
|
|
3255
|
-
let n = e +
|
|
3418
|
+
let n = e + fi, i = Math.max(1, Math.ceil((n - t) / 36e5));
|
|
3256
3419
|
return Error(r("writingAssistant.dailyLimit", {
|
|
3257
3420
|
limit: 10,
|
|
3258
3421
|
hours: i
|
|
@@ -3304,7 +3467,7 @@ function _i(e = {}) {
|
|
|
3304
3467
|
}
|
|
3305
3468
|
async function y(e) {
|
|
3306
3469
|
(e.action === "translate-en" || e.action === "translate-ko") && D();
|
|
3307
|
-
let t = n.value, a = t === "ko", o =
|
|
3470
|
+
let t = n.value, a = t === "ko", o = gi(e, t, r);
|
|
3308
3471
|
if (!e.content.trim()) throw Error(r("writingAssistant.contentRequired"));
|
|
3309
3472
|
return _(async () => {
|
|
3310
3473
|
let t = s.moderation ?? {}, n = Xe(i, {
|
|
@@ -3333,50 +3496,57 @@ function _i(e = {}) {
|
|
|
3333
3496
|
`Task: ${o}`,
|
|
3334
3497
|
e.title ? `Current title: ${e.title}` : "",
|
|
3335
3498
|
`Body:\n${l.slice(0, 12e3)}`
|
|
3336
|
-
]).filter(Boolean).join("\n\n"), d =
|
|
3499
|
+
]).filter(Boolean).join("\n\n"), d = _i((await c.generateContent(u)).response.text());
|
|
3337
3500
|
if (!d) throw Error(r("writingAssistant.emptyResult"));
|
|
3338
3501
|
return d;
|
|
3339
3502
|
});
|
|
3340
3503
|
}
|
|
3341
3504
|
async function b(e) {
|
|
3342
3505
|
D();
|
|
3343
|
-
let t = e.source ?? n.value, i = e.title?.trim() ?? "",
|
|
3344
|
-
|
|
3345
|
-
let
|
|
3346
|
-
if (!Object.keys(
|
|
3347
|
-
if (
|
|
3506
|
+
let t = e.source ?? n.value, i = e.targets?.length ? e.targets : pt(), a = [...i, t], o = e.title?.trim() ?? "", s = e.summary?.trim() ?? "", u = e.content ?? "", d = {};
|
|
3507
|
+
o && (d.title = o), s && (d.summary = s);
|
|
3508
|
+
let f = !!cn(u);
|
|
3509
|
+
if (!Object.keys(d).length && !f) throw Error(r("writingAssistant.translateEmpty"));
|
|
3510
|
+
if (f && u.length > 12e3) throw Error(r("writingAssistant.translateTooLong", {
|
|
3511
|
+
limit: Ln,
|
|
3512
|
+
count: i.length
|
|
3513
|
+
}));
|
|
3348
3514
|
return _(async () => {
|
|
3349
|
-
let [e, n] = await Promise.all([Object.keys(
|
|
3515
|
+
let [e, n] = await Promise.all([Object.keys(d).length ? c([{
|
|
3350
3516
|
id: "post",
|
|
3351
|
-
fields:
|
|
3517
|
+
fields: d,
|
|
3352
3518
|
context: "message board post title and short summary"
|
|
3353
3519
|
}], {
|
|
3354
3520
|
source: t,
|
|
3521
|
+
targets: i,
|
|
3355
3522
|
purpose: "message board post title and short summary"
|
|
3356
|
-
}) : Promise.resolve({}),
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3523
|
+
}) : Promise.resolve({}), f ? l(u, {
|
|
3524
|
+
source: t,
|
|
3525
|
+
targets: i
|
|
3526
|
+
}) : Promise.resolve({})]), p = ft(t, d, e.post);
|
|
3527
|
+
if (Object.keys(d).length && !ht(p, Object.keys(d), a)) throw Error(r("writingAssistant.translateIncomplete"));
|
|
3528
|
+
let m = {
|
|
3529
|
+
i18n: p,
|
|
3360
3530
|
i18nSource: {
|
|
3361
3531
|
locale: t,
|
|
3362
|
-
title:
|
|
3363
|
-
summary:
|
|
3532
|
+
title: o,
|
|
3533
|
+
summary: s
|
|
3364
3534
|
}
|
|
3365
3535
|
};
|
|
3366
|
-
if (
|
|
3536
|
+
if (f) {
|
|
3367
3537
|
let e = {};
|
|
3368
3538
|
for (let [t, r] of Object.entries(n)) {
|
|
3369
3539
|
let n = N(t);
|
|
3370
3540
|
n && typeof r == "string" && r.trim() && (e[n] = { content: r.trim() });
|
|
3371
3541
|
}
|
|
3372
|
-
let i = ft(t, { content:
|
|
3373
|
-
if (!
|
|
3374
|
-
|
|
3542
|
+
let i = ft(t, { content: u }, e);
|
|
3543
|
+
if (!ht(i, ["content"], a)) throw Error(r("writingAssistant.translateIncomplete"));
|
|
3544
|
+
m.contentI18n = i, m.contentI18nSource = {
|
|
3375
3545
|
locale: t,
|
|
3376
|
-
hash: ct(
|
|
3546
|
+
hash: ct(u)
|
|
3377
3547
|
};
|
|
3378
3548
|
}
|
|
3379
|
-
return
|
|
3549
|
+
return m;
|
|
3380
3550
|
});
|
|
3381
3551
|
}
|
|
3382
3552
|
return {
|
|
@@ -3386,14 +3556,14 @@ function _i(e = {}) {
|
|
|
3386
3556
|
}
|
|
3387
3557
|
//#endregion
|
|
3388
3558
|
//#region src/utils/renderMarkdown.ts
|
|
3389
|
-
function
|
|
3559
|
+
function bi(e) {
|
|
3390
3560
|
return e.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3391
3561
|
}
|
|
3392
|
-
function
|
|
3562
|
+
function xi(e) {
|
|
3393
3563
|
let t = e;
|
|
3394
3564
|
return t = t.replace(/`([^`]+)`/g, "<code>$1</code>"), t = t.replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g, "<img src=\"$2\" alt=\"$1\" class=\"max-w-full h-auto rounded-lg my-3\" loading=\"lazy\" />"), t = t.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary underline\">$1</a>"), t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"), t = t.replace(/\*([^*]+)\*/g, "<em>$1</em>"), t = t.replace(/~~([^~]+)~~/g, "<del>$1</del>"), t;
|
|
3395
3565
|
}
|
|
3396
|
-
function
|
|
3566
|
+
function Si(e) {
|
|
3397
3567
|
if (!e?.trim()) return "";
|
|
3398
3568
|
let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
|
|
3399
3569
|
function c() {
|
|
@@ -3402,7 +3572,7 @@ function bi(e) {
|
|
|
3402
3572
|
return;
|
|
3403
3573
|
}
|
|
3404
3574
|
let e = o;
|
|
3405
|
-
n.push(`<${e}>${s.map((e) => `<li>${
|
|
3575
|
+
n.push(`<${e}>${s.map((e) => `<li>${xi(e)}</li>`).join("")}</${e}>`), o = null, s = [];
|
|
3406
3576
|
}
|
|
3407
3577
|
for (; r < t.length;) {
|
|
3408
3578
|
let e = t[r] ?? "", l = e.match(/^```(\w*)\s*$/);
|
|
@@ -3411,7 +3581,7 @@ function bi(e) {
|
|
|
3411
3581
|
continue;
|
|
3412
3582
|
}
|
|
3413
3583
|
if (i) {
|
|
3414
|
-
a.push(
|
|
3584
|
+
a.push(bi(e)), r++;
|
|
3415
3585
|
continue;
|
|
3416
3586
|
}
|
|
3417
3587
|
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(e)) {
|
|
@@ -3422,24 +3592,24 @@ function bi(e) {
|
|
|
3422
3592
|
if (u) {
|
|
3423
3593
|
c();
|
|
3424
3594
|
let e = u[1].length;
|
|
3425
|
-
n.push(`<h${e} class="font-semibold mt-4 mb-2">${
|
|
3595
|
+
n.push(`<h${e} class="font-semibold mt-4 mb-2">${xi(bi(u[2]))}</h${e}>`), r++;
|
|
3426
3596
|
continue;
|
|
3427
3597
|
}
|
|
3428
3598
|
if (e.startsWith("> ")) {
|
|
3429
3599
|
c();
|
|
3430
3600
|
let e = [];
|
|
3431
|
-
for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push(
|
|
3432
|
-
n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${
|
|
3601
|
+
for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push(bi((t[r] ?? "").slice(2))), r++;
|
|
3602
|
+
n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${xi(e.join("<br />"))}</blockquote>`);
|
|
3433
3603
|
continue;
|
|
3434
3604
|
}
|
|
3435
3605
|
let d = e.match(/^[-*+]\s+(.+)$/);
|
|
3436
3606
|
if (d) {
|
|
3437
|
-
o && o !== "ul" && c(), o = "ul", s.push(
|
|
3607
|
+
o && o !== "ul" && c(), o = "ul", s.push(bi(d[1])), r++;
|
|
3438
3608
|
continue;
|
|
3439
3609
|
}
|
|
3440
3610
|
let f = e.match(/^\d+\.\s+(.+)$/);
|
|
3441
3611
|
if (f) {
|
|
3442
|
-
o && o !== "ol" && c(), o = "ol", s.push(
|
|
3612
|
+
o && o !== "ol" && c(), o = "ol", s.push(bi(f[1])), r++;
|
|
3443
3613
|
continue;
|
|
3444
3614
|
}
|
|
3445
3615
|
if (!e.trim()) {
|
|
@@ -3451,22 +3621,22 @@ function bi(e) {
|
|
|
3451
3621
|
for (; r < t.length;) {
|
|
3452
3622
|
let e = t[r] ?? "";
|
|
3453
3623
|
if (!e.trim() || /^(#{1,3}\s|```|[-*+]\s|\d+\.\s|> |(-{3,}|\*{3,})\s*$)/.test(e)) break;
|
|
3454
|
-
p.push(
|
|
3624
|
+
p.push(bi(e)), r++;
|
|
3455
3625
|
}
|
|
3456
|
-
n.push(`<p class="my-2 leading-relaxed">${
|
|
3626
|
+
n.push(`<p class="my-2 leading-relaxed">${xi(p.join("<br />"))}</p>`);
|
|
3457
3627
|
}
|
|
3458
3628
|
return c(), i && a.length && n.push(`<pre class="overflow-x-auto rounded-lg bg-muted p-3 text-sm"><code>${a.join("\n")}</code></pre>`), n.join("\n");
|
|
3459
3629
|
}
|
|
3460
3630
|
//#endregion
|
|
3461
3631
|
//#region src/utils/orphanCleanup.ts
|
|
3462
|
-
function
|
|
3632
|
+
function Ci(e, t, n, r) {
|
|
3463
3633
|
return typeof t.postId != "string" || !t.postId || t.postId.includes("/") ? !1 : !n.has(t.postId) || e === "like" && t.target === "comment" && typeof t.commentId == "string" && !!t.commentId && !t.commentId.includes("/") && !r.has(t.commentId);
|
|
3464
3634
|
}
|
|
3465
|
-
function
|
|
3635
|
+
function wi(e, t) {
|
|
3466
3636
|
let n = Date.parse(e);
|
|
3467
3637
|
return Number.isFinite(n) && t - n >= 864e5;
|
|
3468
3638
|
}
|
|
3469
|
-
function
|
|
3639
|
+
function Ti(e, t) {
|
|
3470
3640
|
if (!e.startsWith(`${t}/`)) return null;
|
|
3471
3641
|
let n = e.slice(t.length + 1).split("/");
|
|
3472
3642
|
if (n.some((e) => !e || e === "." || e === "..")) return null;
|
|
@@ -3476,14 +3646,14 @@ function Ci(e, t) {
|
|
|
3476
3646
|
commentId: a
|
|
3477
3647
|
} : null;
|
|
3478
3648
|
}
|
|
3479
|
-
function
|
|
3649
|
+
function Ei(e, t, n) {
|
|
3480
3650
|
let r = `${t === "comment" ? n.commentsCollection : n.likesCollection}/`, i = e.startsWith(r) ? e.slice(r.length) : "";
|
|
3481
3651
|
return !!i && !i.includes("/") && i !== "." && i !== "..";
|
|
3482
3652
|
}
|
|
3483
3653
|
//#endregion
|
|
3484
3654
|
//#region src/composables/useMemiBoardCleanup.ts
|
|
3485
|
-
function
|
|
3486
|
-
let e = Z(), t = Me(), { isAdmin: n, rolePending: r } =
|
|
3655
|
+
function Di() {
|
|
3656
|
+
let e = Z(), t = Me(), { isAdmin: n, rolePending: r } = Ut(), { t: i } = j(), a = I([]), o = I(!1), s = I(!1), c = I(""), l = I(""), u = [], d = "", f = /* @__PURE__ */ new Map();
|
|
3487
3657
|
function p() {
|
|
3488
3658
|
let e = Le(t).currentUser?.uid;
|
|
3489
3659
|
if (!e) throw Error(i("settings.noAccess"));
|
|
@@ -3504,10 +3674,10 @@ function Ti() {
|
|
|
3504
3674
|
function o(e) {
|
|
3505
3675
|
if (typeof e == "string") {
|
|
3506
3676
|
e.startsWith(`${t.postsCollection}/`) && a.add(e);
|
|
3507
|
-
let n =
|
|
3677
|
+
let n = Xt(e);
|
|
3508
3678
|
n && a.add(n);
|
|
3509
3679
|
for (let t of e.matchAll(/https?:\/\/[^\s"'<>]+/g)) {
|
|
3510
|
-
let e =
|
|
3680
|
+
let e = Xt(t[0].replace(/&/g, "&"));
|
|
3511
3681
|
e && a.add(e);
|
|
3512
3682
|
}
|
|
3513
3683
|
} else Array.isArray(e) ? e.forEach(o) : e && typeof e == "object" && Object.getPrototypeOf(e) === Object.prototype && Object.values(e).forEach(o);
|
|
@@ -3537,7 +3707,7 @@ function Ti() {
|
|
|
3537
3707
|
let r = [];
|
|
3538
3708
|
for (let [e, t] of [["comment", n.commentDocs], ["like", n.likes]]) for (let i of t) {
|
|
3539
3709
|
let t = i.data();
|
|
3540
|
-
|
|
3710
|
+
Ci(e, t, n.posts, n.comments) && r.push({
|
|
3541
3711
|
kind: e,
|
|
3542
3712
|
path: i.ref.path,
|
|
3543
3713
|
postId: t.postId,
|
|
@@ -3555,12 +3725,12 @@ function Ti() {
|
|
|
3555
3725
|
});
|
|
3556
3726
|
o.push(...e.prefixes);
|
|
3557
3727
|
for (let t of e.items) {
|
|
3558
|
-
let e =
|
|
3728
|
+
let e = Ti(t.fullPath, n.cfg.postsCollection);
|
|
3559
3729
|
if (!e) continue;
|
|
3560
3730
|
let { postId: i, commentId: a } = e;
|
|
3561
3731
|
if (n.referenced.has(t.fullPath) || n.posts.has(i) && (!a || n.comments.has(a))) continue;
|
|
3562
3732
|
let o = await Ue(t);
|
|
3563
|
-
|
|
3733
|
+
wi(o.updated, Date.now()) && (f.set(t.fullPath, o.generation), r.push({
|
|
3564
3734
|
kind: "file",
|
|
3565
3735
|
path: t.fullPath,
|
|
3566
3736
|
postId: i,
|
|
@@ -3593,23 +3763,23 @@ function Ti() {
|
|
|
3593
3763
|
try {
|
|
3594
3764
|
let i = !1;
|
|
3595
3765
|
if (n.kind === "file") {
|
|
3596
|
-
if (!
|
|
3766
|
+
if (!Ti(n.path, o.cfg.postsCollection)) throw Error("Invalid cleanup path");
|
|
3597
3767
|
let r = We(t), a = qe(r, n.path), [s, c, l] = await Promise.all([
|
|
3598
3768
|
Se(z(e, o.cfg.postsCollection, n.postId)),
|
|
3599
3769
|
n.commentId ? Se(z(e, o.cfg.commentsCollection, n.commentId)) : null,
|
|
3600
3770
|
Ue(a)
|
|
3601
3771
|
]);
|
|
3602
|
-
m(d), l.generation === f.get(n.path) && !o.referenced.has(n.path) &&
|
|
3772
|
+
m(d), l.generation === f.get(n.path) && !o.referenced.has(n.path) && wi(l.updated, Date.now()) && (!s.exists() || n.commentId && !c?.exists()) && (await He(a), i = !0);
|
|
3603
3773
|
} else {
|
|
3604
3774
|
let t = n.kind;
|
|
3605
|
-
if (!
|
|
3775
|
+
if (!Ei(n.path, t, o.cfg)) throw Error("Invalid cleanup path");
|
|
3606
3776
|
i = await Ee(e, async (r) => {
|
|
3607
3777
|
let i = await r.get(z(e, n.path));
|
|
3608
3778
|
if (!i.exists()) return !0;
|
|
3609
3779
|
let a = i.data();
|
|
3610
3780
|
if (typeof a.postId != "string" || !a.postId || a.postId.includes("/")) return !1;
|
|
3611
3781
|
let s = await r.get(z(e, o.cfg.postsCollection, a.postId)), c = n.kind === "like" && a.target === "comment" && typeof a.commentId == "string" && a.commentId && !a.commentId.includes("/") ? await r.get(z(e, o.cfg.commentsCollection, a.commentId)) : null;
|
|
3612
|
-
return
|
|
3782
|
+
return Ci(t, a, new Set(s.exists() ? [a.postId] : []), new Set(c?.exists() ? [a.commentId] : [])) ? (m(d), r.delete(i.ref), !0) : !1;
|
|
3613
3783
|
});
|
|
3614
3784
|
}
|
|
3615
3785
|
i ? r++ : p++, a.value = a.value.filter((e) => e.path !== n.path);
|
|
@@ -3639,6 +3809,6 @@ function Ti() {
|
|
|
3639
3809
|
};
|
|
3640
3810
|
}
|
|
3641
3811
|
//#endregion
|
|
3642
|
-
export {
|
|
3812
|
+
export { cr as AUTHOR_MEMO_MAX_LENGTH, Nr as BOARD_SECTION_COLS, Ir as BOARD_SECTION_KINDS, Lr as BOARD_SECTION_SORTS, Ar as BOARD_USER_ROLES, mr as COMMENT_BODY_MAX_LENGTH, Ft as DEFAULT_BLOCK_BAN_DECAY_MS, Pt as DEFAULT_BLOCK_BAN_THRESHOLD, Un as DEFAULT_BOARDS, Wn as DEFAULT_CATEGORIES, i as DEFAULT_COMMENTS_COLLECTION, r as DEFAULT_LIKES_COLLECTION, ne as DEFAULT_MEMI_BOARD_LOCALE, m as DEFAULT_POSTS_COLLECTION, ie as DEFAULT_REPORTS_COLLECTION, e as DEFAULT_SETTINGS_COLLECTION, re as DEFAULT_USERS_COLLECTION, k as FALLBACK_MEMI_BOARD_LOCALE, Ln as HTML_MAX_CHARS, T as MEMI_BOARD_LOCALES, xr as REPORT_DETAIL_MAX_LENGTH, Sr as REPORT_REASONS, di as WRITING_ASSISTANT_DAILY_LIMIT, $e as asHttpUrl, xt as boardCopyIsTranslated, at as boardListOgDescription, it as boardListOgTitle, rt as boardPostOgDescription, nt as boardPostOgTitle, Pr as boardSectionKinds, Fr as boardSectionSorts, p as boardSsrKey, pt as boardTranslationLocales, h as boardUserDoc, c as boardUsersCol, gn as buildPostPreview, kt as canManageBoardByRole, At as canWriteCommentByRole, zr as clampSectionCount, d as commentDoc, g as commentsCol, pe as configureMemiBoard, ht as coversLocales, Rr as defaultSectionCount, bt as displayPostContent, yt as displayPostSummary, vt as displayPostTitle, _t as displayStoredField, It as effectiveModerationBlockCount, Yt as extractEditorImageUrls, Et as fetchPublicListForSeo, Tt as fetchPublicPostForSeo, ee as formatDate, ae as formatFullDate, de as formatRelativeDate, zt as formatRestrictedUntilLabel, O as formatTimestampDetails, dn as hasBodyImage, un as hasBodyText, ct as hashText, qt as imeSafeSubmitClick, Kt as imeSafeSubmitPointerDown, Ot as isAssignedStaff, fn as isContentEmpty, Yn as isFirestorePermissionDenied, _ as isMemiBoardI18nEnabled, te as isMemiBoardLocaleLoaded, n as isMemiBoardTranslationEnabled, Rt as isModerationWriteRestricted, me as likeDoc, t as likeDocId, b as likesCol, se as loadMemiBoardLocale, gt as localizedText, yr as memiBoardCommentLikesKey, oe as memiBoardHostLocaleKey, fe as memiBoardLocaleInfo, ft as mergeSourceFields, Xr as miniBoardView, Gr as miniCommentLink, Wr as miniMoreLink, Kr as miniPostImage, Ur as miniPostLink, qr as miniPostTitle, Zr as miniPostsView, Jr as miniViewGroup, Yr as miniViewLabel, Lt as moderationWriteRestrictedUntilMs, ot as normalizeBasePath, oi as normalizeBoardSection, N as normalizeMemiBoardLocale, Br as pairSectionCols, cn as plainTextFromHtml, s as postBodyDoc, y as postDoc, Zt as postNamespaceFromStoragePath, x as postStorageFolder, o as postsCol, ut as readI18nSource, Q as readLocalizedFields, Si as renderMarkdownToHtml, u as reportDoc, ce as reportDocId, Cr as reportReasonLabel, l as reportsCol, C as resolveBoardPathConfig, St as resolvePublicSeoDb, Vr as sectionColClass, Hr as sectionMinHeight, S as settingsCol, w as settingsDoc, E as slugify, Xt as storagePathFromDownloadUrl, ln as titleFromBody, tt as toAbsoluteUrl, et as toBoardOgImageUrl, A as translate, M as useBoardPathConfig, Ut as useMemiBoardAuth, ur as useMemiBoardAuthorMemo, Hn as useMemiBoardAutoTranslate, Di as useMemiBoardCleanup, sr as useMemiBoardCommentFeed, br as useMemiBoardCommentLikes, gr as useMemiBoardComments, v as useMemiBoardConfig, j as useMemiBoardI18n, vr as useMemiBoardLikes, ci as useMemiBoardMiniLatest, li as useMemiBoardMiniPicked, kr as useMemiBoardModeration, tr as useMemiBoardPost, er as useMemiBoardPostList, Zn as useMemiBoardPosts, _r as useMemiBoardReplies, Er as useMemiBoardReportQueue, wr as useMemiBoardReports, si as useMemiBoardSections, qn as useMemiBoardSettings, ar as useMemiBoardUserComments, rr as useMemiBoardUserPosts, Mr as useMemiBoardUserProfile, jr as useMemiBoardUsers, Dr as useMemiBoardViews, yi as useMemiBoardWritingAssistant, ui as versionHistory, hn as videoListCoverUrl, mn as youtubeId };
|
|
3643
3813
|
|
|
3644
3814
|
//# sourceMappingURL=index.js.map
|