819 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			819 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
| <div class="gafaadew" :class="{ modal, _popup: modal }"
 | |
| 	v-size="{ max: [500] }"
 | |
| 	@dragover.stop="onDragover"
 | |
| 	@dragenter="onDragenter"
 | |
| 	@dragleave="onDragleave"
 | |
| 	@drop.stop="onDrop"
 | |
| >
 | |
| 	<header>
 | |
| 		<button v-if="!fixed" class="cancel _button" @click="cancel"><Fa :icon="faTimes"/></button>
 | |
| 		<div>
 | |
| 			<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span>
 | |
| 			<span class="local-only" v-if="localOnly"><Fa :icon="faBiohazard"/></span>
 | |
| 			<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$t('visibility')" :disabled="channel != null">
 | |
| 				<span v-if="visibility === 'public'"><Fa :icon="faGlobe"/></span>
 | |
| 				<span v-if="visibility === 'home'"><Fa :icon="faHome"/></span>
 | |
| 				<span v-if="visibility === 'followers'"><Fa :icon="faUnlock"/></span>
 | |
| 				<span v-if="visibility === 'specified'"><Fa :icon="faEnvelope"/></span>
 | |
| 			</button>
 | |
| 			<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<Fa :icon="reply ? faReply : renote ? faQuoteRight : faPaperPlane"/></button>
 | |
| 		</div>
 | |
| 	</header>
 | |
| 	<div class="form" :class="{ fixed }">
 | |
| 		<XNotePreview class="preview" v-if="reply" :note="reply"/>
 | |
| 		<XNotePreview class="preview" v-if="renote" :note="renote"/>
 | |
| 		<div class="with-quote" v-if="quoteId"><Fa icon="quote-left"/> {{ $t('quoteAttached') }}<button @click="quoteId = null"><Fa icon="times"/></button></div>
 | |
| 		<div v-if="visibility === 'specified'" class="to-specified">
 | |
| 			<span style="margin-right: 8px;">{{ $t('recipient') }}</span>
 | |
| 			<div class="visibleUsers">
 | |
| 				<span v-for="u in visibleUsers" :key="u.id">
 | |
| 					<MkAcct :user="u"/>
 | |
| 					<button class="_button" @click="removeVisibleUser(u)"><Fa :icon="faTimes"/></button>
 | |
| 				</span>
 | |
| 				<button @click="addVisibleUser" class="_buttonPrimary"><Fa :icon="faPlus" fixed-width/></button>
 | |
| 			</div>
 | |
| 		</div>
 | |
| 		<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$t('annotation')" @keydown="onKeydown">
 | |
| 		<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste"></textarea>
 | |
| 		<XPostFormAttaches class="attaches" :files="files" @updated="updateMedia" @detach="detachMedia"/>
 | |
| 		<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
 | |
| 		<footer>
 | |
| 			<button class="_button" @click="chooseFileFrom" v-tooltip="$t('attachFile')"><Fa :icon="faPhotoVideo"/></button>
 | |
| 			<button class="_button" @click="togglePoll" :class="{ active: poll }" v-tooltip="$t('poll')"><Fa :icon="faPollH"/></button>
 | |
| 			<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$t('useCw')"><Fa :icon="faEyeSlash"/></button>
 | |
| 			<button class="_button" @click="insertMention" v-tooltip="$t('mention')"><Fa :icon="faAt"/></button>
 | |
| 			<button class="_button" @click="insertEmoji" v-tooltip="$t('emoji')"><Fa :icon="faLaughSquint"/></button>
 | |
| 			<button class="_button" @click="showActions" v-tooltip="$t('plugin')" v-if="postFormActions.length > 0"><Fa :icon="faPlug"/></button>
 | |
| 		</footer>
 | |
| 	</div>
 | |
| </div>
 | |
| </template>
 | |
| 
 | |
| <script lang="ts">
 | |
| import { defineComponent, defineAsyncComponent } from 'vue';
 | |
