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.
Files changed (78) hide show
  1. package/README.md +55 -9
  2. package/dist/{ar-CQhBdHu3.js → ar-GNguZBN4.js} +45 -4
  3. package/dist/ar-GNguZBN4.js.map +1 -0
  4. package/dist/{commentImages-DsW2yCqu.js → commentImages-CRK_2uVi.js} +195 -70
  5. package/dist/commentImages-CRK_2uVi.js.map +1 -0
  6. package/dist/composables/useMemiBoardModeration.d.ts +2 -0
  7. package/dist/composables/useMemiBoardPosts.d.ts +1 -1
  8. package/dist/composables/useMemiBoardReports.d.ts +102 -3
  9. package/dist/composables/useMemiBoardUserDirectory.d.ts +20 -0
  10. package/dist/composables/useMemiBoardUserProfile.d.ts +1 -1
  11. package/dist/composables/useMemiBoardUsers.d.ts +1 -1
  12. package/dist/{de-B4GZqxQj.js → de-CS0iRLqe.js} +45 -4
  13. package/dist/de-CS0iRLqe.js.map +1 -0
  14. package/dist/{en-BDkfivhc.js → en-DeExi-DR.js} +129 -4
  15. package/dist/en-DeExi-DR.js.map +1 -0
  16. package/dist/{es-DzFDaLwo.js → es-dlEFrox-.js} +45 -4
  17. package/dist/es-dlEFrox-.js.map +1 -0
  18. package/dist/{fr-Zf5mcs8m.js → fr-B9sjKJF4.js} +45 -4
  19. package/dist/fr-B9sjKJF4.js.map +1 -0
  20. package/dist/{id-CaWN8212.js → id-Ca7skouh.js} +45 -4
  21. package/dist/id-Ca7skouh.js.map +1 -0
  22. package/dist/index.d.ts +3 -1
  23. package/dist/index.js +1089 -829
  24. package/dist/index.js.map +1 -1
  25. package/dist/{ja-xDttH9CE.js → ja-ByVLGIDd.js} +45 -4
  26. package/dist/ja-ByVLGIDd.js.map +1 -0
  27. package/dist/module.mjs +4 -0
  28. package/dist/module.mjs.map +1 -1
  29. package/dist/{pt-WIfxYYWQ.js → pt-COQL_Q7A.js} +45 -4
  30. package/dist/pt-COQL_Q7A.js.map +1 -0
  31. package/dist/runtime/components/AccountDeletion.vue +64 -0
  32. package/dist/runtime/components/AuthorMenu.vue +4 -4
  33. package/dist/runtime/components/CommentFeed.vue +17 -13
  34. package/dist/runtime/components/CommentForm.vue +58 -56
  35. package/dist/runtime/components/CommentImageField.vue +18 -13
  36. package/dist/runtime/components/CommentItem.vue +95 -60
  37. package/dist/runtime/components/CommentList.vue +10 -1
  38. package/dist/runtime/components/CommentThread.vue +16 -2
  39. package/dist/runtime/components/CustomFieldEditor.vue +10 -8
  40. package/dist/runtime/components/Detail.vue +18 -10
  41. package/dist/runtime/components/Editor.vue +1 -5
  42. package/dist/runtime/components/ImageLightbox.vue +126 -58
  43. package/dist/runtime/components/LikeButton.vue +14 -11
  44. package/dist/runtime/components/ListViewSwitch.vue +10 -10
  45. package/dist/runtime/components/Profile.vue +14 -2
  46. package/dist/runtime/components/ProfileEditModal.vue +20 -3
  47. package/dist/runtime/components/ReportButton.vue +22 -13
  48. package/dist/runtime/components/ReportList.vue +77 -93
  49. package/dist/runtime/components/UserListControls.vue +29 -0
  50. package/dist/runtime/components/UserListPagination.vue +17 -0
  51. package/dist/runtime/components/UserManagementEmail.vue +21 -0
  52. package/dist/runtime/components/UserRoleGuide.vue +38 -0
  53. package/dist/runtime/components/Users.vue +21 -59
  54. package/dist/runtime/components/UsersSettings.vue +119 -0
  55. package/dist/storage.js +1 -1
  56. package/dist/types.d.ts +5 -0
  57. package/dist/utils/postSlug.d.ts +12 -0
  58. package/dist/utils/profileNameModeration.d.ts +8 -0
  59. package/dist/utils/userDirectoryPage.d.ts +19 -0
  60. package/dist/{zh-CI6Sd8PD.js → zh-DBjX9qAS.js} +45 -4
  61. package/dist/zh-DBjX9qAS.js.map +1 -0
  62. package/docs/account-deletion.md +60 -0
  63. package/docs/firebase-setup.md +77 -0
  64. package/docs/firestore.rules.example +78 -4
  65. package/docs/host-integration.md +85 -11
  66. package/docs/storage.rules.example +7 -2
  67. package/package.json +3 -2
  68. package/scripts/backfill-post-slugs.mjs +38 -0
  69. package/dist/ar-CQhBdHu3.js.map +0 -1
  70. package/dist/commentImages-DsW2yCqu.js.map +0 -1
  71. package/dist/de-B4GZqxQj.js.map +0 -1
  72. package/dist/en-BDkfivhc.js.map +0 -1
  73. package/dist/es-DzFDaLwo.js.map +0 -1
  74. package/dist/fr-Zf5mcs8m.js.map +0 -1
  75. package/dist/id-CaWN8212.js.map +0 -1
  76. package/dist/ja-xDttH9CE.js.map +0 -1
  77. package/dist/pt-WIfxYYWQ.js.map +0 -1
  78. 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="flex flex-col gap-2"
