summaryrefslogtreecommitdiff
path: root/packages/client/src/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-01-16 01:46:25 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2022-01-16 01:46:25 +0900
commitc17e8fa8a4be4cc7b20e18adb37605a444823318 (patch)
treea9d4eaabd4da3fa9fc1b587b938e9f1cc1b1b391 /packages/client/src/components
parentwip: refactor(client): migrate components to composition api (diff)
downloadmisskey-c17e8fa8a4be4cc7b20e18adb37605a444823318.tar.gz
misskey-c17e8fa8a4be4cc7b20e18adb37605a444823318.tar.bz2
misskey-c17e8fa8a4be4cc7b20e18adb37605a444823318.zip
wip: refactor(client): migrate components to composition api
Diffstat (limited to 'packages/client/src/components')
-rw-r--r--packages/client/src/components/post-form.vue1048
1 files changed, 492 insertions, 556 deletions
diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue
index 24f35da2e9..7b2f79e389 100644
--- a/packages/client/src/components/post-form.vue
+++ b/packages/client/src/components/post-form.vue
@@ -9,7 +9,7 @@
<header>
<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button>
<div>
- <span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span>
+ <span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
<button ref="visibilityButton" v-tooltip="$ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
@@ -36,9 +36,9 @@
</div>
</div>
<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ $ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ $ts.add }}</button></MkInfo>
- <input v-show="useCw" ref="cw" v-model="cw" class="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
- <textarea ref="text" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
- <input v-show="withHashtags" ref="hashtags" v-model="hashtags" class="hashtags" :placeholder="$ts.hashtags" list="hashtags">
+ <input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
+ <textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
+ <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="$ts.hashtags" list="hashtags">
<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
<XNotePreview v-if="showPreview" class="preview" :text="text"/>
@@ -58,667 +58,603 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+<script lang="ts" setup>
+import { inject, watch, nextTick, onMounted } from 'vue';
+import * as mfm from 'mfm-js';
+import * as misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
import { length } from 'stringz';
import { toASCII } from 'punycode/';
import XNoteSimple from './note-simple.vue';
import XNotePreview from './note-preview.vue';
-import * as mfm from 'mfm-js';
+import XPostFormAttaches from './post-form-attaches.vue';
+import XPollEditor from './poll-editor.vue';
import { host, url } from '@/config';
import { erase, unique } from '@/scripts/array';
import { extractMentions } from '@/scripts/extract-mentions';
import * as Acct from 'misskey-js/built/acct';
import { formatTimeString } from '@/scripts/format-time-string';
import { Autocomplete } from '@/scripts/autocomplete';
-import { noteVisibilities } from 'misskey-js';
import * as os from '@/os';
import { stream } from '@/stream';
import { selectFiles } from '@/scripts/select-file';
import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
import { throttle } from 'throttle-debounce';
import MkInfo from '@/components/ui/info.vue';
+import { i18n } from '@/i18n';
+import { instance } from '@/instance';
+import { $i } from '@/account';
-export default defineComponent({
- components: {
- XNoteSimple,
- XNotePreview,
- XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')),
- XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')),
- MkInfo,
- },
+const modal = inject('modal');
- inject: ['modal'],
+const props = withDefaults(defineProps<{
+ reply?: misskey.entities.Note;
+ renote?: misskey.entities.Note;
+ channel?: any; // TODO
+ mention?: misskey.entities.User;
+ specified?: misskey.entities.User;
+ initialText?: string;
+ initialVisibility?: typeof misskey.noteVisibilities;
+ initialFiles?: misskey.entities.DriveFile[];
+ initialLocalOnly?: boolean;
+ initialVisibleUsers?: misskey.entities.User[];
+ initialNote?: misskey.entities.Note;
+ share?: boolean;
+ fixed?: boolean;
+ autofocus?: boolean;
+}>(), {
+ initialVisibleUsers: [],
+ autofocus: true,
+});
- props: {
- reply: {
- type: Object,
- required: false
- },
- renote: {
- type: Object,
- required: false
- },
- channel: {
- type: Object,
- required: false
- },
- mention: {
- type: Object,
- required: false
- },
- specified: {
- type: Object,
- required: false
- },
- initialText: {
- type: String,
- required: false
- },
- initialVisibility: {
- type: String,
- required: false
- },
- initialFiles: {
- type: Array,
- required: false
- },
- initialLocalOnly: {
- type: Boolean,
- required: false
- },
- initialVisibleUsers: {
- type: Array,
- required: false,
- default: () => []
- },
- initialNote: {
- type: Object,
- required: false
- },
- share: {
- type: Boolean,
- required: false,
- default: false
- },
- fixed: {
- type: Boolean,
- required: false,
- default: false
- },
- autofocus: {
- type: Boolean,
- required: false,
- default: true
- },
- },
+const emit = defineEmits<{
+ (e: 'posted'): void;
+ (e: 'cancel'): void;
+ (e: 'esc'): void;
+}>();
- emits: ['posted', 'cancel', 'esc'],
+const textareaEl = $ref<HTMLTextAreaElement | null>(null);
+const cwInputEl = $ref<HTMLInputElement | null>(null);
+const hashtagsInputEl = $ref<HTMLInputElement | null>(null);
+const visibilityButton = $ref<HTMLElement | null>(null);
- data() {
- return {
- posting: false,
- text: '',
- files: [],
- poll: null,
- useCw: false,
- showPreview: false,
- cw: null,
- localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
- visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number],
- visibleUsers: [],
- autocomplete: null,
- draghover: false,
- quoteId: null,
- hasNotSpecifiedMentions: false,
- recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
- imeText: '',
- typing: throttle(3000, () => {
- if (this.channel) {
- stream.send('typingOnChannel', { channel: this.channel.id });
- }
- }),
- postFormActions,
- };
- },
+let posting = $ref(false);
+let text = $ref(props.initialText ?? '');
+let files = $ref(props.initialFiles ?? []);
+let poll = $ref<{
+ choices: string[];
+ multiple: boolean;
+ expiresAt: string;
+ expiredAfter: string;
+} | null>(null);
+let useCw = $ref(false);
+let showPreview = $ref(false);
+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(props.initialVisibleUsers ?? []);
+let autocomplete = $ref(null);
+let draghover = $ref(false);
+let quoteId = $ref(null);
+let hasNotSpecifiedMentions = $ref(false);
+let recentHashtags = $ref(JSON.parse(localStorage.getItem('hashtags') || '[]'));
+let imeText = $ref('');
- computed: {
- draftKey(): string {
- let key = this.channel ? `channel:${this.channel.id}` : '';
+const typing = throttle(3000, () => {
+ if (props.channel) {
+ stream.send('typingOnChannel', { channel: props.channel.id });
+ }
+});
- if (this.renote) {
- key += `renote:${this.renote.id}`;
- } else if (this.reply) {
- key += `reply:${this.reply.id}`;
- } else {
- key += 'note';
- }
+const draftKey = $computed((): string => {
+ let key = props.channel ? `channel:${props.channel.id}` : '';
- return key;
- },
+ if (props.renote) {
+ key += `renote:${props.renote.id}`;
+ } else if (props.reply) {
+ key += `reply:${props.reply.id}`;
+ } else {
+ key += 'note';
+ }
- placeholder(): string {
- if (this.renote) {
- return this.$ts._postForm.quotePlaceholder;
- } else if (this.reply) {
- return this.$ts._postForm.replyPlaceholder;
- } else if (this.channel) {
- return this.$ts._postForm.channelPlaceholder;
- } else {
- const xs = [
- this.$ts._postForm._placeholders.a,
- this.$ts._postForm._placeholders.b,
- this.$ts._postForm._placeholders.c,
- this.$ts._postForm._placeholders.d,
- this.$ts._postForm._placeholders.e,
- this.$ts._postForm._placeholders.f
- ];
- return xs[Math.floor(Math.random() * xs.length)];
- }
- },
+ return key;
+});
- submitText(): string {
- return this.renote
- ? this.$ts.quote
- : this.reply
- ? this.$ts.reply
- : this.$ts.note;
- },
+const placeholder = $computed((): string => {
+ if (props.renote) {
+ return i18n.locale._postForm.quotePlaceholder;
+ } else if (props.reply) {
+ return i18n.locale._postForm.replyPlaceholder;
+ } else if (props.channel) {
+ return i18n.locale._postForm.channelPlaceholder;
+ } else {
+ const xs = [
+ i18n.locale._postForm._placeholders.a,
+ i18n.locale._postForm._placeholders.b,
+ i18n.locale._postForm._placeholders.c,
+ i18n.locale._postForm._placeholders.d,
+ i18n.locale._postForm._placeholders.e,
+ i18n.locale._postForm._placeholders.f
+ ];
+ return xs[Math.floor(Math.random() * xs.length)];
+ }
+});
- textLength(): number {
- return length((this.text + this.imeText).trim());
- },
+const submitText = $computed((): string => {
+ return props.renote
+ ? i18n.locale.quote
+ : props.reply
+ ? i18n.locale.reply
+ : i18n.locale.note;
+});
- canPost(): boolean {
- return !this.posting &&
- (1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) &&
- (this.textLength <= this.max) &&
- (!this.poll || this.poll.choices.length >= 2);
- },
+const textLength = $computed((): number => {
+ return length((text + imeText).trim());
+});
- max(): number {
- return this.$instance ? this.$instance.maxNoteTextLength : 1000;
- },
+const maxTextLength = $computed((): number => {
+ return instance ? instance.maxNoteTextLength : 1000;
+});
- withHashtags: defaultStore.makeGetterSetter('postFormWithHashtags'),
- hashtags: defaultStore.makeGetterSetter('postFormHashtags'),
- },
+const canPost = $computed((): boolean => {
+ return !posting &&
+ (1 <= textLength || 1 <= files.length || !!poll || !!props.renote) &&
+ (textLength <= maxTextLength) &&
+ (!poll || poll.choices.length >= 2);
+});
- watch: {
- text() {
- this.checkMissingMention();
- },
- visibleUsers: {
- handler() {
- this.checkMissingMention();
- },
- deep: true
- }
- },
+const withHashtags = $computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
+const hashtags = $computed(defaultStore.makeGetterSetter('postFormHashtags'));
- mounted() {
- if (this.initialText) {
- this.text = this.initialText;
- }
+watch($$(text), () => {
+ checkMissingMention();
+});
- if (this.initialVisibility) {
- this.visibility = this.initialVisibility;
- }
+watch($$(visibleUsers), () => {
+ checkMissingMention();
+}, {
+ deep: true,
+});
- if (this.initialFiles) {
- this.files = this.initialFiles;
- }
+if (props.mention) {
+ text = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`;
+ text += ' ';
+}
- if (typeof this.initialLocalOnly === 'boolean') {
- this.localOnly = this.initialLocalOnly;
- }
+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) : ''} `;
+}
- if (this.initialVisibleUsers) {
- this.visibleUsers = this.initialVisibleUsers;
- }
+if (props.reply && props.reply.text != null) {
+ const ast = mfm.parse(props.reply.text);
+ const otherHost = props.reply.user.host;
- if (this.mention) {
- this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
- this.text += ' ';
- }
+ for (const x of extractMentions(ast)) {
+ const mention = x.host ?
+ `@${x.username}@${toASCII(x.host)}` :
+ (otherHost == null || otherHost == host) ?
+ `@${x.username}` :
+ `@${x.username}@${toASCII(otherHost)}`;
- if (this.reply && (this.reply.user.username != this.$i.username || (this.reply.user.host != null && this.reply.user.host != host))) {
- this.text = `@${this.reply.user.username}${this.reply.user.host != null ? '@' + toASCII(this.reply.user.host) : ''} `;
- }
+ // 自分は除外
+ if ($i.username == x.username && x.host == null) continue;
+ if ($i.username == x.username && x.host == host) continue;
- if (this.reply && this.reply.text != null) {
- const ast = mfm.parse(this.reply.text);
- const otherHost = this.reply.user.host;
+ // 重複は除外
+ if (text.indexOf(`${mention} `) != -1) continue;
- for (const x of extractMentions(ast)) {
- const mention = x.host ?
- `@${x.username}@${toASCII(x.host)}` :
- (otherHost == null || otherHost == host) ?
- `@${x.username}` :
- `@${x.username}@${toASCII(otherHost)}`;
+ text += `${mention} `;
+ }
+}
- // 自分は除外
- if (this.$i.username == x.username && x.host == null) continue;
- if (this.$i.username == x.username && x.host == host) continue;
+if (props.channel) {
+ visibility = 'public';
+ localOnly = true; // TODO: チャンネルが連合するようになった折には消す
+}
- // 重複は除外
- if (this.text.indexOf(`${mention} `) != -1) continue;
+// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
+if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) {
+ visibility = props.reply.visibility;
+ if (props.reply.visibility === 'specified') {
+ os.api('users/show', {
+ userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId)
+ }).then(users => {
+ visibleUsers.push(...users);
+ });
- this.text += `${mention} `;
- }
+ if (props.reply.userId !== $i.id) {
+ os.api('users/show', { userId: props.reply.userId }).then(user => {
+ visibleUsers.push(user);
+ });
}
+ }
+}
- if (this.channel) {
- this.visibility = 'public';
- this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す
- }
+if (props.specified) {
+ visibility = 'specified';
+ visibleUsers.push(props.specified);
+}
- // 公開以外へのリプライ時は元の公開範囲を引き継ぐ
- if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
- this.visibility = this.reply.visibility;
- if (this.reply.visibility === 'specified') {
- os.api('users/show', {
- userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId)
- }).then(users => {
- this.visibleUsers.push(...users);
- });
+// keep cw when reply
+if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
+ useCw = true;
+ cw = props.reply.cw;
+}
- if (this.reply.userId !== this.$i.id) {
- os.api('users/show', { userId: this.reply.userId }).then(user => {
- this.visibleUsers.push(user);
- });
- }
- }
- }
+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());
+}
- if (this.specified) {
- this.visibility = 'specified';
- this.visibleUsers.push(this.specified);
- }
+function checkMissingMention() {
+ if (visibility === 'specified') {
+ const ast = mfm.parse(text);
- // keep cw when reply
- if (this.$store.state.keepCw && this.reply && this.reply.cw) {
- this.useCw = true;
- this.cw = this.reply.cw;
+ for (const x of extractMentions(ast)) {
+ if (!visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
+ hasNotSpecifiedMentions = true;
+ return;
+ }
}
+ hasNotSpecifiedMentions = false;
+ }
+}
- if (this.autofocus) {
- this.focus();
+function addMissingMention() {
+ const ast = mfm.parse(text);
- this.$nextTick(() => {
- this.focus();
+ for (const x of extractMentions(ast)) {
+ if (!visibleUsers.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);
});
}
+ }
+}
- // TODO: detach when unmount
- new Autocomplete(this.$refs.text, this, { model: 'text' });
- new Autocomplete(this.$refs.cw, this, { model: 'cw' });
- new Autocomplete(this.$refs.hashtags, this, { model: 'hashtags' });
+function togglePoll() {
+ if (poll) {
+ poll = null;
+ } else {
+ poll = {
+ choices: ['', ''],
+ multiple: false,
+ expiresAt: null,
+ expiredAfter: null,
+ };
+ }
+}
- this.$nextTick(() => {
- // 書きかけの投稿を復元
- if (!this.share && !this.mention && !this.specified) {
- const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
- if (draft) {
- this.text = draft.data.text;
- this.useCw = draft.data.useCw;
- this.cw = draft.data.cw;
- this.visibility = draft.data.visibility;
- this.localOnly = draft.data.localOnly;
- this.files = (draft.data.files || []).filter(e => e);
- if (draft.data.poll) {
- this.poll = draft.data.poll;
- }
- }
- }
+function addTag(tag: string) {
+ insertTextAtCursor(textareaEl, ` #${tag} `);
+}
- // 削除して編集
- if (this.initialNote) {
- const init = this.initialNote;
- this.text = init.text ? init.text : '';
- this.files = init.files;
- this.cw = init.cw;
- this.useCw = init.cw != null;
- if (init.poll) {
- this.poll = {
- choices: init.poll.choices.map(x => x.text),
- multiple: init.poll.multiple,
- expiresAt: init.poll.expiresAt,
- expiredAfter: init.poll.expiredAfter,
- };
- }
- this.visibility = init.visibility;
- this.localOnly = init.localOnly;
- this.quoteId = init.renote ? init.renote.id : null;
- }
+function focus() {
+ textareaEl.focus();
+}
- this.$nextTick(() => this.watch());
- });
- },
+function chooseFileFrom(ev) {
+ selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files => {
+ for (const file of files) {
+ files.push(file);
+ }
+ });
+}
- methods: {
- watch() {
- this.$watch('text', () => this.saveDraft());
- this.$watch('useCw', () => this.saveDraft());
- this.$watch('cw', () => this.saveDraft());
- this.$watch('poll', () => this.saveDraft());
- this.$watch('files', () => this.saveDraft(), { deep: true });
- this.$watch('visibility', () => this.saveDraft());
- this.$watch('localOnly', () => this.saveDraft());
- },
+function detachFile(id) {
+ files = files.filter(x => x.id != id);
+}
- checkMissingMention() {
- if (this.visibility === 'specified') {
- const ast = mfm.parse(this.text);
+function updateFiles(files) {
+ files = files;
+}
- for (const x of extractMentions(ast)) {
- if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
- this.hasNotSpecifiedMentions = true;
- return;
- }
- }
- this.hasNotSpecifiedMentions = false;
- }
- },
+function updateFileSensitive(file, sensitive) {
+ files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
+}
- addMissingMention() {
- const ast = mfm.parse(this.text);
+function updateFileName(file, name) {
+ files[files.findIndex(x => x.id === file.id)].name = name;
+}
- for (const x of extractMentions(ast)) {
- if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
- os.api('users/show', { username: x.username, host: x.host }).then(user => {
- this.visibleUsers.push(user);
- });
- }
- }
- },
+function upload(file: File, name?: string) {
+ os.upload(file, defaultStore.state.uploadFolder, name).then(res => {
+ files.push(res);
+ });
+}
- togglePoll() {
- if (this.poll) {
- this.poll = null;
- } else {
- this.poll = {
- choices: ['', ''],
- multiple: false,
- expiresAt: null,
- expiredAfter: null,
- };
+function onPollUpdate(poll) {
+ poll = poll;
+ saveDraft();
+}
+
+function setVisibility() {
+ if (props.channel) {
+ // TODO: information dialog
+ return;
+ }
+
+ os.popup(import('./visibility-picker.vue'), {
+ currentVisibility: visibility,
+ currentLocalOnly: localOnly,
+ src: visibilityButton,
+ }, {
+ changeVisibility: visibility => {
+ visibility = visibility;
+ if (defaultStore.state.rememberNoteVisibility) {
+ defaultStore.set('visibility', visibility);
}
},
+ changeLocalOnly: localOnly => {
+ localOnly = localOnly;
+ if (defaultStore.state.rememberNoteVisibility) {
+ defaultStore.set('localOnly', localOnly);
+ }
+ }
+ }, 'closed');
+}
- addTag(tag: string) {
- insertTextAtCursor(this.$refs.text, ` #${tag} `);
- },
+function addVisibleUser() {
+ os.selectUser().then(user => {
+ visibleUsers.push(user);
+ });
+}
- focus() {
- (this.$refs.text as any).focus();
- },
+function removeVisibleUser(user) {
+ visibleUsers = erase(user, visibleUsers);
+}
- chooseFileFrom(ev) {
- selectFiles(ev.currentTarget || ev.target, this.$ts.attachFile).then(files => {
- for (const file of files) {
- this.files.push(file);
- }
- });
- },
+function clear() {
+ text = '';
+ files = [];
+ poll = null;
+ quoteId = null;
+}
- detachFile(id) {
- this.files = this.files.filter(x => x.id != id);
- },
+function onKeydown(e: KeyboardEvent) {
+ if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && canPost) post();
+ if (e.which === 27) emit('esc');
+ typing();
+}
- updateFiles(files) {
- this.files = files;
- },
+function onCompositionUpdate(e: CompositionEvent) {
+ imeText = e.data;
+ typing();
+}
- updateFileSensitive(file, sensitive) {
- this.files[this.files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
- },
+function onCompositionEnd(e: CompositionEvent) {
+ imeText = '';
+}
- updateFileName(file, name) {
- this.files[this.files.findIndex(x => x.id === file.id)].name = name;
- },
+async function onPaste(e: ClipboardEvent) {
+ for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
+ if (item.kind == 'file') {
+ const file = item.getAsFile();
+ const lio = file.name.lastIndexOf('.');
+ const ext = lio >= 0 ? file.name.slice(lio) : '';
+ const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
+ upload(file, formatted);
+ }
+ }
- upload(file: File, name?: string) {
- os.upload(file, this.$store.state.uploadFolder, name).then(res => {
- this.files.push(res);
- });
- },
+ const paste = e.clipboardData.getData('text');
- onPollUpdate(poll) {
- this.poll = poll;
- this.saveDraft();
- },
+ if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) {
+ e.preventDefault();
- setVisibility() {
- if (this.channel) {
- // TODO: information dialog
+ os.confirm({
+ type: 'info',
+ text: i18n.locale.quoteQuestion,
+ }).then(({ canceled }) => {
+ if (canceled) {
+ insertTextAtCursor(textareaEl, paste);
return;
}
- os.popup(import('./visibility-picker.vue'), {
- currentVisibility: this.visibility,
- currentLocalOnly: this.localOnly,
- src: this.$refs.visibilityButton
- }, {
- changeVisibility: visibility => {
- this.visibility = visibility;
- if (this.$store.state.rememberNoteVisibility) {
- this.$store.set('visibility', visibility);
- }
- },
- changeLocalOnly: localOnly => {
- this.localOnly = localOnly;
- if (this.$store.state.rememberNoteVisibility) {
- this.$store.set('localOnly', localOnly);
- }
- }
- }, 'closed');
- },
-
- addVisibleUser() {
- os.selectUser().then(user => {
- this.visibleUsers.push(user);
- });
- },
+ quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
+ });
+ }
+}
- removeVisibleUser(user) {
- this.visibleUsers = erase(user, this.visibleUsers);
- },
+function onDragover(e) {
+ if (!e.dataTransfer.items[0]) return;
+ const isFile = e.dataTransfer.items[0].kind == 'file';
+ const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+ if (isFile || isDriveFile) {
+ e.preventDefault();
+ draghover = true;
+ e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+ }
+}
- clear() {
- this.text = '';
- this.files = [];
- this.poll = null;
- this.quoteId = null;
- },
+function onDragenter(e) {
+ draghover = true;
+}
- onKeydown(e: KeyboardEvent) {
- if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
- if (e.which === 27) this.$emit('esc');
- this.typing();
- },
+function onDragleave(e) {
+ draghover = false;
+}
- onCompositionUpdate(e: CompositionEvent) {
- this.imeText = e.data;
- this.typing();
- },
+function onDrop(e): void {
+ draghover = false;
- onCompositionEnd(e: CompositionEvent) {
- this.imeText = '';
- },
+ // ファイルだったら
+ if (e.dataTransfer.files.length > 0) {
+ e.preventDefault();
+ for (const x of Array.from(e.dataTransfer.files)) upload(x);
+ return;
+ }
- async onPaste(e: ClipboardEvent) {
- for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
- if (item.kind == 'file') {
- const file = item.getAsFile();
- const lio = file.name.lastIndexOf('.');
- const ext = lio >= 0 ? file.name.slice(lio) : '';
- const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
- this.upload(file, formatted);
- }
- }
+ //#region ドライブのファイル
+ const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+ if (driveFile != null && driveFile != '') {
+ const file = JSON.parse(driveFile);
+ files.push(file);
+ e.preventDefault();
+ }
+ //#endregion
+}
- const paste = e.clipboardData.getData('text');
+function saveDraft() {
+ const data = JSON.parse(localStorage.getItem('drafts') || '{}');
- if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
- e.preventDefault();
+ data[draftKey] = {
+ updatedAt: new Date(),
+ data: {
+ text: text,
+ useCw: useCw,
+ cw: cw,
+ visibility: visibility,
+ localOnly: localOnly,
+ files: files,
+ poll: poll
+ }
+ };
- os.confirm({
- type: 'info',
- text: this.$ts.quoteQuestion,
- }).then(({ canceled }) => {
- if (canceled) {
- insertTextAtCursor(this.$refs.text, paste);
- return;
- }
+ localStorage.setItem('drafts', JSON.stringify(data));
+}
- this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
- });
- }
- },
+function deleteDraft() {
+ const data = JSON.parse(localStorage.getItem('drafts') || '{}');
- onDragover(e) {
- if (!e.dataTransfer.items[0]) return;
- const isFile = e.dataTransfer.items[0].kind == 'file';
- const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
- if (isFile || isDriveFile) {
- e.preventDefault();
- this.draghover = true;
- e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
- }
- },
+ delete data[draftKey];
- onDragenter(e) {
- this.draghover = true;
- },
+ localStorage.setItem('drafts', JSON.stringify(data));
+}
- onDragleave(e) {
- this.draghover = false;
- },
+async function post() {
+ let data = {
+ text: text == '' ? undefined : text,
+ fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
+ replyId: props.reply ? props.reply.id : undefined,
+ renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
+ channelId: props.channel ? props.channel.id : undefined,
+ poll: poll,
+ cw: useCw ? cw || '' : undefined,
+ localOnly: localOnly,
+ visibility: visibility,
+ visibleUserIds: visibility == 'specified' ? visibleUsers.map(u => u.id) : undefined,
+ };
- onDrop(e): void {
- this.draghover = false;
+ if (withHashtags && hashtags && hashtags.trim() !== '') {
+ const hashtags = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
+ data.text = data.text ? `${data.text} ${hashtags}` : hashtags;
+ }
- // ファイルだったら
- if (e.dataTransfer.files.length > 0) {
- e.preventDefault();
- for (const x of Array.from(e.dataTransfer.files)) this.upload(x);
- return;
- }
+ // plugin
+ if (notePostInterruptors.length > 0) {
+ for (const interruptor of notePostInterruptors) {
+ data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
+ }
+ }
- //#region ドライブのファイル
- const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
- if (driveFile != null && driveFile != '') {
- const file = JSON.parse(driveFile);
- this.files.push(file);
- e.preventDefault();
+ posting = true;
+ os.api('notes/create', data).then(() => {
+ clear();
+ nextTick(() => {
+ deleteDraft();
+ emit('posted');
+ if (data.text && data.text != '') {
+ const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
+ const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
+ localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
- //#endregion
- },
-
- saveDraft() {
- const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+ posting = false;
+ });
+ }).catch(err => {
+ posting = false;
+ os.alert({
+ type: 'error',
+ text: err.message + '\n' + (err as any).id,
+ });
+ });
+}
- data[this.draftKey] = {
- updatedAt: new Date(),
- data: {
- text: this.text,
- useCw: this.useCw,
- cw: this.cw,
- visibility: this.visibility,
- localOnly: this.localOnly,
- files: this.files,
- poll: this.poll
- }
- };
+function cancel() {
+ emit('cancel');
+}
- localStorage.setItem('drafts', JSON.stringify(data));
- },
+function insertMention() {
+ os.selectUser().then(user => {
+ insertTextAtCursor(textareaEl, '@' + Acct.toString(user) + ' ');
+ });
+}
- deleteDraft() {
- const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+async function insertEmoji(ev) {
+ os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl);
+}
- delete data[this.draftKey];
+function showActions(ev) {
+ os.popupMenu(postFormActions.map(action => ({
+ text: action.title,
+ action: () => {
+ action.handler({
+ text: text
+ }, (key, value) => {
+ if (key === 'text') { text = value; }
+ });
+ }
+ })), ev.currentTarget || ev.target);
+}
- localStorage.setItem('drafts', JSON.stringify(data));
- },
+onMounted(() => {
+ if (props.autofocus) {
+ focus();
- async post() {
- let data = {
- text: this.text == '' ? undefined : this.text,
- fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
- replyId: this.reply ? this.reply.id : undefined,
- renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
- channelId: this.channel ? this.channel.id : undefined,
- poll: this.poll,
- cw: this.useCw ? this.cw || '' : undefined,
- localOnly: this.localOnly,
- visibility: this.visibility,
- visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
- };
+ nextTick(() => {
+ focus();
+ });
+ }
- if (this.withHashtags && this.hashtags && this.hashtags.trim() !== '') {
- const hashtags = this.hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
- data.text = data.text ? `${data.text} ${hashtags}` : hashtags;
- }
+ // TODO: detach when unmount
+ new Autocomplete(textareaEl, $$(text));
+ new Autocomplete(cwInputEl, $$(cw));
+ new Autocomplete(hashtagsInputEl, $$(hashtags));
- // plugin
- if (notePostInterruptors.length > 0) {
- for (const interruptor of notePostInterruptors) {
- data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
+ nextTick(() => {
+ // 書きかけの投稿を復元
+ if (!props.share && !props.mention && !props.specified) {
+ const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey];
+ 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(e => e);
+ if (draft.data.poll) {
+ poll = draft.data.poll;
}
}
+ }
- this.posting = true;
- os.api('notes/create', data).then(() => {
- this.clear();
- this.$nextTick(() => {
- this.deleteDraft();
- this.$emit('posted');
- if (data.text && data.text != '') {
- const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
- const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
- localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
- }
- this.posting = false;
- });
- }).catch(err => {
- this.posting = false;
- os.alert({
- type: 'error',
- text: err.message + '\n' + (err as any).id,
- });
- });
- },
-
- cancel() {
- this.$emit('cancel');
- },
-
- insertMention() {
- os.selectUser().then(user => {
- insertTextAtCursor(this.$refs.text, '@' + Acct.toString(user) + ' ');
- });
- },
-
- async insertEmoji(ev) {
- os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
- },
-
- showActions(ev) {
- os.popupMenu(postFormActions.map(action => ({
- text: action.title,
- action: () => {
- action.handler({
- text: this.text
- }, (key, value) => {
- if (key === 'text') { this.text = value; }
- });
- }
- })), ev.currentTarget || ev.target);
+ // 削除して編集
+ if (props.initialNote) {
+ const init = props.initialNote;
+ text = init.text ? init.text : '';
+ files = init.files;
+ cw = init.cw;
+ useCw = init.cw != null;
+ if (init.poll) {
+ poll = {
+ choices: init.poll.choices.map(x => x.text),
+ multiple: init.poll.multiple,
+ expiresAt: init.poll.expiresAt,
+ expiredAfter: init.poll.expiredAfter,
+ };
+ }
+ visibility = init.visibility;
+ localOnly = init.localOnly;
+ quoteId = init.renote ? init.renote.id : null;
}
- }
+
+ nextTick(() => watchForDraft());
+ });
});
</script>