| import { faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
 | |
| import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons';
 | |
| import insertTextAtCursor from 'insert-text-at-cursor';
 | |
| import { length } from 'stringz';
 | |
| import { toASCII } from 'punycode';
 | |
| import XNotePreview from './note-preview.vue';
 | |
| import { parse } from '../../mfm/parse';
 | |
| import { host, url } from '@/config';
 | |
| import { erase, unique } from '../../prelude/array';
 | |
| import extractMentions from '../../misc/extract-mentions';
 | |
| import getAcct from '../../misc/acct/render';
 | |
| import { formatTimeString } from '../../misc/format-time-string';
 | |
| import { Autocomplete } from '@/scripts/autocomplete';
 | |
| import { noteVisibilities } from '../../types';
 | |
| import * as os from '@/os';
 | |
| import { selectFile } from '@/scripts/select-file';
 | |
| import { notePostInterruptors, postFormActions } from '@/store';
 | |
| 
 | |
| export default defineComponent({
 | |
| 	components: {
 | |
| 		XNotePreview,
 | |
| 		XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')),
 | |
| 		XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue'))
 | |
| 	},
 | |
| 
 | |
| 	inject: ['modal'],
 | |
| 
 | |
| 	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
 | |
| 		},
 | |
| 		initialNote: {
 | |
| 			type: Object,
 | |
| 			required: false
 | |
| 		},
 | |
| 		instant: {
 | |
| 			type: Boolean,
 | |
| 			required: false,
 | |
| 			default: false
 | |
| 		},
 | |
| 		fixed: {
 | |
| 			type: Boolean,
 | |
| 			required: false,
 | |
| 			default: false
 | |
| 		},
 | |
| 		autofocus: {
 | |
| 			type: Boolean,
 | |
| 			required: false,
 | |
| 			default: true
 | |
| 		},
 | |
| 	},
 | |
| 
 | |
| 	emits: ['posted', 'cancel', 'esc'],
 | |
| 
 | |
| 	data() {
 | |
| 		return {
 | |
| 			posting: false,
 | |
| 			text: '',
 | |
| 			files: [],
 | |
| 			poll: null,
 | |
| 			useCw: false,
 | |
| 			cw: null,
 | |
| 			localOnly: this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.localOnly : this.$store.state.settings.defaultNoteLocalOnly,
 | |
| 			visibility: this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.visibility : this.$store.state.settings.defaultNoteVisibility,
 | |
| 			visibleUsers: [],
 | |
| 			autocomplete: null,
 | |
| 			draghover: false,
 | |
| 			quoteId: null,
 | |
| 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
 | |
| 			postFormActions,
 | |
| 			faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug
 | |
| 		};
 | |
| 	},
 | |
| 
 | |
| 	computed: {
 | |
| 		draftKey(): string {
 | |
| 			let key = this.channel ? `channel:${this.channel.id}` : '';
 | |
| 
 | |
| 			if (this.renote) {
 | |
| 				key += `renote:${this.renote.id}`;
 | |
| 			} else if (this.reply) {
 | |
| 				key += `reply:${this.reply.id}`;
 | |
| 			} else {
 | |
| 				key += 'note';
 | |
| 			}
 | |
| 
 | |
| 			return key;
 | |
| 		},
 | |
| 
 | |
| 		placeholder(): string {
 | |
| 			if (this.renote) {
 | |
| 				return this.$t('_postForm.quotePlaceholder');
 | |
| 			} else if (this.reply) {
 | |
| 				return this.$t('_postForm.replyPlaceholder');
 | |
| 			} else if (this.channel) {
 | |
| 				return this.$t('_postForm.channelPlaceholder');
 | |
| 			} else {
 | |
| 				const xs = [
 | |
| 					this.$t('_postForm._placeholders.a'),
 | |
| 					this.$t('_postForm._placeholders.b'),
 | |
| 					this.$t('_postForm._placeholders.c'),
 | |
| 					this.$t('_postForm._placeholders.d'),
 | |
| 					this.$t('_postForm._placeholders.e'),
 | |
| 					this.$t('_postForm._placeholders.f')
 | |
| 				];
 | |
| 				return xs[Math.floor(Math.random() * xs.length)];
 | |
| 			}
 | |
| 		},
 | |
| 
 | |
| 		submitText(): string {
 | |
| 			return this.renote
 | |
| 				? this.$t('quote')
 | |
| 				: this.reply
 | |
| 					? this.$t('reply')
 | |
| 					: this.$t('note');
 | |
| 		},
 | |
| 
 | |
| 		canPost(): boolean {
 | |
| 			return !this.posting &&
 | |
| 				(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
 | |
| 				(length(this.text.trim()) <= this.max) &&
 | |
| 				(!this.poll || this.poll.choices.length >= 2);
 | |
| 		},
 | |
| 
 | |
| 		max(): number {
 | |
| 			return this.$store.state.instance.meta ? this.$store.state.instance.meta.maxNoteTextLength : 1000;
 | |
| 		}
 | |
| 	},
 | |
| 
 | |
| 	mounted() {
 | |
| 		if (this.initialText) {
 | |
| 			this.text = this.initialText;
 | |
| 		}
 | |
| 
 | |
| 		if (this.mention) {
 | |
| 			this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
 | |
| 			this.text += ' ';
 | |
| 		}
 | |
| 
 | |
| 		if (this.reply && this.reply.user.host != null) {
 | |
| 			this.text = `@${this.reply.user.username}@${toASCII(this.reply.user.host)} `;
 | |
| 		}
 | |
| 
 | |
| 		if (this.reply && this.reply.text != null) {
 | |
| 			const ast = parse(this.reply.text);
 | |
| 
 | |
| 			for (const x of extractMentions(ast)) {
 | |
| 				const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
 | |
| 
 | |
| 				// 自分は除外
 | |
| 				if (this.$store.state.i.username == x.username && x.host == null) continue;
 | |
| 				if (this.$store.state.i.username == x.username && x.host == host) continue;
 | |
| 
 | |
| 				// 重複は除外
 | |
| 				if (this.text.indexOf(`${mention} `) != -1) continue;
 | |
| 
 | |
| 				this.text += `${mention} `;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if (this.channel) {
 | |
| 			this.visibility = 'public';
 | |
| 			this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す
 | |
| 		}
 | |
| 
 | |
| 		// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
 | |
| 		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.$store.state.i.id && uid !== this.reply.userId)
 | |
| 				}).then(users => {
 | |
| 					this.visibleUsers.push(...users);
 | |
| 				});
 | |
| 
 | |
| 				if (this.reply.userId !== this.$store.state.i.id) {
 | |
| 					os.api('users/show', { userId: this.reply.userId }).then(user => {
 | |
| 						this.visibleUsers.push(user);
 | |
| 					});
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if (this.specified) {
 | |
| 			this.visibility = 'specified';
 | |
| 			this.visibleUsers.push(this.specified);
 | |
| 		}
 | |
| 
 | |
| 		// keep cw when reply
 | |
| 		if (this.$store.state.settings.keepCw && this.reply && this.reply.cw) {
 | |
| 			this.useCw = true;
 | |
| 			this.cw = this.reply.cw;
 | |
| 		}
 | |
| 
 | |
| 		if (this.autofocus) {
 | |
| 			this.focus();
 | |
| 
 | |
| 			this.$nextTick(() => {
 | |
| 				this.focus();
 | |
| 			});
 | |
| 		}
 | |
| 
 | |
| 		// TODO: detach when unmount
 | |
| 		new Autocomplete(this.$refs.text, this, { model: 'text' });
 | |
| 		new Autocomplete(this.$refs.cw, this, { model: 'cw' });
 | |
| 
 | |
| 		this.$nextTick(() => {
 | |
| 			// 書きかけの投稿を復元
 | |
| 			if (!this.instant && !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;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// 削除して編集
 | |
| 			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 = init.poll;
 | |
| 				}
 | |
| 				this.visibility = init.visibility;
 | |
| 				this.localOnly = init.localOnly;
 | |
| 				this.quoteId = init.renote ? init.renote.id : null;
 | |
| 			}
 | |
| 
 | |
| 			this.$nextTick(() => this.watch());
 | |
| 		});
 | |
| 	},
 | |
| 
 | |
| 	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());
 | |