137
+ class="overflow-hidden rounded-xl border border-default bg-default"
138
138
  @submit.prevent="handleSubmit"
139
139
  >
140
- <UTextarea
141
- v-model="body"
142
- @paste="imageField?.handlePaste($event)"
143
- :placeholder="parent ? t('commentForm.replyTo', { name: parent.authorName || t('common.label.user') }) : t('commentForm.placeholder')"
144
- :rows="2"
145
- autoresize
146
- :maxrows="10"
147
- :disabled="submitting || aiBusy || isWriteRestricted"
148
- :maxlength="COMMENT_BODY_MAX_LENGTH"
149
- />
150
- <MemiBoardCommentAi v-model="body" v-model:translations="translations" :board-id="boardId"
151
- :disabled="submitting || imageBusy || isWriteRestricted" @busy="aiBusy = $event" />
152
- <MemiBoardCommentImageField
153
- ref="imageField"
154
- v-model="image"
155
- :post-id="postId"
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
- <div class="flex-1" />
181
- <UButton
182
- v-if="parent"
183
- type="button"
184
- size="sm"
185
- :label="t('common.action.cancel')"
186
- color="neutral"
187
- variant="ghost"
188
- :disabled="imageBusy || submitting || aiBusy"
189
- @click="handleCancel"
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
- <UButton
170
- type="button"
171
- icon="i-lucide-x"
172
- size="xs"
173
- color="neutral"
174
- variant="ghost"
175
- :aria-label="t('commentForm.removeImage')"
176
- :disabled="disabled || uploading"
177
- @click="remove"
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 py-4 text-left text-xs text-muted transition-colors disabled:cursor-not-allowed disabled:opacity-60"
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
- async function toggleBlind() {
147
- if (!props.comment.id || !user.value || !canManageBoard(props.comment.boardId)) return
148
- const next = !localBlinded.value
149
- const confirmed = window.confirm(next
150
- ? t('commentItem.blindConfirm')
151
- : t('commentItem.unblindConfirm'))
152
- if (!confirmed) return
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
- <span class="text-sm font-medium">{{ comment.authorName ?? t('common.label.anonymous') }}</span>
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
- <UButton
306
- v-if="isSignedIn && commentLikes && comment.id"
307
- icon="i-lucide-heart"
308
- size="xs"
309
- variant="ghost"
310
- :color="isLiked ? 'error' : 'neutral'"
311
- :label="String(localLikeCount)"
312
- :loading="likePending"
313
- :aria-label="isLiked ? t('common.action.unlike') : t('common.action.like')"
314
- @click="toggleLike"
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
- <UButton
325
- v-if="canEditComment(comment) && comment.id && !localBlinded"
326
- icon="i-lucide-pencil"
327
- size="xs"
328
- color="neutral"
329
- variant="ghost"
330
- :aria-label="t('commentItem.edit')"
331
- :disabled="editing"
332
- @click="startEdit"
333
- />
334
- <UButton
335
- v-if="canManageBoard(comment.boardId) && comment.id"
336
- :icon="localBlinded ? 'i-lucide-eye' : 'i-lucide-eye-off'"
337
- size="xs"
338
- :color="localBlinded ? 'neutral' : 'warning'"
339
- variant="ghost"
340
- :loading="blindSaving"
341
- :aria-label="localBlinded ? t('commentItem.unblind') : t('commentItem.blind')"
342
- @click="toggleBlind"
343
- />
344
- <UButton
345
- icon="i-lucide-reply"
346
- size="xs"
347
- color="neutral"
348
- variant="ghost"
349
- :aria-label="t('common.action.reply')"
350
- @click="emit('reply', comment)"
351
- />
352
- <UButton
353
- v-if="canDeleteComment(comment) && comment.id"
354
- icon="i-lucide-trash-2"
355
- size="xs"
356
- variant="ghost"
357
- color="error"
358
- :loading="deleting"
359
- :aria-label="t('commentItem.delete')"
360
- @click="emit('delete', comment.id)"
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<{ boardId: string, postId: string }>()
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<{ boardId: string, postId: string, root: CommentModel, now: number }>()
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
- :post-id="postId"
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
- <UButton
65
- class="shrink-0"
66
- icon="i-lucide-x"
67
- color="neutral"
68
- variant="ghost"
69
- :aria-label="t('common.action.delete')"
70
- @click="remove(index)"
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
- <UButton
275
- icon="i-lucide-arrow-left"
276
- :aria-label="t('detail.backToList')"
277
- color="neutral"
278
- variant="ghost"
279
- size="sm"
280
- class="shrink-0"
281
- @click="emit('list')"
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 :board-id="boardId" :post-id="postId" />
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, resolveUniqueSlug, setPostPinned } = useMemiBoardPosts(resolvedBoardId)
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,