diff options
Diffstat (limited to 'packages/frontend/src/components/MkPostForm.vue')
| -rw-r--r-- | packages/frontend/src/components/MkPostForm.vue | 407 |
1 files changed, 217 insertions, 190 deletions
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 5c9ac40427..c9784fc40f 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -67,13 +67,14 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> <input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> <div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> - <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> + <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div> + <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> </div> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/> <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> - <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :user="postAccount ?? $i"/> + <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/> <div v-if="showingOptions" style="padding: 8px 16px;"> </div> <footer :class="$style.footer"> @@ -99,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide } from 'vue'; +import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue'; import * as mfm from '@sharkey/sfm-js'; import * as Misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; @@ -125,6 +126,7 @@ import { deepClone } from '@/scripts/clone.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement } from '@/scripts/achievements.js'; +import { emojiPicker } from '@/scripts/emoji-picker.js'; const modal = inject('modal'); @@ -135,6 +137,7 @@ const props = withDefaults(defineProps<{ mention?: Misskey.entities.User; specified?: Misskey.entities.User; initialText?: string; + initialCw?: string; initialVisibility?: (typeof Misskey.noteVisibilities)[number]; initialFiles?: Misskey.entities.DriveFile[]; initialLocalOnly?: boolean; @@ -144,7 +147,7 @@ const props = withDefaults(defineProps<{ fixed?: boolean; autofocus?: boolean; freezeAfterPosted?: boolean; - editId?: Misskey.entities.Note["id"]; + editId?: Misskey.entities.Note['id']; mock?: boolean; }>(), { initialVisibleUsers: () => [], @@ -163,41 +166,42 @@ const emit = defineEmits<{ (ev: 'fileChangeSensitive', fileId: string, to: boolean): void; }>(); -const textareaEl = $shallowRef<HTMLTextAreaElement | null>(null); -const cwInputEl = $shallowRef<HTMLInputElement | null>(null); -const hashtagsInputEl = $shallowRef<HTMLInputElement | null>(null); -const visibilityButton = $shallowRef<HTMLElement | null>(null); +const textareaEl = shallowRef<HTMLTextAreaElement | null>(null); +const cwInputEl = shallowRef<HTMLInputElement | null>(null); +const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null); +const visibilityButton = shallowRef<HTMLElement | null>(null); -let posting = $ref(false); -let posted = $ref(false); -let text = $ref(props.initialText ?? ''); -let files = $ref(props.initialFiles ?? []); -let poll = $ref<{ +const posting = ref(false); +const posted = ref(false); +const text = ref(props.initialText ?? ''); +const files = ref(props.initialFiles ?? []); +const poll = ref<{ choices: string[]; multiple: boolean; expiresAt: string | null; expiredAfter: string | null; } | null>(null); -let useCw = $ref(false); -let showPreview = $ref(defaultStore.state.showPreview); -watch($$(showPreview), () => defaultStore.set('showPreview', showPreview)); -let cw = $ref<string | null>(null); -let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); -let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]); -let visibleUsers = $ref([]); +const useCw = ref<boolean>(!!props.initialCw); +const showPreview = ref(defaultStore.state.showPreview); +watch(showPreview, () => defaultStore.set('showPreview', showPreview.value)); +const cw = ref<string | null>(props.initialCw ?? null); +const localOnly = ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); +const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]); +const visibleUsers = ref([]); if (props.initialVisibleUsers) { props.initialVisibleUsers.forEach(pushVisibleUser); } -let reactionAcceptance = $ref(defaultStore.state.reactionAcceptance); -let autocomplete = $ref(null); -let draghover = $ref(false); -let quoteId = $ref(null); -let hasNotSpecifiedMentions = $ref(false); -let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]')); -let imeText = $ref(''); -let showingOptions = $ref(false); +const reactionAcceptance = ref(defaultStore.state.reactionAcceptance); +const autocomplete = ref(null); +const draghover = ref(false); +const quoteId = ref(null); +const hasNotSpecifiedMentions = ref(false); +const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]')); +const imeText = ref(''); +const showingOptions = ref(false); +const textAreaReadOnly = ref(false); -const draftKey = $computed((): string => { +const draftKey = computed((): string => { let key = props.channel ? `channel:${props.channel.id}` : ''; if (props.renote) { @@ -211,7 +215,7 @@ const draftKey = $computed((): string => { return key; }); -const placeholder = $computed((): string => { +const placeholder = computed((): string => { if (props.renote) { return i18n.ts._postForm.quotePlaceholder; } else if (props.reply) { @@ -231,7 +235,7 @@ const placeholder = $computed((): string => { } }); -const submitText = $computed((): string => { +const submitText = computed((): string => { return props.renote ? i18n.ts.quote : props.reply @@ -239,45 +243,45 @@ const submitText = $computed((): string => { : i18n.ts.note; }); -const textLength = $computed((): number => { - return (text + imeText).trim().length; +const textLength = computed((): number => { + return (text.value + imeText.value).trim().length; }); -const maxTextLength = $computed((): number => { +const maxTextLength = computed((): number => { return instance ? instance.maxNoteTextLength : 1000; }); -const canPost = $computed((): boolean => { - return !props.mock && !posting && !posted && - (1 <= textLength || 1 <= files.length || !!poll || !!props.renote) && - (textLength <= maxTextLength) && - (!poll || poll.choices.length >= 2); +const canPost = computed((): boolean => { + return !props.mock && !posting.value && !posted.value && + (1 <= textLength.value || 1 <= files.value.length || !!poll.value || !!props.renote) && + (textLength.value <= maxTextLength.value) && + (!poll.value || poll.value.choices.length >= 2); }); -const withHashtags = $computed(defaultStore.makeGetterSetter('postFormWithHashtags')); -const hashtags = $computed(defaultStore.makeGetterSetter('postFormHashtags')); +const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags')); +const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags')); -watch($$(text), () => { +watch(text, () => { checkMissingMention(); }, { immediate: true }); -watch($$(visibility), () => { +watch(visibility, () => { checkMissingMention(); }, { immediate: true }); -watch($$(visibleUsers), () => { +watch(visibleUsers, () => { checkMissingMention(); }, { deep: true, }); if (props.mention) { - text = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`; - text += ' '; + text.value = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`; + text.value += ' '; } if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) { - text = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `; + text.value = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `; } if (props.reply && props.reply.text != null) { @@ -295,32 +299,32 @@ if (props.reply && props.reply.text != null) { if ($i.username === x.username && (x.host == null || x.host === host)) continue; // 重複は除外 - if (text.includes(`${mention} `)) continue; + if (text.value.includes(`${mention} `)) continue; - text += `${mention} `; + text.value += `${mention} `; } } -if ($i?.isSilenced && visibility === 'public') { - visibility = 'home'; +if ($i?.isSilenced && visibility.value === 'public') { + visibility.value = 'home'; } if (props.channel) { - visibility = 'public'; - localOnly = true; // TODO: チャンネルが連合するようになった折には消す + visibility.value = 'public'; + localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す } // 公開以外へのリプライ時は元の公開範囲を引き継ぐ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) { - if (props.reply.visibility === 'home' && visibility === 'followers') { - visibility = 'followers'; - } else if (['home', 'followers'].includes(props.reply.visibility) && visibility === 'specified') { - visibility = 'specified'; + if (props.reply.visibility === 'home' && visibility.value === 'followers') { + visibility.value = 'followers'; + } else if (['home', 'followers'].includes(props.reply.visibility) && visibility.value === 'specified') { + visibility.value = 'specified'; } else { - visibility = props.reply.visibility; + visibility.value = props.reply.visibility; } - if (visibility === 'specified') { + if (visibility.value === 'specified') { if (props.reply.visibleUserIds) { os.api('users/show', { userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId), @@ -338,24 +342,24 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib } if (props.specified) { - visibility = 'specified'; + visibility.value = 'specified'; pushVisibleUser(props.specified); } // keep cw when reply if (defaultStore.state.keepCw && props.reply && props.reply.cw) { - useCw = true; - cw = props.reply.cw; + useCw.value = true; + cw.value = props.reply.cw; } function watchForDraft() { - watch($$(text), () => saveDraft()); - watch($$(useCw), () => saveDraft()); - watch($$(cw), () => saveDraft()); - watch($$(poll), () => saveDraft()); - watch($$(files), () => saveDraft(), { deep: true }); - watch($$(visibility), () => saveDraft()); - watch($$(localOnly), () => saveDraft()); + watch(text, () => saveDraft()); + watch(useCw, () => saveDraft()); + watch(cw, () => saveDraft()); + watch(poll, () => saveDraft()); + watch(files, () => saveDraft(), { deep: true }); + watch(visibility, () => saveDraft()); + watch(localOnly, () => saveDraft()); } function MFMWindow() { @@ -363,36 +367,36 @@ function MFMWindow() { } function checkMissingMention() { - if (visibility === 'specified') { - const ast = mfm.parse(text); + if (visibility.value === 'specified') { + const ast = mfm.parse(text.value); for (const x of extractMentions(ast)) { - if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) { - hasNotSpecifiedMentions = true; + if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) { + hasNotSpecifiedMentions.value = true; return; } } - hasNotSpecifiedMentions = false; } + hasNotSpecifiedMentions.value = false; } function addMissingMention() { - const ast = mfm.parse(text); + const ast = mfm.parse(text.value); for (const x of extractMentions(ast)) { - if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) { + if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) { os.api('users/show', { username: x.username, host: x.host }).then(user => { - visibleUsers.push(user); + visibleUsers.value.push(user); }); } } } function togglePoll() { - if (poll) { - poll = null; + if (poll.value) { + poll.value = null; } else { - poll = { + poll.value = { choices: ['', ''], multiple: false, expiresAt: null, @@ -402,13 +406,13 @@ function togglePoll() { } function addTag(tag: string) { - insertTextAtCursor(textareaEl, ` #${tag} `); + insertTextAtCursor(textareaEl.value, ` #${tag} `); } function focus() { - if (textareaEl) { - textareaEl.focus(); - textareaEl.setSelectionRange(textareaEl.value.length, textareaEl.value.length); + if (textareaEl.value) { + textareaEl.value.focus(); + textareaEl.value.setSelectionRange(textareaEl.value.value.length, textareaEl.value.value.length); } } @@ -417,55 +421,55 @@ function chooseFileFrom(ev) { selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => { for (const file of files_) { - files.push(file); + files.value.push(file); } }); } function detachFile(id) { - files = files.filter(x => x.id !== id); + files.value = files.value.filter(x => x.id !== id); } function updateFileSensitive(file, sensitive) { if (props.mock) { emit('fileChangeSensitive', file.id, sensitive); } - files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive; + files.value[files.value.findIndex(x => x.id === file.id)].isSensitive = sensitive; } function updateFileName(file, name) { - files[files.findIndex(x => x.id === file.id)].name = name; + files.value[files.value.findIndex(x => x.id === file.id)].name = name; } function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities.DriveFile): void { - files[files.findIndex(x => x.id === file.id)] = newFile; + files.value[files.value.findIndex(x => x.id === file.id)] = newFile; } function upload(file: File, name?: string): void { if (props.mock) return; uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { - files.push(res); + files.value.push(res); }); } function setVisibility() { if (props.channel) { - visibility = 'public'; - localOnly = true; // TODO: チャンネルが連合するようになった折には消す + visibility.value = 'public'; + localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す return; } os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { - currentVisibility: visibility, + currentVisibility: visibility.value, isSilenced: $i?.isSilenced, - localOnly: localOnly, - src: visibilityButton, + localOnly: localOnly.value, + src: visibilityButton.value, }, { changeVisibility: v => { - visibility = v; + visibility.value = v; if (defaultStore.state.rememberNoteVisibility) { - defaultStore.set('visibility', visibility); + defaultStore.set('visibility', visibility.value); } }, }, 'closed'); @@ -473,14 +477,14 @@ function setVisibility() { async function toggleLocalOnly() { if (props.channel) { - visibility = 'public'; - localOnly = true; // TODO: チャンネルが連合するようになった折には消す + visibility.value = 'public'; + localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す return; } const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo'); - if (!localOnly && neverShowInfo !== 'true') { + if (!localOnly.value && neverShowInfo !== 'true') { const confirm = await os.actions({ type: 'question', title: i18n.ts.disableFederationConfirm, @@ -510,7 +514,7 @@ async function toggleLocalOnly() { } } - localOnly = !localOnly; + localOnly.value = !localOnly.value; } async function toggleReactionAcceptance() { @@ -523,15 +527,15 @@ async function toggleReactionAcceptance() { { value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }, { value: 'likeOnly' as const, text: i18n.ts.likeOnly }, ], - default: reactionAcceptance, + default: reactionAcceptance.value, }); if (select.canceled) return; - reactionAcceptance = select.result; + reactionAcceptance.value = select.result; } function pushVisibleUser(user) { - if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) { - visibleUsers.push(user); + if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) { + visibleUsers.value.push(user); } } @@ -539,34 +543,34 @@ function addVisibleUser() { os.selectUser().then(user => { pushVisibleUser(user); - if (!text.toLowerCase().includes(`@${user.username.toLowerCase()}`)) { - text = `@${Misskey.acct.toString(user)} ${text}`; + if (!text.value.toLowerCase().includes(`@${user.username.toLowerCase()}`)) { + text.value = `@${Misskey.acct.toString(user)} ${text.value}`; } }); } function removeVisibleUser(user) { - visibleUsers = erase(user, visibleUsers); + visibleUsers.value = erase(user, visibleUsers.value); } function clear() { - text = ''; - files = []; - poll = null; - quoteId = null; + text.value = ''; + files.value = []; + poll.value = null; + quoteId.value = null; } function onKeydown(ev: KeyboardEvent) { - if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost) post(); + if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post(); if (ev.key === 'Escape') emit('esc'); } function onCompositionUpdate(ev: CompositionEvent) { - imeText = ev.data; + imeText.value = ev.data; } function onCompositionEnd(ev: CompositionEvent) { - imeText = ''; + imeText.value = ''; } async function onPaste(ev: ClipboardEvent) { @@ -584,7 +588,7 @@ async function onPaste(ev: ClipboardEvent) { const paste = ev.clipboardData.getData('text'); - if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) { + if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) { ev.preventDefault(); os.confirm({ @@ -592,11 +596,11 @@ async function onPaste(ev: ClipboardEvent) { text: i18n.ts.quoteQuestion, }).then(({ canceled }) => { if (canceled) { - insertTextAtCursor(textareaEl, paste); + insertTextAtCursor(textareaEl.value, paste); return; } - quoteId = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; + quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; }); } } @@ -607,7 +611,7 @@ function onDragover(ev) { const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; if (isFile || isDriveFile) { ev.preventDefault(); - draghover = true; + draghover.value = true; switch (ev.dataTransfer.effectAllowed) { case 'all': case 'uninitialized': @@ -628,15 +632,15 @@ function onDragover(ev) { } function onDragenter(ev) { - draghover = true; + draghover.value = true; } function onDragleave(ev) { - draghover = false; + draghover.value = false; } function onDrop(ev): void { - draghover = false; + draghover.value = false; // ファイルだったら if (ev.dataTransfer.files.length > 0) { @@ -649,7 +653,7 @@ function onDrop(ev): void { const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); - files.push(file); + files.value.push(file); ev.preventDefault(); } //#endregion @@ -660,16 +664,16 @@ function saveDraft() { const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); - draftData[draftKey] = { + draftData[draftKey.value] = { updatedAt: new Date(), data: { - text: text, - useCw: useCw, - cw: cw, - visibility: visibility, - localOnly: localOnly, - files: files, - poll: poll, + text: text.value, + useCw: useCw.value, + cw: cw.value, + visibility: visibility.value, + localOnly: localOnly.value, + files: files.value, + poll: poll.value, }, }; @@ -679,13 +683,13 @@ function saveDraft() { function deleteDraft() { const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); - delete draftData[draftKey]; + delete draftData[draftKey.value]; miLocalStorage.setItem('drafts', JSON.stringify(draftData)); } async function post(ev?: MouseEvent) { - if (useCw && (cw == null || cw.trim() === '')) { + if (useCw.value && (cw.value == null || cw.value.trim() === '')) { os.alert({ type: 'error', text: i18n.ts.cwNotationRequired, @@ -704,13 +708,13 @@ async function post(ev?: MouseEvent) { if (props.mock) return; const annoying = - text.includes('$[x2') || - text.includes('$[x3') || - text.includes('$[x4') || - text.includes('$[scale') || - text.includes('$[position'); + text.value.includes('$[x2') || + text.value.includes('$[x3') || + text.value.includes('$[x4') || + text.value.includes('$[scale') || + text.value.includes('$[position'); - if (annoying && visibility === 'public') { + if (annoying && visibility.value === 'public') { const { canceled, result } = await os.actions({ type: 'warning', text: i18n.ts.thisPostMayBeAnnoying, @@ -730,27 +734,27 @@ async function post(ev?: MouseEvent) { if (canceled) return; if (result === 'cancel') return; if (result === 'home') { - visibility = 'home'; + visibility.value = 'home'; } } let postData = { - text: text === '' ? null : text, - fileIds: files.length > 0 ? files.map(f => f.id) : undefined, + text: text.value === '' ? null : text.value, + fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined, replyId: props.reply ? props.reply.id : undefined, - renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined, + renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined, channelId: props.channel ? props.channel.id : undefined, - poll: poll, - cw: useCw ? cw ?? '' : null, - localOnly: localOnly, - visibility: visibility, - visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined, - reactionAcceptance, + poll: poll.value, + cw: useCw.value ? cw.value ?? '' : null, + localOnly: localOnly.value, + visibility: visibility.value, + visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined, + reactionAcceptance: reactionAcceptance.value, editId: props.editId ? props.editId : undefined, }; - if (withHashtags && hashtags && hashtags.trim() !== '') { - const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); + if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') { + const hashtags_ = hashtags.value.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_; } @@ -767,15 +771,15 @@ async function post(ev?: MouseEvent) { let token = undefined; - if (postAccount) { + if (postAccount.value) { const storedAccounts = await getAccounts(); - token = storedAccounts.find(x => x.id === postAccount.id)?.token; + token = storedAccounts.find(x => x.id === postAccount.value.id)?.token; } - posting = true; - os.api(postData.editId ? "notes/edit" : "notes/create", postData, token).then(() => { + posting.value = true; + os.api(postData.editId ? 'notes/edit' : 'notes/create', postData, token).then(() => { if (props.freezeAfterPosted) { - posted = true; + posted.value = true; } else { clear(); } @@ -787,8 +791,8 @@ async function post(ev?: MouseEvent) { const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[]; miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history)))); } - posting = false; - postAccount = null; + posting.value = false; + postAccount.value = null; incNotesCount(); if (notesCount === 1) { @@ -833,7 +837,7 @@ async function post(ev?: MouseEvent) { } }); }).catch(err => { - posting = false; + posting.value = false; os.alert({ type: 'error', text: err.message + '\n' + (err as any).id, @@ -847,12 +851,23 @@ function cancel() { function insertMention() { os.selectUser().then(user => { - insertTextAtCursor(textareaEl, '@' + Misskey.acct.toString(user) + ' '); + insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' '); }); } async function insertEmoji(ev: MouseEvent) { - os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl); + textAreaReadOnly.value = true; + + emojiPicker.show( + ev.currentTarget ?? ev.target, + emoji => { + insertTextAtCursor(textareaEl.value, emoji); + }, + () => { + textAreaReadOnly.value = false; + nextTick(() => focus()); + }, + ); } function showActions(ev) { @@ -860,17 +875,17 @@ function showActions(ev) { text: action.title, action: () => { action.handler({ - text: text, - cw: cw, + text: text.value, + cw: cw.value, }, (key, value) => { - if (key === 'text') { text = value; } - if (key === 'cw') { useCw = value !== null; cw = value; } + if (key === 'text') { text.value = value; } + if (key === 'cw') { useCw.value = value !== null; cw.value = value; } }); }, })), ev.currentTarget ?? ev.target); } -let postAccount = $ref<Misskey.entities.UserDetailed | null>(null); +const postAccount = ref<Misskey.entities.UserDetailed | null>(null); function openAccountMenu(ev: MouseEvent) { if (props.mock) return; @@ -878,12 +893,12 @@ function openAccountMenu(ev: MouseEvent) { openAccountMenu_({ withExtraOperation: false, includeCurrentAccount: true, - active: postAccount != null ? postAccount.id : $i.id, + active: postAccount.value != null ? postAccount.value.id : $i.id, onChoose: (account) => { if (account.id === $i.id) { - postAccount = null; + postAccount.value = null; } else { - postAccount = account; + postAccount.value = account; } }, }, ev); @@ -899,23 +914,23 @@ onMounted(() => { } // TODO: detach when unmount - new Autocomplete(textareaEl, $$(text)); - new Autocomplete(cwInputEl, $$(cw)); - new Autocomplete(hashtagsInputEl, $$(hashtags)); + new Autocomplete(textareaEl.value, text); + new Autocomplete(cwInputEl.value, cw); + new Autocomplete(hashtagsInputEl.value, hashtags); nextTick(() => { // 書きかけの投稿を復元 if (!props.instant && !props.mention && !props.specified && !props.mock) { - const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey]; + const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey.value]; if (draft) { - text = draft.data.text; - useCw = draft.data.useCw; - cw = draft.data.cw; - visibility = draft.data.visibility; - localOnly = draft.data.localOnly; - files = (draft.data.files || []).filter(draftFile => draftFile); + text.value = draft.data.text; + useCw.value = draft.data.useCw; + cw.value = draft.data.cw; + visibility.value = draft.data.visibility; + localOnly.value = draft.data.localOnly; + files.value = (draft.data.files || []).filter(draftFile => draftFile); if (draft.data.poll) { - poll = draft.data.poll; + poll.value = draft.data.poll; } } } @@ -923,21 +938,21 @@ onMounted(() => { // 削除して編集 if (props.initialNote) { const init = props.initialNote; - text = init.text ? init.text : ''; - files = init.files; - cw = init.cw; - useCw = init.cw != null; + text.value = init.text ? init.text : ''; + files.value = init.files; + cw.value = init.cw; + useCw.value = init.cw != null; if (init.poll) { - poll = { + poll.value = { choices: init.poll.choices.map(x => x.text), multiple: init.poll.multiple, expiresAt: init.poll.expiresAt ? new Date(init.poll.expiresAt).getTime().toString() : null, expiredAfter: init.poll.expiredAfter ? new Date(init.poll.expiredAfter).getTime().toString() : null, }; } - visibility = init.visibility; - localOnly = init.localOnly; - quoteId = init.renote ? init.renote.id : null; + visibility.value = init.visibility; + localOnly.value = init.localOnly; + quoteId.value = init.renote ? init.renote.id : null; } nextTick(() => watchForDraft()); @@ -1031,6 +1046,16 @@ defineExpose({ } } +.colorBar { + position: absolute; + top: 0px; + left: 12px; + width: 5px; + height: 100% ; + border-radius: 999px; + pointer-events: none; +} + .submitInner { padding: 0 12px; line-height: 34px; @@ -1066,8 +1091,9 @@ defineExpose({ .visibility { overflow: clip; - text-overflow: ellipsis; - white-space: nowrap; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 210px; &:enabled { > .headerRightButtonText { @@ -1288,5 +1314,6 @@ defineExpose({ .headerRight { gap: 0; } + } </style> |