| 		},
 | |
| 
 | |
| 		togglePoll() {
 | |
| 			if (this.poll) {
 | |
| 				this.poll = null;
 | |
| 			} else {
 | |
| 				this.poll = {
 | |
| 					choices: ['', ''],
 | |
| 					multiple: false,
 | |
| 					expiresAt: null,
 | |
| 					expiredAfter: null,
 | |
| 				};
 | |
| 			}
 | |
| 		},
 | |
| 
 | |
| 		trimmedLength(text: string) {
 | |
| 			return length(text.trim());
 | |
| 		},
 | |
| 
 | |
| 		addTag(tag: string) {
 | |
| 			insertTextAtCursor(this.$refs.text, ` #${tag} `);
 | |
| 		},
 | |
| 
 | |
| 		focus() {
 | |
| 			(this.$refs.text as any).focus();
 | |
| 		},
 | |
| 
 | |
| 		chooseFileFrom(ev) {
 | |
| 			selectFile(ev.currentTarget || ev.target, this.$t('attachFile'), true).then(files => {
 | |
| 				for (const file of files) {
 | |
| 					this.files.push(file);
 | |
| 				}
 | |
| 			});
 | |
| 		},
 | |
| 
 | |
| 		detachMedia(id) {
 | |
| 			this.files = this.files.filter(x => x.id != id);
 | |
| 		},
 | |
