memi-board 0.52.0 → 0.52.2
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 +297 -297
- package/dist/{commentImages-CzXO42gE.js → commentImages-D_JJnuoj.js} +5 -4
- package/dist/commentImages-D_JJnuoj.js.map +1 -0
- package/dist/composables/useMemiBoardAutoTranslate.d.ts +1 -0
- package/dist/composables/useMemiBoardComments.d.ts +35 -2
- package/dist/composables/useMemiBoardWritingAssistant.d.ts +6 -1
- package/dist/{en-CmPLbFcN.js → en-DSCLJyUd.js} +4 -3
- package/dist/en-DSCLJyUd.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +987 -903
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/CommentAi.vue +93 -0
- package/dist/runtime/components/CommentFeed.vue +3 -3
- package/dist/runtime/components/CommentForm.vue +11 -4
- package/dist/runtime/components/CommentItem.vue +20 -8
- package/dist/runtime/components/Editor.vue +0 -2
- package/dist/runtime/components/MiniComments.vue +3 -3
- package/dist/runtime/components/UserCommentList.vue +3 -3
- package/dist/storage.js +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/utils/localizedFields.d.ts +3 -1
- package/docs/firebase-setup.md +85 -85
- package/docs/firestore.rules.example +58 -58
- package/docs/host-integration.md +80 -80
- package/docs/internationalization.md +76 -72
- package/docs/roles-migration.md +18 -18
- package/docs/storage.rules.example +18 -18
- package/package.json +2 -2
- package/dist/commentImages-CzXO42gE.js.map +0 -1
- package/dist/en-CmPLbFcN.js.map +0 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import { boardTranslationLocales, COMMENT_BODY_MAX_LENGTH, useMemiBoardI18n, useMemiBoardSettings, useMemiBoardWritingAssistant } from 'memi-board/runtime'
|
|
4
|
+
import type { CommentTranslationSet, WritingAssistantAction } from 'memi-board/runtime'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{ boardId: string, disabled?: boolean }>()
|
|
7
|
+
const body = defineModel<string>({ required: true })
|
|
8
|
+
const translations = defineModel<CommentTranslationSet | undefined>('translations')
|
|
9
|
+
const emit = defineEmits<{ busy: [value: boolean] }>()
|
|
10
|
+
const { t, locale, translationEnabled } = useMemiBoardI18n()
|
|
11
|
+
const { getBoard } = useMemiBoardSettings()
|
|
12
|
+
const { assist, translateComment } = useMemiBoardWritingAssistant({ locale })
|
|
13
|
+
const targets = computed(() => boardTranslationLocales(getBoard(props.boardId)))
|
|
14
|
+
const running = ref(false)
|
|
15
|
+
const error = ref('')
|
|
16
|
+
const notice = ref('')
|
|
17
|
+
const previewOpen = ref(false)
|
|
18
|
+
const before = ref('')
|
|
19
|
+
const result = ref('')
|
|
20
|
+
const customOpen = ref(false)
|
|
21
|
+
const instruction = ref('')
|
|
22
|
+
watch(body, () => { translations.value = undefined; notice.value = '' }, { flush: 'sync' })
|
|
23
|
+
const items = computed(() => [
|
|
24
|
+
[
|
|
25
|
+
...(['proofread', 'polish', 'continue', 'summarize'] as const).map(action => ({ label: t(`editor.ai.${action}`), onSelect: () => run(action) })),
|
|
26
|
+
],
|
|
27
|
+
...(translationEnabled.value ? [[{ label: t('editor.ai.translateAll', { count: targets.value.length }), icon: 'i-lucide-languages', onSelect: translate }]] : []),
|
|
28
|
+
[{ label: t('editor.ai.custom'), onSelect: () => { instruction.value = ''; customOpen.value = true } }],
|
|
29
|
+
])
|
|
30
|
+
function busy(value: boolean) { running.value = value; emit('busy', value) }
|
|
31
|
+
async function run(action: WritingAssistantAction) {
|
|
32
|
+
if (running.value || props.disabled || !body.value.trim()) return
|
|
33
|
+
before.value = body.value
|
|
34
|
+
error.value = ''; notice.value = ''
|
|
35
|
+
busy(true)
|
|
36
|
+
try {
|
|
37
|
+
const output = await assist({ action, content: before.value, selection: true, customInstruction: instruction.value })
|
|
38
|
+
result.value = action === 'continue' ? `${before.value}\n\n${output}` : output
|
|
39
|
+
if (result.value.length > COMMENT_BODY_MAX_LENGTH) throw new Error(t('comments.bodyTooLong', { max: COMMENT_BODY_MAX_LENGTH }))
|
|
40
|
+
previewOpen.value = true
|
|
41
|
+
}
|
|
42
|
+
catch (cause) { error.value = cause instanceof Error ? cause.message : t('editor.ai.failed') }
|
|
43
|
+
finally { busy(false) }
|
|
44
|
+
}
|
|
45
|
+
async function translate() {
|
|
46
|
+
if (running.value || props.disabled || !body.value.trim()) return
|
|
47
|
+
const source = body.value
|
|
48
|
+
error.value = ''; notice.value = ''
|
|
49
|
+
busy(true)
|
|
50
|
+
try {
|
|
51
|
+
const output = await translateComment({ body: source, targets: targets.value })
|
|
52
|
+
if (body.value === source) {
|
|
53
|
+
translations.value = output
|
|
54
|
+
notice.value = t('commentForm.translationPending', { count: targets.value.length })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (cause) { error.value = cause instanceof Error ? cause.message : t('editor.ai.failed') }
|
|
58
|
+
finally { busy(false) }
|
|
59
|
+
}
|
|
60
|
+
function apply() {
|
|
61
|
+
if (body.value === before.value) body.value = result.value
|
|
62
|
+
previewOpen.value = false
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
68
|
+
<UDropdownMenu :items="items">
|
|
69
|
+
<UButton type="button" icon="i-lucide-sparkles" label="AI" size="sm" color="neutral" variant="ghost"
|
|
70
|
+
:aria-label="t('editor.aiAssistant')" :loading="running" :disabled="disabled || running || !body.trim()" />
|
|
71
|
+
</UDropdownMenu>
|
|
72
|
+
<span v-if="error" class="text-xs text-error" role="alert">{{ error }}</span>
|
|
73
|
+
<span v-else-if="notice" class="text-xs text-primary" role="status">{{ notice }}</span>
|
|
74
|
+
</div>
|
|
75
|
+
<UModal v-model:open="customOpen" :title="t('editor.ai.custom')">
|
|
76
|
+
<template #body><UTextarea v-model="instruction" :maxlength="200" /></template>
|
|
77
|
+
<template #footer>
|
|
78
|
+
<UButton type="button" :disabled="!instruction.trim()" :label="t('editor.apply')" @click="customOpen = false; run('custom')" />
|
|
79
|
+
</template>
|
|
80
|
+
</UModal>
|
|
81
|
+
<UModal v-model:open="previewOpen" :title="t('editor.aiPreview.title')">
|
|
82
|
+
<template #body>
|
|
83
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
84
|
+
<div><p class="text-xs text-muted">{{ t('editor.aiPreview.before') }}</p><p class="whitespace-pre-wrap break-words text-sm">{{ before }}</p></div>
|
|
85
|
+
<div><p class="text-xs text-primary">{{ t('editor.aiPreview.after') }}</p><p class="whitespace-pre-wrap break-words text-sm">{{ result }}</p></div>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
<template #footer>
|
|
89
|
+
<UButton type="button" color="neutral" variant="ghost" :label="t('common.action.cancel')" @click="previewOpen = false" />
|
|
90
|
+
<UButton type="button" :label="t('editor.apply')" :disabled="disabled || body !== before" @click="apply" />
|
|
91
|
+
</template>
|
|
92
|
+
</UModal>
|
|
93
|
+
</template>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, resolveComponent, toRef } from 'vue'
|
|
3
|
-
import { useMemiBoardCommentFeed, useMemiBoardI18n } from 'memi-board/runtime'
|
|
3
|
+
import { displayCommentBody, useMemiBoardCommentFeed, useMemiBoardI18n } from 'memi-board/runtime'
|
|
4
4
|
import type { CommentFeedSort, CommentModel } from 'memi-board/runtime'
|
|
5
5
|
|
|
6
6
|
const NuxtLink = resolveComponent('NuxtLink')
|
|
@@ -18,7 +18,7 @@ const props = withDefaults(defineProps<{
|
|
|
18
18
|
|
|
19
19
|
const emit = defineEmits<{ select: [comment: CommentModel] }>()
|
|
20
20
|
|
|
21
|
-
const { t, formatRelativeDate } = useMemiBoardI18n()
|
|
21
|
+
const { t, locale, formatRelativeDate } = useMemiBoardI18n()
|
|
22
22
|
|
|
23
23
|
const { comments, commentsPending, hasMore, loadingMore, loadError, loadMore } = useMemiBoardCommentFeed(
|
|
24
24
|
toRef(props, 'sort'),
|
|
@@ -111,7 +111,7 @@ const emptyMessage = computed(() =>
|
|
|
111
111
|
v-if="comment.replyToName"
|
|
112
112
|
class="mr-1 text-primary"
|
|
113
113
|
>@{{ comment.replyToName }}</span>
|
|
114
|
-
{{ comment
|
|
114
|
+
{{ displayCommentBody(comment, locale) }}
|
|
115
115
|
</p>
|
|
116
116
|
<p
|
|
117
117
|
v-if="(comment.likeCount ?? 0) > 0"
|
|
@@ -6,8 +6,9 @@ import { COMMENT_BODY_MAX_LENGTH, useMemiBoardComments } from 'memi-board/runtim
|
|
|
6
6
|
import { useMemiBoardModeration } from 'memi-board/runtime'
|
|
7
7
|
import { useMemiBoardSettings } from 'memi-board/runtime'
|
|
8
8
|
import { useMemiBoardI18n } from 'memi-board/runtime'
|
|
9
|
-
import type { Attachment, CommentModel } from 'memi-board/runtime'
|
|
9
|
+
import type { Attachment, CommentModel, CommentTranslationSet } from 'memi-board/runtime'
|
|
10
10
|
import { useMemiBoardStorage } from 'memi-board/storage'
|
|
11
|
+
import MemiBoardCommentAi from './CommentAi.vue'
|
|
11
12
|
import MemiBoardCommentImageField from './CommentImageField.vue'
|
|
12
13
|
|
|
13
14
|
const props = defineProps<{ boardId: string, postId: string, parent?: CommentModel | null }>()
|
|
@@ -37,6 +38,8 @@ const deniedMessage = computed(() => {
|
|
|
37
38
|
})
|
|
38
39
|
|
|
39
40
|
const body = ref('')
|
|
41
|
+
const translations = ref<CommentTranslationSet>()
|
|
42
|
+
const aiBusy = ref(false)
|
|
40
43
|
const draftId = ref(newCommentId())
|
|
41
44
|
const image = ref<Attachment | null>(null)
|
|
42
45
|
const imageField = ref<InstanceType<typeof MemiBoardCommentImageField> | null>(null)
|
|
@@ -56,7 +59,7 @@ async function handleCancel() {
|
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
async function handleSubmit() {
|
|
59
|
-
if (submitting.value || imageBusy.value) return
|
|
62
|
+
if (submitting.value || imageBusy.value || aiBusy.value) return
|
|
60
63
|
error.value = ''
|
|
61
64
|
if (!body.value.trim()) return
|
|
62
65
|
if (!user.value) {
|
|
@@ -83,6 +86,7 @@ async function handleSubmit() {
|
|
|
83
86
|
const input = {
|
|
84
87
|
id: draftId.value,
|
|
85
88
|
body: body.value,
|
|
89
|
+
translations: translations.value,
|
|
86
90
|
images: image.value ? [image.value] : [],
|
|
87
91
|
authorUid: user.value.uid,
|
|
88
92
|
authorName: user.value.displayName,
|
|
@@ -120,8 +124,11 @@ onUnmounted(() => {
|
|
|
120
124
|
@paste="imageField?.handlePaste($event)"
|
|
121
125
|
:placeholder="parent ? t('commentForm.replyTo', { name: parent.authorName || t('common.label.user') }) : t('commentForm.placeholder')"
|
|
122
126
|
:rows="2"
|
|
127
|
+
:disabled="submitting || aiBusy || isWriteRestricted"
|
|
123
128
|
:maxlength="COMMENT_BODY_MAX_LENGTH"
|
|
124
129
|
/>
|
|
130
|
+
<MemiBoardCommentAi v-model="body" v-model:translations="translations" :board-id="boardId"
|
|
131
|
+
:disabled="submitting || imageBusy || isWriteRestricted" @busy="aiBusy = $event" />
|
|
125
132
|
<MemiBoardCommentImageField
|
|
126
133
|
ref="imageField"
|
|
127
134
|
v-model="image"
|
|
@@ -152,7 +159,7 @@ onUnmounted(() => {
|
|
|
152
159
|
:label="t('common.action.cancel')"
|
|
153
160
|
color="neutral"
|
|
154
161
|
variant="ghost"
|
|
155
|
-
:disabled="imageBusy"
|
|
162
|
+
:disabled="imageBusy || submitting || aiBusy"
|
|
156
163
|
@click="handleCancel"
|
|
157
164
|
/>
|
|
158
165
|
<UButton
|
|
@@ -162,7 +169,7 @@ onUnmounted(() => {
|
|
|
162
169
|
size="sm"
|
|
163
170
|
:label="parent ? t('commentForm.submitReply') : t('commentForm.submit')"
|
|
164
171
|
:loading="submitting"
|
|
165
|
-
:disabled="!body.trim() || isWriteRestricted || imageBusy"
|
|
172
|
+
:disabled="!body.trim() || isWriteRestricted || imageBusy || aiBusy"
|
|
166
173
|
/>
|
|
167
174
|
</div>
|
|
168
175
|
</form>
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
import { imeSafeSubmitClick, imeSafeSubmitPointerDown } from 'memi-board/runtime'
|
|
3
3
|
import { computed, inject, ref, watch } from 'vue'
|
|
4
4
|
import { Timestamp } from 'firebase/firestore'
|
|
5
|
-
import type { Attachment, CommentModel } from 'memi-board/runtime'
|
|
5
|
+
import type { Attachment, CommentModel, CommentTranslationSet } from 'memi-board/runtime'
|
|
6
6
|
import { useMemiBoardStorage } from 'memi-board/storage'
|
|
7
7
|
import {
|
|
8
8
|
COMMENT_BODY_MAX_LENGTH,
|
|
9
|
+
displayCommentBody,
|
|
9
10
|
memiBoardCommentLikesKey,
|
|
10
11
|
useMemiBoardAuth,
|
|
11
12
|
useMemiBoardComments,
|
|
12
13
|
useMemiBoardI18n,
|
|
13
14
|
useMemiBoardModeration,
|
|
14
15
|
} from 'memi-board/runtime'
|
|
16
|
+
import MemiBoardCommentAi from './CommentAi.vue'
|
|
15
17
|
import MemiBoardCommentImageField from './CommentImageField.vue'
|
|
16
18
|
|
|
17
19
|
const props = defineProps<{
|
|
@@ -35,10 +37,14 @@ const { updateComment, setCommentBlinded } = useMemiBoardComments(
|
|
|
35
37
|
)
|
|
36
38
|
const { checkText } = useMemiBoardModeration()
|
|
37
39
|
const { deleteAttachment } = useMemiBoardStorage()
|
|
38
|
-
const { t, formatRelativeDate, formatTimestampDetails } = useMemiBoardI18n()
|
|
40
|
+
const { t, locale, formatRelativeDate, formatTimestampDetails } = useMemiBoardI18n()
|
|
39
41
|
const commentLikes = inject(memiBoardCommentLikesKey, null)
|
|
40
42
|
|
|
41
43
|
const editing = ref(false)
|
|
44
|
+
const translations = ref<CommentTranslationSet>()
|
|
45
|
+
const aiBusy = ref(false)
|
|
46
|
+
const localTranslations = ref<CommentTranslationSet>()
|
|
47
|
+
const displayedBody = computed(() => displayCommentBody({ ...props.comment, ...localTranslations.value, body: localBody.value }, locale.value))
|
|
42
48
|
const editBody = ref(props.comment.body)
|
|
43
49
|
const localBody = ref(props.comment.body)
|
|
44
50
|
const localImage = ref<Attachment | null>(props.comment.images?.[0] ?? null)
|
|
@@ -58,6 +64,7 @@ const likePending = computed(() => commentLikes?.isPending(props.comment.id) ??
|
|
|
58
64
|
const imageChanged = computed(() => (editImage.value?.path ?? '') !== (localImage.value?.path ?? ''))
|
|
59
65
|
|
|
60
66
|
watch(() => props.comment.body, (body) => {
|
|
67
|
+
localTranslations.value = undefined
|
|
61
68
|
localBody.value = body
|
|
62
69
|
if (!editing.value) editBody.value = body
|
|
63
70
|
})
|
|
@@ -81,6 +88,7 @@ function startEdit() {
|
|
|
81
88
|
editBody.value = localBody.value
|
|
82
89
|
editImage.value = localImage.value
|
|
83
90
|
editError.value = ''
|
|
91
|
+
translations.value = undefined
|
|
84
92
|
editing.value = true
|
|
85
93
|
}
|
|
86
94
|
|
|
@@ -95,10 +103,10 @@ async function cancelEdit() {
|
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
async function saveEdit() {
|
|
98
|
-
if (saving.value || imageBusy.value) return
|
|
106
|
+
if (saving.value || imageBusy.value || aiBusy.value) return
|
|
99
107
|
const body = editBody.value.trim()
|
|
100
108
|
if (!props.comment.id || !body) return
|
|
101
|
-
if (body === localBody.value && !imageChanged.value) {
|
|
109
|
+
if (body === localBody.value && !imageChanged.value && !translations.value) {
|
|
102
110
|
editing.value = false
|
|
103
111
|
return
|
|
104
112
|
}
|
|
@@ -117,7 +125,8 @@ async function saveEdit() {
|
|
|
117
125
|
}
|
|
118
126
|
const previous = localImage.value
|
|
119
127
|
const next = editImage.value
|
|
120
|
-
await updateComment(props.comment.id, body, next ? [next] : [])
|
|
128
|
+
await updateComment(props.comment.id, body, next ? [next] : [], translations.value)
|
|
129
|
+
localTranslations.value = translations.value
|
|
121
130
|
localBody.value = body
|
|
122
131
|
localImage.value = next
|
|
123
132
|
localUpdatedAt.value = Timestamp.now()
|
|
@@ -203,8 +212,11 @@ async function toggleLike() {
|
|
|
203
212
|
@paste="imageField?.handlePaste($event)"
|
|
204
213
|
:rows="2"
|
|
205
214
|
:maxlength="COMMENT_BODY_MAX_LENGTH"
|
|
215
|
+
:disabled="saving || aiBusy"
|
|
206
216
|
autofocus
|
|
207
217
|
/>
|
|
218
|
+
<MemiBoardCommentAi v-model="editBody" v-model:translations="translations" :board-id="boardId"
|
|
219
|
+
:disabled="saving || imageBusy || isWriteRestricted" @busy="aiBusy = $event" />
|
|
208
220
|
<MemiBoardCommentImageField
|
|
209
221
|
ref="imageField"
|
|
210
222
|
v-if="comment.id"
|
|
@@ -227,7 +239,7 @@ async function toggleLike() {
|
|
|
227
239
|
size="xs"
|
|
228
240
|
color="neutral"
|
|
229
241
|
variant="ghost"
|
|
230
|
-
:disabled="saving || imageBusy"
|
|
242
|
+
:disabled="saving || imageBusy || aiBusy"
|
|
231
243
|
@click="cancelEdit"
|
|
232
244
|
/>
|
|
233
245
|
<UButton
|
|
@@ -237,7 +249,7 @@ async function toggleLike() {
|
|
|
237
249
|
:label="t('common.action.save')"
|
|
238
250
|
size="xs"
|
|
239
251
|
:loading="saving"
|
|
240
|
-
:disabled="!editBody.trim() || imageBusy || (editBody.trim() === localBody && !imageChanged)"
|
|
252
|
+
:disabled="!editBody.trim() || imageBusy || aiBusy || (editBody.trim() === localBody && !imageChanged && !translations)"
|
|
241
253
|
/>
|
|
242
254
|
</div>
|
|
243
255
|
</form>
|
|
@@ -260,7 +272,7 @@ async function toggleLike() {
|
|
|
260
272
|
<div v-else>
|
|
261
273
|
<p class="whitespace-pre-wrap break-words text-sm">
|
|
262
274
|
<span v-if="comment.parentId && comment.replyToName" class="mr-1 text-primary">@{{ comment.replyToName }}</span>
|
|
263
|
-
{{
|
|
275
|
+
{{ displayedBody }}
|
|
264
276
|
</p>
|
|
265
277
|
<a
|
|
266
278
|
v-if="localImage"
|
|
@@ -759,7 +759,6 @@ async function translateAllLocales() {
|
|
|
759
759
|
aiRunning.value = true
|
|
760
760
|
try {
|
|
761
761
|
const translations = await translatePost({
|
|
762
|
-
source: locale.value,
|
|
763
762
|
targets: translationTargets.value,
|
|
764
763
|
title: storedTitle,
|
|
765
764
|
summary,
|
|
@@ -1061,7 +1060,6 @@ async function handleSubmit() {
|
|
|
1061
1060
|
if (!props.postId && translationEnabled.value && getBoard(resolvedBoardId.value)?.autoTranslatePosts === true && !translations) {
|
|
1062
1061
|
submitHint.value = t('editor.hint.translating', { count: translationTargets.value.length })
|
|
1063
1062
|
translations = await translatePost({
|
|
1064
|
-
source: locale.value,
|
|
1065
1063
|
targets: translationTargets.value,
|
|
1066
1064
|
title: storedTitle,
|
|
1067
1065
|
summary: buildPostPreview(bodyContent, imageBoard ? [] : attachments.value).summary,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, resolveComponent } from 'vue'
|
|
3
3
|
import type { BoardSection, CommentModel } from 'memi-board/runtime'
|
|
4
|
-
import { miniCommentLink, useMemiBoardCommentFeed, useMemiBoardI18n } from 'memi-board/runtime'
|
|
4
|
+
import { displayCommentBody, miniCommentLink, useMemiBoardCommentFeed, useMemiBoardI18n } from 'memi-board/runtime'
|
|
5
5
|
|
|
6
6
|
const props = defineProps<{
|
|
7
7
|
section: BoardSection
|
|
@@ -9,7 +9,7 @@ const props = defineProps<{
|
|
|
9
9
|
}>()
|
|
10
10
|
|
|
11
11
|
const NuxtLink = resolveComponent('NuxtLink')
|
|
12
|
-
const { t, formatRelativeDate } = useMemiBoardI18n()
|
|
12
|
+
const { t, locale, formatRelativeDate } = useMemiBoardI18n()
|
|
13
13
|
const now = ref(Date.now())
|
|
14
14
|
const sort = computed(() => props.section.sort === 'likes' ? 'likes' : 'latest')
|
|
15
15
|
const { comments, commentsPending } = useMemiBoardCommentFeed(sort, {
|
|
@@ -56,7 +56,7 @@ function commentTo(comment: CommentModel) {
|
|
|
56
56
|
v-if="comment.replyToName"
|
|
57
57
|
class="mr-1 text-primary"
|
|
58
58
|
>@{{ comment.replyToName }}</span>
|
|
59
|
-
{{ comment
|
|
59
|
+
{{ displayCommentBody(comment, locale) }}
|
|
60
60
|
</p>
|
|
61
61
|
<p class="flex flex-wrap items-center gap-x-1.5 text-[11px] text-muted">
|
|
62
62
|
<span>{{ comment.authorName || t('common.label.anonymous') }}</span>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, resolveComponent } from 'vue'
|
|
3
|
-
import { useMemiBoardI18n, useMemiBoardUserComments } from 'memi-board/runtime'
|
|
3
|
+
import { displayCommentBody, useMemiBoardI18n, useMemiBoardUserComments } from 'memi-board/runtime'
|
|
4
4
|
import type { CommentModel } from 'memi-board/runtime'
|
|
5
5
|
|
|
6
6
|
const NuxtLink = resolveComponent('NuxtLink')
|
|
@@ -20,7 +20,7 @@ const props = withDefaults(defineProps<{
|
|
|
20
20
|
|
|
21
21
|
const emit = defineEmits<{ select: [comment: CommentModel] }>()
|
|
22
22
|
|
|
23
|
-
const { t, formatRelativeDate } = useMemiBoardI18n()
|
|
23
|
+
const { t, locale, formatRelativeDate } = useMemiBoardI18n()
|
|
24
24
|
|
|
25
25
|
const { comments, commentsPending, hasMore, loadingMore, loadError, loadMore } = useMemiBoardUserComments(
|
|
26
26
|
() => props.uid,
|
|
@@ -83,7 +83,7 @@ function commentLink(comment: CommentModel): string | undefined {
|
|
|
83
83
|
v-else
|
|
84
84
|
class="text-sm text-highlighted line-clamp-3 whitespace-pre-wrap"
|
|
85
85
|
>
|
|
86
|
-
{{ comment
|
|
86
|
+
{{ displayCommentBody(comment, locale) }}
|
|
87
87
|
</p>
|
|
88
88
|
<p class="mt-1 text-xs text-muted">
|
|
89
89
|
{{ formatRelativeDate(comment.createdAt, now) }}
|
package/dist/storage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { W as e, i as t, m as n, n as r, s as i, w as a } from "./commentImages-
|
|
1
|
+
import { W as e, i as t, m as n, n as r, s as i, w as a } from "./commentImages-D_JJnuoj.js";
|
|
2
2
|
import { useFirebaseApp as o } from "vuefire";
|
|
3
3
|
import { getAuth as s } from "firebase/auth";
|
|
4
4
|
import { deleteObject as c, getDownloadURL as l, getStorage as u, ref as d, uploadBytes as f, uploadBytesResumable as p } from "firebase/storage";
|
package/dist/types.d.ts
CHANGED
|
@@ -115,6 +115,13 @@ export interface PostDetail extends PostModel {
|
|
|
115
115
|
* 보드 경계를 넘는 작성자별 글 목록(예: /board-user/{uid}) 등에서 PostModel 을 그대로 쓰면 된다.
|
|
116
116
|
*/
|
|
117
117
|
export type UserPostModel = PostModel;
|
|
118
|
+
export interface CommentTranslationSet {
|
|
119
|
+
i18n: MemiBoardI18nMap;
|
|
120
|
+
i18nSource: {
|
|
121
|
+
locale?: MemiBoardLocale;
|
|
122
|
+
body: string;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
118
125
|
export interface CommentModel {
|
|
119
126
|
id?: string;
|
|
120
127
|
/** 속한 글. flat 컬렉션이라 경로 대신 필드로 스코핑한다. */
|
|
@@ -127,6 +134,8 @@ export interface CommentModel {
|
|
|
127
134
|
*/
|
|
128
135
|
listed?: boolean;
|
|
129
136
|
body: string;
|
|
137
|
+
i18n?: MemiBoardI18nMap;
|
|
138
|
+
i18nSource?: CommentTranslationSet['i18nSource'];
|
|
130
139
|
authorUid: string;
|
|
131
140
|
authorName: string | null;
|
|
132
141
|
authorPhoto: string | null;
|
|
@@ -237,7 +246,7 @@ export interface BoardModel {
|
|
|
237
246
|
* 없거나 false 이면 번역하지 않는다.
|
|
238
247
|
*/
|
|
239
248
|
autoTranslate?: boolean;
|
|
240
|
-
/** 새
|
|
249
|
+
/** 새 글과 댓글·답글 작성 시 번역한다. 기본 false. */
|
|
241
250
|
autoTranslatePosts?: boolean;
|
|
242
251
|
/**
|
|
243
252
|
* 이름·설명과 글을 번역할 언어(MEMI_BOARD_LOCALES 코드).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MemiBoardLocale } from '../i18n/locales';
|
|
2
|
-
import { MemiBoardI18nMap, MemiBoardI18nSource, PostContentI18nSource, PostDetail, PostModel } from '../types';
|
|
2
|
+
import { CommentModel, MemiBoardI18nMap, MemiBoardI18nSource, PostContentI18nSource, PostDetail, PostModel } from '../types';
|
|
3
3
|
/** 본문처럼 긴 문자열이 번역 당시와 같은지 비교할 때 쓴다. */
|
|
4
4
|
export declare function hashText(value: string): string;
|
|
5
5
|
/** Firestore에 넣은 i18n 맵만 남긴다. 지원하지 않는 키와 빈 문자열은 버린다. */
|
|
@@ -41,3 +41,5 @@ export declare function boardCopyIsTranslated(board: {
|
|
|
41
41
|
i18nSource?: MemiBoardI18nSource;
|
|
42
42
|
translationLocales?: readonly string[];
|
|
43
43
|
}): boolean;
|
|
44
|
+
/** 수정 전의 번역은 숨기고 원문을 보여 준다. */
|
|
45
|
+
export declare function displayCommentBody(comment: Pick<CommentModel, 'body' | 'i18n' | 'i18nSource'>, locale?: string | null): string;
|