memi-board 0.56.2 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -9
- package/dist/{ar-CQhBdHu3.js → ar-GNguZBN4.js} +45 -4
- package/dist/ar-GNguZBN4.js.map +1 -0
- package/dist/{commentImages-DsW2yCqu.js → commentImages-CRK_2uVi.js} +195 -70
- package/dist/commentImages-CRK_2uVi.js.map +1 -0
- package/dist/composables/useMemiBoardModeration.d.ts +2 -0
- package/dist/composables/useMemiBoardPosts.d.ts +1 -1
- package/dist/composables/useMemiBoardReports.d.ts +102 -3
- package/dist/composables/useMemiBoardUserDirectory.d.ts +20 -0
- package/dist/composables/useMemiBoardUserProfile.d.ts +1 -1
- package/dist/composables/useMemiBoardUsers.d.ts +1 -1
- package/dist/{de-B4GZqxQj.js → de-CS0iRLqe.js} +45 -4
- package/dist/de-CS0iRLqe.js.map +1 -0
- package/dist/{en-BDkfivhc.js → en-DeExi-DR.js} +129 -4
- package/dist/en-DeExi-DR.js.map +1 -0
- package/dist/{es-DzFDaLwo.js → es-dlEFrox-.js} +45 -4
- package/dist/es-dlEFrox-.js.map +1 -0
- package/dist/{fr-Zf5mcs8m.js → fr-B9sjKJF4.js} +45 -4
- package/dist/fr-B9sjKJF4.js.map +1 -0
- package/dist/{id-CaWN8212.js → id-Ca7skouh.js} +45 -4
- package/dist/id-Ca7skouh.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1089 -829
- package/dist/index.js.map +1 -1
- package/dist/{ja-xDttH9CE.js → ja-ByVLGIDd.js} +45 -4
- package/dist/ja-ByVLGIDd.js.map +1 -0
- package/dist/module.mjs +4 -0
- package/dist/module.mjs.map +1 -1
- package/dist/{pt-WIfxYYWQ.js → pt-COQL_Q7A.js} +45 -4
- package/dist/pt-COQL_Q7A.js.map +1 -0
- package/dist/runtime/components/AccountDeletion.vue +64 -0
- package/dist/runtime/components/AuthorMenu.vue +4 -4
- package/dist/runtime/components/CommentFeed.vue +17 -13
- package/dist/runtime/components/CommentForm.vue +58 -56
- package/dist/runtime/components/CommentImageField.vue +18 -13
- package/dist/runtime/components/CommentItem.vue +95 -60
- package/dist/runtime/components/CommentList.vue +10 -1
- package/dist/runtime/components/CommentThread.vue +16 -2
- package/dist/runtime/components/CustomFieldEditor.vue +10 -8
- package/dist/runtime/components/Detail.vue +18 -10
- package/dist/runtime/components/Editor.vue +1 -5
- package/dist/runtime/components/ImageLightbox.vue +126 -58
- package/dist/runtime/components/LikeButton.vue +14 -11
- package/dist/runtime/components/ListViewSwitch.vue +10 -10
- package/dist/runtime/components/Profile.vue +14 -2
- package/dist/runtime/components/ProfileEditModal.vue +20 -3
- package/dist/runtime/components/ReportButton.vue +22 -13
- package/dist/runtime/components/ReportList.vue +77 -93
- package/dist/runtime/components/UserListControls.vue +29 -0
- package/dist/runtime/components/UserListPagination.vue +17 -0
- package/dist/runtime/components/UserManagementEmail.vue +21 -0
- package/dist/runtime/components/UserRoleGuide.vue +38 -0
- package/dist/runtime/components/Users.vue +21 -59
- package/dist/runtime/components/UsersSettings.vue +119 -0
- package/dist/storage.js +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/utils/postSlug.d.ts +12 -0
- package/dist/utils/profileNameModeration.d.ts +8 -0
- package/dist/utils/userDirectoryPage.d.ts +19 -0
- package/dist/{zh-CI6Sd8PD.js → zh-DBjX9qAS.js} +45 -4
- package/dist/zh-DBjX9qAS.js.map +1 -0
- package/docs/account-deletion.md +60 -0
- package/docs/firebase-setup.md +77 -0
- package/docs/firestore.rules.example +78 -4
- package/docs/host-integration.md +85 -11
- package/docs/storage.rules.example +7 -2
- package/package.json +3 -2
- package/scripts/backfill-post-slugs.mjs +38 -0
- package/dist/ar-CQhBdHu3.js.map +0 -1
- package/dist/commentImages-DsW2yCqu.js.map +0 -1
- package/dist/de-B4GZqxQj.js.map +0 -1
- package/dist/en-BDkfivhc.js.map +0 -1
- package/dist/es-DzFDaLwo.js.map +0 -1
- package/dist/fr-Zf5mcs8m.js.map +0 -1
- package/dist/id-CaWN8212.js.map +0 -1
- package/dist/ja-xDttH9CE.js.map +0 -1
- package/dist/pt-WIfxYYWQ.js.map +0 -1
- package/dist/zh-CI6Sd8PD.js.map +0 -1
|
@@ -134,69 +134,71 @@ onUnmounted(() => {
|
|
|
134
134
|
<template>
|
|
135
135
|
<form
|
|
136
136
|
v-if="isSignedIn && canComment"
|
|
137
|
-
class="
|
|
137
|
+
class="overflow-hidden rounded-xl border border-default bg-default"
|
|
138
138
|
@submit.prevent="handleSubmit"
|
|
139
139
|
>
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
:comment-id="draftId"
|
|
157
|
-
:disabled="submitting || isWriteRestricted"
|
|
158
|
-
@busy="imageBusy = $event"
|
|
159
|
-
/>
|
|
160
|
-
<p
|
|
161
|
-
v-if="isWriteRestricted && restrictedMessage"
|
|
162
|
-
class="text-xs text-warning"
|
|
163
|
-
>
|
|
164
|
-
{{ restrictedMessage }}
|
|
165
|
-
</p>
|
|
166
|
-
<div class="flex justify-between items-center">
|
|
167
|
-
<span class="text-xs text-dimmed">{{ formatNumber(body.length) }} / {{ formatNumber(COMMENT_BODY_MAX_LENGTH) }}</span>
|
|
168
|
-
<p
|
|
169
|
-
v-if="error"
|
|
170
|
-
class="text-xs text-error"
|
|
171
|
-
>
|
|
140
|
+
<div class="flex flex-col gap-3 p-3 sm:p-4">
|
|
141
|
+
<UTextarea
|
|
142
|
+
v-model="body"
|
|
143
|
+
@paste="imageField?.handlePaste($event)"
|
|
144
|
+
:placeholder="parent ? t('commentForm.replyTo', { name: parent.authorName || t('common.label.user') }) : t('commentForm.placeholder')"
|
|
145
|
+
:rows="3"
|
|
146
|
+
autoresize
|
|
147
|
+
:maxrows="10"
|
|
148
|
+
:disabled="submitting || aiBusy || isWriteRestricted"
|
|
149
|
+
:maxlength="COMMENT_BODY_MAX_LENGTH"
|
|
150
|
+
class="w-full"
|
|
151
|
+
/>
|
|
152
|
+
<p v-if="isWriteRestricted && restrictedMessage" class="text-xs text-warning">
|
|
153
|
+
{{ restrictedMessage }}
|
|
154
|
+
</p>
|
|
155
|
+
<p v-if="error" class="text-xs text-error" role="alert">
|
|
172
156
|
{{ error }}
|
|
173
157
|
</p>
|
|
174
|
-
<p
|
|
175
|
-
v-else-if="notice"
|
|
176
|
-
class="text-xs text-warning"
|
|
177
|
-
>
|
|
158
|
+
<p v-else-if="notice" class="text-xs text-warning" role="status">
|
|
178
159
|
{{ notice }}
|
|
179
160
|
</p>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
<UButton
|
|
192
|
-
type="submit"
|
|
193
|
-
@pointerdown="imeSafeSubmitPointerDown"
|
|
194
|
-
@click="imeSafeSubmitClick"
|
|
195
|
-
size="sm"
|
|
196
|
-
:label="parent ? t('commentForm.submitReply') : t('commentForm.submit')"
|
|
197
|
-
:loading="submitting"
|
|
198
|
-
:disabled="!body.trim() || isWriteRestricted || imageBusy || aiBusy"
|
|
161
|
+
</div>
|
|
162
|
+
<div class="flex flex-col gap-3 border-t border-default bg-muted/30 px-3 py-2.5 sm:flex-row sm:items-center sm:px-4">
|
|
163
|
+
<MemiBoardCommentImageField
|
|
164
|
+
ref="imageField"
|
|
165
|
+
v-model="image"
|
|
166
|
+
:post-id="postId"
|
|
167
|
+
:comment-id="draftId"
|
|
168
|
+
:disabled="submitting || isWriteRestricted"
|
|
169
|
+
compact
|
|
170
|
+
class="min-w-0 flex-1"
|
|
171
|
+
@busy="imageBusy = $event"
|
|
199
172
|
/>
|
|
173
|
+
<div class="ms-auto flex shrink-0 flex-wrap items-center justify-end gap-2 sm:gap-3">
|
|
174
|
+
<MemiBoardCommentAi
|
|
175
|
+
v-model="body"
|
|
176
|
+
v-model:translations="translations"
|
|
177
|
+
:board-id="boardId"
|
|
178
|
+
:disabled="submitting || imageBusy || isWriteRestricted"
|
|
179
|
+
@busy="aiBusy = $event"
|
|
180
|
+
/>
|
|
181
|
+
<span class="text-xs text-dimmed tabular-nums">{{ formatNumber(body.length) }} / {{ formatNumber(COMMENT_BODY_MAX_LENGTH) }}</span>
|
|
182
|
+
<UButton
|
|
183
|
+
v-if="parent"
|
|
184
|
+
type="button"
|
|
185
|
+
size="sm"
|
|
186
|
+
:label="t('common.action.cancel')"
|
|
187
|
+
color="neutral"
|
|
188
|
+
variant="ghost"
|
|
189
|
+
:disabled="imageBusy || submitting || aiBusy"
|
|
190
|
+
@click="handleCancel"
|
|
191
|
+
/>
|
|
192
|
+
<UButton
|
|
193
|
+
type="submit"
|
|
194
|
+
@pointerdown="imeSafeSubmitPointerDown"
|
|
195
|
+
@click="imeSafeSubmitClick"
|
|
196
|
+
size="sm"
|
|
197
|
+
:label="parent ? t('commentForm.submitReply') : t('commentForm.submit')"
|
|
198
|
+
:loading="submitting"
|
|
199
|
+
:disabled="!body.trim() || isWriteRestricted || imageBusy || aiBusy"
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
200
202
|
</div>
|
|
201
203
|
</form>
|
|
202
204
|
<component
|
|
@@ -9,6 +9,7 @@ const props = defineProps<{
|
|
|
9
9
|
commentId: string
|
|
10
10
|
modelValue: Attachment | null
|
|
11
11
|
disabled?: boolean
|
|
12
|
+
compact?: boolean
|
|
12
13
|
}>()
|
|
13
14
|
|
|
14
15
|
const emit = defineEmits<{
|
|
@@ -166,22 +167,25 @@ onUnmounted(() => {
|
|
|
166
167
|
class="max-h-40 max-w-full rounded-md border border-default object-contain"
|
|
167
168
|
>
|
|
168
169
|
</button>
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
<UTooltip :text="t('commentForm.removeImage')" :delay-duration="200">
|
|
171
|
+
<UButton
|
|
172
|
+
type="button"
|
|
173
|
+
icon="i-lucide-x"
|
|
174
|
+
size="xs"
|
|
175
|
+
color="neutral"
|
|
176
|
+
variant="ghost"
|
|
177
|
+
:aria-label="t('commentForm.removeImage')"
|
|
178
|
+
:disabled="disabled || uploading"
|
|
179
|
+
@click="remove"
|
|
180
|
+
/>
|
|
181
|
+
</UTooltip>
|
|
179
182
|
</div>
|
|
180
183
|
<button
|
|
181
184
|
v-else
|
|
182
185
|
type="button"
|
|
183
|
-
class="rounded-md border border-dashed px-3
|
|
184
|
-
:class="dragOver ? 'border-primary bg-elevated/60' : 'border-default hover:bg-elevated/40'"
|
|
186
|
+
class="flex items-center gap-2 rounded-md border border-dashed px-3 text-left text-xs text-muted transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
|
187
|
+
:class="[compact ? 'py-2.5' : 'py-4', dragOver ? 'border-primary bg-elevated/60' : 'border-default hover:bg-elevated/40']"
|
|
188
|
+
:title="compact ? t('commentForm.attachNote') : undefined"
|
|
185
189
|
:disabled="disabled || uploading"
|
|
186
190
|
@click="openPicker"
|
|
187
191
|
@dragenter.prevent="dragOver = true"
|
|
@@ -189,6 +193,7 @@ onUnmounted(() => {
|
|
|
189
193
|
@dragleave="onDragLeave"
|
|
190
194
|
@drop.prevent="onDrop"
|
|
191
195
|
>
|
|
196
|
+
<UIcon v-if="compact" name="i-lucide-image-plus" class="size-4 shrink-0" />
|
|
192
197
|
{{ uploading ? t('commentForm.uploading') : t('commentForm.attachHint') }}
|
|
193
198
|
</button>
|
|
194
199
|
<UProgress
|
|
@@ -196,7 +201,7 @@ onUnmounted(() => {
|
|
|
196
201
|
:model-value="Math.round(progress * 100)"
|
|
197
202
|
size="sm"
|
|
198
203
|
/>
|
|
199
|
-
<p class="text-xs text-dimmed">
|
|
204
|
+
<p v-if="!compact" class="text-xs text-dimmed">
|
|
200
205
|
{{ t('commentForm.attachNote') }}
|
|
201
206
|
</p>
|
|
202
207
|
<p
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
useMemiBoardI18n,
|
|
14
14
|
useMemiBoardModeration,
|
|
15
15
|
} from 'memi-board/runtime'
|
|
16
|
+
import MemiBoardAuthorMenu from './AuthorMenu.vue'
|
|
16
17
|
import MemiBoardCommentAi from './CommentAi.vue'
|
|
17
18
|
import MemiBoardCommentImageField from './CommentImageField.vue'
|
|
18
19
|
import MemiBoardImageLightbox from './ImageLightbox.vue'
|
|
@@ -23,6 +24,9 @@ const props = defineProps<{
|
|
|
23
24
|
postId: string
|
|
24
25
|
now: number
|
|
25
26
|
deleting?: boolean
|
|
27
|
+
authorProfileTo?: (authorUid: string) => string | undefined
|
|
28
|
+
authorPostsTo?: (authorUid: string) => string | undefined
|
|
29
|
+
authorCommentsTo?: (authorUid: string) => string | undefined
|
|
26
30
|
}>()
|
|
27
31
|
|
|
28
32
|
const emit = defineEmits<{
|
|
@@ -143,19 +147,23 @@ async function saveEdit() {
|
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
const blindConfirmOpen = ref(false)
|
|
151
|
+
const blindTarget = ref(false)
|
|
152
|
+
function toggleBlind() {
|
|
153
|
+
if (!props.comment.id || !user.value || !canManageBoard(props.comment.boardId) || blindSaving.value) return
|
|
154
|
+
editError.value = ''
|
|
155
|
+
blindTarget.value = !localBlinded.value
|
|
156
|
+
blindConfirmOpen.value = true
|
|
157
|
+
}
|
|
158
|
+
async function confirmBlind() {
|
|
159
|
+
if (!props.comment.id || !user.value || !canManageBoard(props.comment.boardId) || blindSaving.value) return
|
|
160
|
+
const next = blindTarget.value
|
|
154
161
|
blindSaving.value = true
|
|
155
162
|
editError.value = ''
|
|
156
163
|
try {
|
|
157
164
|
await setCommentBlinded(props.comment.id, next, user.value.uid)
|
|
158
165
|
localBlinded.value = next
|
|
166
|
+
blindConfirmOpen.value = false
|
|
159
167
|
if (next) cancelEdit()
|
|
160
168
|
}
|
|
161
169
|
catch (cause) {
|
|
@@ -189,7 +197,17 @@ async function toggleLike() {
|
|
|
189
197
|
/>
|
|
190
198
|
<div class="min-w-0 flex-1">
|
|
191
199
|
<div class="flex items-center gap-2">
|
|
192
|
-
<
|
|
200
|
+
<MemiBoardAuthorMenu
|
|
201
|
+
v-if="comment.authorUid"
|
|
202
|
+
:author-uid="comment.authorUid"
|
|
203
|
+
:author-name="comment.authorName"
|
|
204
|
+
:author-photo="comment.authorPhoto"
|
|
205
|
+
:show-avatar="false"
|
|
206
|
+
:author-profile-to="authorProfileTo"
|
|
207
|
+
:author-posts-to="authorPostsTo"
|
|
208
|
+
:author-comments-to="authorCommentsTo"
|
|
209
|
+
/>
|
|
210
|
+
<span v-else class="text-sm font-medium">{{ comment.authorName ?? t('common.label.anonymous') }}</span>
|
|
193
211
|
<UPopover :content="{ side: 'top' }" :ui="{ content: 'h-auto w-max' }">
|
|
194
212
|
<time
|
|
195
213
|
:datetime="date?.toISOString()"
|
|
@@ -275,8 +293,7 @@ async function toggleLike() {
|
|
|
275
293
|
</div>
|
|
276
294
|
<div v-else>
|
|
277
295
|
<p class="whitespace-pre-wrap break-words text-sm">
|
|
278
|
-
<span v-if="comment.parentId && comment.replyToName" class="mr-1 text-primary">@{{ comment.replyToName }}</span
|
|
279
|
-
{{ displayedBody }}
|
|
296
|
+
<span v-if="comment.parentId && comment.replyToName" class="mr-1 text-primary">@{{ comment.replyToName }}</span><span v-text="displayedBody" />
|
|
280
297
|
</p>
|
|
281
298
|
<button
|
|
282
299
|
v-if="localImage"
|
|
@@ -302,17 +319,18 @@ async function toggleLike() {
|
|
|
302
319
|
</p>
|
|
303
320
|
</div>
|
|
304
321
|
<div class="flex shrink-0 items-center gap-1">
|
|
305
|
-
<
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
322
|
+
<UTooltip v-if="isSignedIn && commentLikes && comment.id" :text="isLiked ? t('common.action.unlike') : t('common.action.like')" :delay-duration="200">
|
|
323
|
+
<UButton
|
|
324
|
+
icon="i-lucide-heart"
|
|
325
|
+
size="xs"
|
|
326
|
+
variant="ghost"
|
|
327
|
+
:color="isLiked ? 'error' : 'neutral'"
|
|
328
|
+
:label="String(localLikeCount)"
|
|
329
|
+
:loading="likePending"
|
|
330
|
+
:aria-label="isLiked ? t('common.action.unlike') : t('common.action.like')"
|
|
331
|
+
@click="toggleLike"
|
|
332
|
+
/>
|
|
333
|
+
</UTooltip>
|
|
316
334
|
<span
|
|
317
335
|
v-else
|
|
318
336
|
class="flex items-center gap-0.5 px-1 text-xs text-muted"
|
|
@@ -321,44 +339,61 @@ async function toggleLike() {
|
|
|
321
339
|
<UIcon name="i-lucide-heart" class="size-3.5" />
|
|
322
340
|
{{ localLikeCount }}
|
|
323
341
|
</span>
|
|
324
|
-
<
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
342
|
+
<UTooltip v-if="canEditComment(comment) && comment.id && !localBlinded" :text="t('commentItem.edit')" :delay-duration="200">
|
|
343
|
+
<UButton
|
|
344
|
+
icon="i-lucide-pencil"
|
|
345
|
+
size="xs"
|
|
346
|
+
color="neutral"
|
|
347
|
+
variant="ghost"
|
|
348
|
+
:aria-label="t('commentItem.edit')"
|
|
349
|
+
:disabled="editing"
|
|
350
|
+
@click="startEdit"
|
|
351
|
+
/>
|
|
352
|
+
</UTooltip>
|
|
353
|
+
<UTooltip v-if="canManageBoard(comment.boardId) && comment.id" :text="localBlinded ? t('commentItem.unblind') : t('commentItem.blind')" :delay-duration="200">
|
|
354
|
+
<UButton
|
|
355
|
+
:icon="localBlinded ? 'i-lucide-eye' : 'i-lucide-eye-off'"
|
|
356
|
+
size="xs"
|
|
357
|
+
:color="localBlinded ? 'neutral' : 'warning'"
|
|
358
|
+
variant="ghost"
|
|
359
|
+
:loading="blindSaving"
|
|
360
|
+
:aria-label="localBlinded ? t('commentItem.unblind') : t('commentItem.blind')"
|
|
361
|
+
@click="toggleBlind"
|
|
362
|
+
/>
|
|
363
|
+
</UTooltip>
|
|
364
|
+
<UTooltip :text="t('common.action.reply')" :delay-duration="200">
|
|
365
|
+
<UButton
|
|
366
|
+
icon="i-lucide-reply"
|
|
367
|
+
size="xs"
|
|
368
|
+
color="neutral"
|
|
369
|
+
variant="ghost"
|
|
370
|
+
:aria-label="t('common.action.reply')"
|
|
371
|
+
@click="emit('reply', comment)"
|
|
372
|
+
/>
|
|
373
|
+
</UTooltip>
|
|
374
|
+
<UTooltip v-if="canDeleteComment(comment) && comment.id" :text="t('commentItem.delete')" :delay-duration="200">
|
|
375
|
+
<UButton
|
|
376
|
+
icon="i-lucide-trash-2"
|
|
377
|
+
size="xs"
|
|
378
|
+
variant="ghost"
|
|
379
|
+
color="error"
|
|
380
|
+
:loading="deleting"
|
|
381
|
+
:aria-label="t('commentItem.delete')"
|
|
382
|
+
@click="emit('delete', comment.id)"
|
|
383
|
+
/>
|
|
384
|
+
</UTooltip>
|
|
362
385
|
</div>
|
|
386
|
+
<UModal v-model:open="blindConfirmOpen" :title="t(blindTarget ? 'commentItem.blind' : 'commentItem.unblind')" :dismissible="!blindSaving" :close="blindSaving ? false : undefined">
|
|
387
|
+
<template #body>
|
|
388
|
+
<div class="space-y-4">
|
|
389
|
+
<p class="text-sm text-muted">{{ t(blindTarget ? 'commentItem.blindConfirm' : 'commentItem.unblindConfirm') }}</p>
|
|
390
|
+
<p v-if="editError" role="alert" class="text-sm text-error">{{ editError }}</p>
|
|
391
|
+
<div class="flex flex-wrap justify-end gap-2">
|
|
392
|
+
<UButton color="neutral" variant="outline" :label="t('common.action.cancel')" :disabled="blindSaving" @click="blindConfirmOpen = false" />
|
|
393
|
+
<UButton :label="t('common.action.confirm')" :loading="blindSaving" @click="confirmBlind" />
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</template>
|
|
397
|
+
</UModal>
|
|
363
398
|
</div>
|
|
364
399
|
</template>
|
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
import MemiBoardCommentSkeleton from './CommentSkeleton.vue'
|
|
10
10
|
import MemiBoardCommentThread from './CommentThread.vue'
|
|
11
11
|
|
|
12
|
-
const props = defineProps<{
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
boardId: string
|
|
14
|
+
postId: string
|
|
15
|
+
authorProfileTo?: (authorUid: string) => string | undefined
|
|
16
|
+
authorPostsTo?: (authorUid: string) => string | undefined
|
|
17
|
+
authorCommentsTo?: (authorUid: string) => string | undefined
|
|
18
|
+
}>()
|
|
13
19
|
|
|
14
20
|
const { t } = useMemiBoardI18n()
|
|
15
21
|
|
|
@@ -99,6 +105,9 @@ onUnmounted(() => {
|
|
|
99
105
|
:board-id="boardId"
|
|
100
106
|
:post-id="postId"
|
|
101
107
|
:now="now"
|
|
108
|
+
:author-profile-to="authorProfileTo"
|
|
109
|
+
:author-posts-to="authorPostsTo"
|
|
110
|
+
:author-comments-to="authorCommentsTo"
|
|
102
111
|
/>
|
|
103
112
|
|
|
104
113
|
<div
|
|
@@ -6,7 +6,15 @@ import MemiBoardCommentForm from './CommentForm.vue'
|
|
|
6
6
|
import MemiBoardCommentItem from './CommentItem.vue'
|
|
7
7
|
import MemiBoardCommentSkeleton from './CommentSkeleton.vue'
|
|
8
8
|
|
|
9
|
-
const props = defineProps<{
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
boardId: string
|
|
11
|
+
postId: string
|
|
12
|
+
root: CommentModel
|
|
13
|
+
now: number
|
|
14
|
+
authorProfileTo?: (authorUid: string) => string | undefined
|
|
15
|
+
authorPostsTo?: (authorUid: string) => string | undefined
|
|
16
|
+
authorCommentsTo?: (authorUid: string) => string | undefined
|
|
17
|
+
}>()
|
|
10
18
|
|
|
11
19
|
const { t } = useMemiBoardI18n()
|
|
12
20
|
|
|
@@ -81,6 +89,9 @@ async function handleDelete(comment: CommentModel) {
|
|
|
81
89
|
:board-id="boardId"
|
|
82
90
|
:post-id="postId"
|
|
83
91
|
:now="now"
|
|
92
|
+
:author-profile-to="authorProfileTo"
|
|
93
|
+
:author-posts-to="authorPostsTo"
|
|
94
|
+
:author-comments-to="authorCommentsTo"
|
|
84
95
|
:deleting="deletingId === root.id"
|
|
85
96
|
@reply="startReply"
|
|
86
97
|
@delete="handleDelete(root)"
|
|
@@ -104,8 +115,11 @@ async function handleDelete(comment: CommentModel) {
|
|
|
104
115
|
:key="reply.id"
|
|
105
116
|
:comment="reply"
|
|
106
117
|
:board-id="boardId"
|
|
107
|
-
|
|
118
|
+
:post-id="postId"
|
|
108
119
|
:now="now"
|
|
120
|
+
:author-profile-to="authorProfileTo"
|
|
121
|
+
:author-posts-to="authorPostsTo"
|
|
122
|
+
:author-comments-to="authorCommentsTo"
|
|
109
123
|
:deleting="deletingId === reply.id"
|
|
110
124
|
:class="(reply.depth ?? 1) >= 2 ? 'ml-4 sm:ml-8' : ''"
|
|
111
125
|
@reply="startReply"
|
|
@@ -61,14 +61,16 @@ function remove(index: number) {
|
|
|
61
61
|
@update:model-value="emit('dirty')"
|
|
62
62
|
/>
|
|
63
63
|
</div>
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
<UTooltip :text="t('common.action.delete')" :delay-duration="200">
|
|
65
|
+
<UButton
|
|
66
|
+
class="shrink-0"
|
|
67
|
+
icon="i-lucide-x"
|
|
68
|
+
color="neutral"
|
|
69
|
+
variant="ghost"
|
|
70
|
+
:aria-label="t('common.action.delete')"
|
|
71
|
+
@click="remove(index)"
|
|
72
|
+
/>
|
|
73
|
+
</UTooltip>
|
|
72
74
|
</div>
|
|
73
75
|
<div>
|
|
74
76
|
<UButton
|
|
@@ -271,15 +271,17 @@ watch(() => props.postId, () => {
|
|
|
271
271
|
{{ shownTitle }}
|
|
272
272
|
</h1>
|
|
273
273
|
</div>
|
|
274
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
274
|
+
<UTooltip :text="t('detail.backToList')" :delay-duration="200">
|
|
275
|
+
<UButton
|
|
276
|
+
icon="i-lucide-arrow-left"
|
|
277
|
+
:aria-label="t('detail.backToList')"
|
|
278
|
+
color="neutral"
|
|
279
|
+
variant="ghost"
|
|
280
|
+
size="sm"
|
|
281
|
+
class="shrink-0"
|
|
282
|
+
@click="emit('list')"
|
|
283
|
+
/>
|
|
284
|
+
</UTooltip>
|
|
283
285
|
</div>
|
|
284
286
|
<div
|
|
285
287
|
v-if="post.tags?.length"
|
|
@@ -435,7 +437,13 @@ watch(() => props.postId, () => {
|
|
|
435
437
|
<h2 class="text-sm font-medium text-muted">
|
|
436
438
|
{{ t('detail.comments') }}
|
|
437
439
|
</h2>
|
|
438
|
-
<MemiBoardCommentList
|
|
440
|
+
<MemiBoardCommentList
|
|
441
|
+
:board-id="boardId"
|
|
442
|
+
:post-id="postId"
|
|
443
|
+
:author-profile-to="authorProfileTo"
|
|
444
|
+
:author-posts-to="authorPostsTo"
|
|
445
|
+
:author-comments-to="authorCommentsTo"
|
|
446
|
+
/>
|
|
439
447
|
<MemiBoardCommentForm :board-id="boardId" :post-id="postId" />
|
|
440
448
|
</section>
|
|
441
449
|
|
|
@@ -50,7 +50,7 @@ const emit = defineEmits<{ saved: [id: string], cancel: [] }>()
|
|
|
50
50
|
|
|
51
51
|
const { user, isSignedIn, isWriteRestricted, restrictedMessage, canManageBoard } = useMemiBoardAuth()
|
|
52
52
|
const resolvedBoardId = computed(() => props.boardId || props.fixedCategory || '')
|
|
53
|
-
const { createPostId, getPost, createPost, updatePost,
|
|
53
|
+
const { createPostId, getPost, createPost, updatePost, setPostPinned } = useMemiBoardPosts(resolvedBoardId)
|
|
54
54
|
const { getBoard, ensureSettings } = useMemiBoardSettings()
|
|
55
55
|
// 보드 설정 언어 → 호스트 언어 순.
|
|
56
56
|
const { t, dir, locale, translationEnabled, aiEnabled } = useMemiBoardI18n({ boardLocale: () => getBoard(resolvedBoardId.value)?.locale })
|
|
@@ -1161,9 +1161,6 @@ async function handleSubmit() {
|
|
|
1161
1161
|
|
|
1162
1162
|
saving.value = true
|
|
1163
1163
|
submitHint.value = t('editor.hint.reviewing')
|
|
1164
|
-
// 새 글 slug 중복 확인을 검열과 동시에 시작 — 검열에 걸리면 결과만 버린다.
|
|
1165
|
-
const slugPromise = !props.postId && !imageBoard ? resolveUniqueSlug(title.value) : null
|
|
1166
|
-
slugPromise?.catch(() => {})
|
|
1167
1164
|
try {
|
|
1168
1165
|
const plain = plainTextFromHtml(content.value)
|
|
1169
1166
|
const moderationText = `${title.value}\n${plain}`
|
|
@@ -1226,7 +1223,6 @@ async function handleSubmit() {
|
|
|
1226
1223
|
else {
|
|
1227
1224
|
const id = await createPost({
|
|
1228
1225
|
...payload,
|
|
1229
|
-
slug: (await slugPromise) ?? undefined,
|
|
1230
1226
|
postId: attachmentNamespace.value,
|
|
1231
1227
|
authorUid: user.value.uid,
|
|
1232
1228
|
authorName: user.value.displayName,
|