| 
 | |
| 		updateMedia(file) {
 | |
| 			this.files[this.files.findIndex(x => x.id === file.id)] = file;
 | |
| 		},
 | |
| 
 | |
| 		upload(file: File, name?: string) {
 | |
| 			os.upload(file, this.$store.state.settings.uploadFolder, name).then(res => {
 | |
| 				this.files.push(res);
 | |
| 			});
 | |
| 		},
 | |
| 
 | |
| 		onPollUpdate(poll) {
 | |
| 			this.poll = poll;
 | |
| 			this.saveDraft();
 | |
| 		},
 | |
| 
 | |
| 		async setVisibility() {
 | |
| 			if (this.channel) {
 | |
| 				// TODO: information dialog
 | |
| 				return;
 | |
| 			}
 | |
| 
 | |
| 			os.popup(await import('./visibility-picker.vue'), {
 | |
| 				currentVisibility: this.visibility,
 | |
| 				currentLocalOnly: this.localOnly,
 | |
| 				src: this.$refs.visibilityButton
 | |
| 			}, {
 | |
| 				changeVisibility: visibility => {
 | |
| 					this.visibility = visibility;
 | |
| 					if (this.$store.state.settings.rememberNoteVisibility) {
 | |
| 						this.$store.commit('deviceUser/setVisibility', visibility);
 | |
| 					}
 | |
| 				},
 | |
| 				changeLocalOnly: localOnly => {
 | |
| 					this.localOnly = localOnly;
 | |
| 					if (this.$store.state.settings.rememberNoteVisibility) {
 | |
| 						this.$store.commit('deviceUser/setLocalOnly', localOnly);
 | |
| 					}
 | |
| 				}
 | |
| 			}, 'closed');
 | |
| 		},
 | |
| 
 | |
| 		addVisibleUser() {
 | |
| 			os.selectUser().then(user => {
 | |
| 				this.visibleUsers.push(user);
 | |
| 			});
 | |
| 		},
 | |
| 
 | |
| 		removeVisibleUser(user) {
 | |
| 			this.visibleUsers = erase(user, this.visibleUsers);
 | |
| 		},
 | |
| 
 | |
| 		clear() {
 | |
| 			this.text = '';
 | |
| 			this.files = [];
 | |
| 			this.poll = null;
 | |
| 			this.quoteId = null;
 | |
| 		},
 | |
| 
 | |
| 		onKeydown(e) {
 | |
| 			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
 | |
| 			if (e.which === 27) this.$emit('esc');
 | |
| 		},
 | |
| 
 | |
| 		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.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
 | |
| 					this.upload(file, formatted);
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			const paste = e.clipboardData.getData('text');
 | |
| 
 | |
| 			if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
 | |
| 				e.preventDefault();
 | |
| 
 | |
| 				os.dialog({
 | |
| 					type: 'info',
 | |
| 					text: this.$t('quoteQuestion'),
 | |
| 					showCancelButton: true
 | |
| 				}).then(({ canceled }) => {
 | |
| 					if (canceled) {
 | |
| 						insertTextAtCursor(this.$refs.text, paste);
 | |
| 						return;
 | |
| 					}
 | |
| 
 | |
| 					this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
 | |
| 				});
 | |
| 			}
 | |
| 		},
 | |
| 
 | |
| 		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';
 | |
| 			}
 | |
| 		},
 | |
| 
 | |
| 		onDragenter(e) {
 | |
| 			this.draghover = true;
 | |
| 		},
 | |
| 
 | |
| 		onDragleave(e) {
 | |
| 			this.draghover = false;
 | |
| 		},
 | |
| 
 | |
| 		onDrop(e): void {
 | |
| 			this.draghover = false;
 | |
| 
 | |
| 			// ファイルだったら
 | |
| 			if (e.dataTransfer.files.length > 0) {
 | |
| 				e.preventDefault();
 | |
| 				for (const x of Array.from(e.dataTransfer.files)) this.upload(x);
 | |
| 				return;
 | |
| 			}
 | |
| 
 | |
| 			//#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();
 | |
| 			}
 | |
