summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkPostForm.vue
diff options
context:
space:
mode:
authorMarie <Marie@kaifa.ch>2023-12-23 02:09:23 +0100
committerMarie <Marie@kaifa.ch>2023-12-23 02:09:23 +0100
commit5db583a3eb61d50de14d875ebf7ecef20490e313 (patch)
tree783dd43d2ac660c32e745a4485d499e9ddc43324 /packages/frontend/src/components/MkPostForm.vue
parentadd: Custom MOTDs (diff)
parentUpdate CHANGELOG.md (diff)
downloadsharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.tar.gz
sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.tar.bz2
sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.zip
merge: upstream
Diffstat (limited to 'packages/frontend/src/components/MkPostForm.vue')
-rw-r--r--packages/frontend/src/components/MkPostForm.vue407
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>