| 			//#endregion
 | |
| 		},
 | |
| 
 | |
| 		saveDraft() {
 | |
| 			if (this.instant) return;
 | |
| 
 | |
| 			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
 | |
| 
 | |
| 			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
 | |
| 				}
 | |
| 			};
 | |
| 
 | |
| 			localStorage.setItem('drafts', JSON.stringify(data));
 | |
| 		},
 | |
| 
 | |
| 		deleteDraft() {
 | |
| 			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
 | |
| 
 | |
| 			delete data[this.draftKey];
 | |
| 
 | |
| 			localStorage.setItem('drafts', JSON.stringify(data));
 | |
| 		},
 | |
| 
 | |
| 		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,
 | |
| 				viaMobile: os.isMobile
 | |
| 			};
 | |
| 
 | |
| 			// plugin
 | |
| 			if (notePostInterruptors.length > 0) {
 | |
| 				for (const interruptor of notePostInterruptors) {
 | |
| 					data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			this.posting = true;
 | |
| 			os.api('notes/create', data).then(() => {
 | |
| 				this.clear();
 | |
| 				this.$nextTick(() => {
 | |
| 					this.deleteDraft();
 | |
| 					this.$emit('posted');
 | |
| 					if (this.text && this.text != '') {
 | |
| 						const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.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;
 | |
| 			});
 | |
| 		},
 | |
| 
 | |
| 		cancel() {
 | |
| 			this.$emit('cancel');
 | |
| 		},
 | |
| 
 | |
| 		insertMention() {
 | |
| 			os.selectUser().then(user => {
 | |
| 				insertTextAtCursor(this.$refs.text, '@' + getAcct(user) + ' ');
 | |
| 			});
 | |
| 		},
 | |
| 
 | |
| 		async insertEmoji(ev) {
 | |
| 			os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
 | |
| 				insertTextAtCursor(this.$refs.text, emoji);
 | |
| 			});
 | |
| 		},
 | |
| 
 | |
| 		showActions(ev) {
 | |
| 			os.modalMenu(postFormActions.map(action => ({
 | |
| 				text: action.title,
 | |
| 				action: () => {
 | |
| 					action.handler({
 | |
| 						text: this.text
 | |
| 					}, (key, value) => {
 | |
| 						if (key === 'text') { this.text = value; }
 | |
| 					});
 | |
| 				}
 | |
| 			})), ev.currentTarget || ev.target);
 | |
| 		}
 | |
| 	}
 | |
| });
 | |
| </script>
 | |
| 
 | |
| <style lang="scss" scoped>
 | |
| .gafaadew {
 | |
| 	position: relative;
 | |
| 
 | |
| 	&.modal {
 | |
| 		width: 100%;
 | |
| 		max-width: 520px;
 | |
| 	}
 | |
| 
 | |
| 	> header {
 | |
| 		z-index: 1000;
 | |
| 		height: 66px;
 | |
| 
 | |
| 		> .cancel {
 | |
| 			padding: 0;
 | |
| 			font-size: 20px;
 | |
| 			width: 64px;
 | |
| 			line-height: 66px;
 | |
| 		}
 | |
| 
 | |
| 		> div {
 | |
| 			position: absolute;
 | |
| 			top: 0;
 | |
| 			right: 0;
 | |
| 
 | |
| 			> .text-count {
 | |
| 				opacity: 0.7;
 | |
| 				line-height: 66px;
 | |
| 			}
 | |
| 
 | |
| 			> .visibility {
 | |
| 				height: 34px;
 | |
| 				width: 34px;
 | |
| 				margin: 0 8px;
 | |
| 
 | |
| 				& + .localOnly {
 | |
| 					margin-left: 0 !important;
 | |
| 				}
 | |
| 			}
 | |
| 			
 | |
| 			> .local-only {
 | |
| 				margin: 0 0 0 12px;
 | |
| 				opacity: 0.7;
 | |
| 			}
 | |
| 
 | |
| 			> .submit {
 | |
| 				margin: 16px 16px 16px 0;
 | |
| 				padding: 0 12px;
 | |
| 				line-height: 34px;
 | |
| 				font-weight: bold;
 | |
| 				vertical-align: bottom;
 | |
| 				border-radius: 4px;
 | |
| 
 | |
| 				&:disabled {
 | |
| 					opacity: 0.7;
 | |
| 				}
 | |
| 
 | |
| 				> [data-icon] {
 | |
| 					margin-left: 6px;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	> .form {
 | |
| 		> .preview {
 | |
| 			padding: 16px;
 | |
| 		}
 | |
| 
 | |
| 		> .with-quote {
 | |
| 			margin: 0 0 8px 0;
 | |
| 			color: var(--accent);
 | |
| 
 | |
| 			> button {
 | |
| 				padding: 4px 8px;
 | |
| 				color: var(--accentAlpha04);
 | |
| 
 | |
| 				&:hover {
 | |
| 					color: var(--accentAlpha06);
 | |
| 				}
 | |
| 
 | |
| 				&:active {
 | |
| 					color: var(--accentDarken30);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		> .to-specified {
 | |
| 			padding: 6px 24px;
 | |
| 			margin-bottom: 8px;
 | |
| 			overflow: auto;
 | |
| 			white-space: nowrap;
 | |
| 
 | |
| 			> .visibleUsers {
 | |
| 				display: inline;
 | |
| 				top: -1px;
 | |
| 				font-size: 14px;
 | |
| 
 | |
| 				> button {
 | |
| 					padding: 4px;
 | |
| 					border-radius: 8px;
 | |
| 				}
 | |
| 
 | |
| 				> span {
 | |
| 					margin-right: 14px;
 | |
| 					padding: 8px 0 8px 8px;
 | |
| 					border-radius: 8px;
 | |
| 					background: var(--X4);
 | |
| 
 | |
| 					> button {
 | |
| 						padding: 4px 8px;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		> .cw,
 | |
| 		> .text {
 | |
| 			display: block;
 | |
| 			box-sizing: border-box;
 | |
| 			padding: 0 24px;
 | |
| 			margin: 0;
 | |
| 			width: 100%;
 | |
| 			font-size: 16px;
 | |
| 			border: none;
 | |
| 			border-radius: 0;
 | |
| 			background: transparent;
 | |
| 			color: var(--fg);
 | |
| 			font-family: inherit;
 | |
| 
 | |
| 			&:focus {
 | |
| 				outline: none;
 | |
| 			}
 | |
| 
 | |
| 			&:disabled {
 | |
| 				opacity: 0.5;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		> .cw {
 | |
| 			z-index: 1;
 | |
| 			padding-bottom: 8px;
 | |
| 			border-bottom: solid 1px var(--divider);
 | |
| 		}
 | |
| 
 | |
| 		> .text {
 | |
| 			max-width: 100%;
 | |
| 			min-width: 100%;
 | |
| 			min-height: 90px;
 | |
| 
 | |
| 			&.withCw {
 | |
| 				padding-top: 8px;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		> footer {
 | |
| 			padding: 0 16px 16px 16px;
 | |
| 
 | |
| 			> button {
 | |
| 				display: inline-block;
 | |
| 				padding: 0;
 | |
| 				margin: 0;
 | |
| 				font-size: 16px;
 | |
| 				width: 48px;
 | |
| 				height: 48px;
 | |
| 				border-radius: 6px;
 | |
| 
 | |
| 				&:hover {
 | |
| 					background: var(--X5);
 | |
| 				}
 | |
| 
 | |
| 				&.active {
 | |
| 					color: var(--accent);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	&.max-width_500px {
 | |
| 		> header {
 | |
| 			height: 50px;
 | |
| 
 | |
| 			> .cancel {
 | |
| 				width: 50px;
 | |
| 				line-height: 50px;
 | |
| 			}
 | |
| 
 | |
| 			> div {
 | |
| 				> .text-count {
 | |
| 					line-height: 50px;
 | |
| 				}
 | |
| 
 | |
| 				> .submit {
 | |
| 					margin: 8px;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		> .form {
 | |
| 			> .to-specified {
 | |
| 				padding: 6px 16px;
 | |
| 			}
 | |
| 
 | |
| 			> .cw,
 | |
| 			> .text {
 | |
| 				padding: 0 16px;
 | |
| 			}
 | |
| 
 | |
| 			> .text {
 | |
| 				min-height: 80px;
 | |
| 			}
 | |
| 
 | |
| 			> footer {
 | |
| 				padding: 0 8px 8px 8px;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| </style>
 | 
