feat(frontend/draft): 下書き機能の改良・強化 (MisskeyIO#881)

This commit is contained in:
あわわわとーにゅ 2025-01-12 18:36:16 +09:00 committed by GitHub
parent 8a0b98aa26
commit 31d57f270c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 377 additions and 59 deletions

View file

@ -1317,6 +1317,8 @@ consentAll: "Allow All Items"
consentSelected: "Allow Selected Items" consentSelected: "Allow Selected Items"
emailAddressLogin: "Login with email address" emailAddressLogin: "Login with email address"
usernameLogin: "Login with username" usernameLogin: "Login with username"
autoloadDrafts: "Automatically load drafts when opening the posting form"
drafts: "Drafts"
_bubbleGame: _bubbleGame:
howToPlay: "How to play" howToPlay: "How to play"

8
locales/index.d.ts vendored
View file

@ -5330,6 +5330,14 @@ export interface Locale extends ILocale {
* *
*/ */
"usernameLogin": string; "usernameLogin": string;
/**
* 稿
*/
"autoloadDrafts": string;
/**
*
*/
"drafts": string;
"_bubbleGame": { "_bubbleGame": {
/** /**
* *

View file

@ -1326,6 +1326,8 @@ consentAll: "全て許可"
consentSelected: "選択した項目のみ許可" consentSelected: "選択した項目のみ許可"
emailAddressLogin: "メールアドレスでログイン" emailAddressLogin: "メールアドレスでログイン"
usernameLogin: "ユーザー名でログイン" usernameLogin: "ユーザー名でログイン"
autoloadDrafts: "投稿フォームを開いたときに下書きを自動で読み込む"
drafts: "下書き"
_bubbleGame: _bubbleGame:
howToPlay: "遊び方" howToPlay: "遊び方"

View file

@ -1314,6 +1314,8 @@ consentAll: "모두 허용"
consentSelected: "선택한 항목만 허용" consentSelected: "선택한 항목만 허용"
emailAddressLogin: "이메일 주소로 로그인" emailAddressLogin: "이메일 주소로 로그인"
usernameLogin: "사용자명으로 로그인" usernameLogin: "사용자명으로 로그인"
autoloadDrafts: "글 작성 시 자동으로 임시 저장된 글 불러오기"
drafts: "임시 저장"
_bubbleGame: _bubbleGame:
howToPlay: "설명" howToPlay: "설명"

View file

@ -0,0 +1,183 @@
<template>
<MkModalWindow
ref="dialog"
:height="500"
:width="800"
@click="done(true)"
@close="done(true)"
@closed="emit('closed')"
>
<template #header>
{{ i18n.ts.drafts }}
</template>
<div style="display: flex; flex-direction: column">
<div v-if="drafts.length === 0" class="empty">
<div class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div>
</div>
</div>
<div v-for="draft in drafts" :key="draft.id" :class="$style.draftItem">
<div :class="$style.draftNote" @click="selectDraft(draft.id)">
<div :class="$style.draftNoteHeader">
<div :class="$style.draftNoteDestination">
<span v-if="draft.channel" style="opacity: 0.7; padding-right: 0.5em">
<i class="ti ti-device-tv"></i> {{ draft.channel.name }}
</span>
<span v-if="draft.renote">
<i class="ti ti-quote"></i> <MkAcct :user="draft.renote.user" /> <span>{{ draft.renote.text }}</span>
</span>
<span v-else-if="draft.reply">
<i class="ti ti-arrow-back-up"></i> <MkAcct :user="draft.reply.user" /> <span>{{ draft.reply.text }}</span>
</span>
<span v-else>
<i class="ti ti-pencil"></i>
</span>
</div>
<div :class="$style.draftNoteInfo">
<MkTime :time="draft.createdAt" colored />
<span v-if="draft.visibility !== 'public'" :title="i18n.ts._visibility[draft.visibility]" style="margin-left: 0.5em">
<i v-if="draft.visibility === 'home'" class="ti ti-home"></i>
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="draft.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']" style="margin-left: 0.5em">
<i class="ti ti-rocket-off"></i>
</span>
<span v-if="draft.channel" :title="draft.channel.name" style="margin-left: 0.5em">
<i class="ti ti-device-tv"></i>
</span>
</div>
</div>
<div>
<p v-if="!!draft.cw" :class="$style.draftNoteCw">
<Mfm :text="draft.cw" />
</p>
<MkSubNoteContent :class="$style.draftNoteText" :note="draft" />
</div>
</div>
<button :class="$style.delete" class="_button" @click="removeDraft(draft.id)">
<i class="ti ti-trash"></i>
</button>
</div>
</div>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { onActivated, onMounted, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import { miLocalStorage } from '@/local-storage.js';
import { infoImageUrl } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import type { NoteDraftItem } from '@/types/note-draft-item.js';
const emit = defineEmits<{
(ev: 'done', v: { canceled: true } | { canceled: false; selected: string | undefined }): void;
(ev: 'closed'): void;
}>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const drafts = ref<(Misskey.entities.Note & { useCw: boolean })[]>([]);
onMounted(loadDrafts);
onActivated(loadDrafts);
function loadDrafts() {
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
drafts.value = Object.keys(stored).map((key) => ({
...(stored[key].data as Misskey.entities.Note & { useCw: boolean }),
id: key,
createdAt: stored[key].updatedAt,
channel: stored[key].channel as Misskey.entities.Channel,
renote: stored[key].renote as Misskey.entities.Note,
reply: stored[key].reply as Misskey.entities.Note,
user: $i as Misskey.entities.User,
}));
}
function selectDraft(draft: string) {
done(false, draft);
}
function removeDraft(draft: string) {
const stored = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
delete stored[draft];
miLocalStorage.setItem('drafts', JSON.stringify(stored));
loadDrafts();
}
function done(canceled: boolean, selected?: string): void {
emit('done', { canceled, selected } as
| { canceled: true }
| { canceled: false; selected: string | undefined });
dialog.value?.close();
}
</script>
<style lang="scss" module>
.draftItem {
display: flex;
padding: 8px 0 8px 0;
border-bottom: 1px solid var(--divider);
&:hover {
color: var(--accent);
background-color: var(--accentedBg);
}
}
.draftNote {
flex: 1;
width: calc(100% - 16px - 48px - 4px);
margin: 0 8px 0 8px;
}
.draftNoteHeader {
display: flex;
flex-wrap: nowrap;
margin-bottom: 4px;
}
.draftNoteDestination {
flex-shrink: 1;
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 4px;
}
.draftNoteInfo {
flex-shrink: 0;
margin-left: auto;
}
.draftNoteCw {
cursor: default;
display: block;
overflow-wrap: break-word;
}
.draftNoteText {
cursor: default;
}
.delete {
width: 48px;
height: 64px;
display: flex;
align-self: center;
justify-content: center;
align-items: center;
background-color: var(--buttonBg);
border-radius: 4px;
margin-right: 4px;
}
</style>

View file

@ -41,6 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span> <span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
<span v-else><i class="ti ti-icons"></i></span> <span v-else><i class="ti ti-icons"></i></span>
</button> </button>
<button v-if="!props.instant" v-click-anime v-tooltip="i18n.ts.drafts" class="_button" :class="$style.headerRightItem" @click="openDrafts">
<i class="ti ti-pencil"></i>
</button>
<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> <button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
<div :class="$style.submitInner"> <div :class="$style.submitInner">
<template v-if="posted"></template> <template v-if="posted"></template>
@ -110,6 +113,7 @@ import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkNotePreview from '@/components/MkNotePreview.vue'; import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue'; import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
import MkDraftsDialog from '@/components/MkDraftsDialog.vue';
import { host, url } from '@/config.js'; import { host, url } from '@/config.js';
import { erase, unique } from '@/scripts/array.js'; import { erase, unique } from '@/scripts/array.js';
import { extractMentions } from '@/scripts/extract-mentions.js'; import { extractMentions } from '@/scripts/extract-mentions.js';
@ -130,6 +134,7 @@ import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js'; import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
import type { NoteDraftItem } from '@/types/note-draft-item.js';
const $i = signinRequired(); const $i = signinRequired();
@ -180,6 +185,10 @@ const visibilityButton = shallowRef<HTMLElement>();
const posting = ref(false); const posting = ref(false);
const posted = ref(false); const posted = ref(false);
const draftId = ref<string>(Date.now().toString());
const reply = ref(props.reply ?? null);
const renote = ref(props.renote ?? null);
const channel = ref(props.channel ?? null);
const text = ref(props.initialText ?? ''); const text = ref(props.initialText ?? '');
const files = ref(props.initialFiles ?? []); const files = ref(props.initialFiles ?? []);
const poll = ref<PollEditorModelValue | null>(null); const poll = ref<PollEditorModelValue | null>(null);
@ -210,25 +219,25 @@ const textAreaReadOnly = ref(false);
const nsfwGuideUrl = 'https://go.misskey.io/media-guideline'; const nsfwGuideUrl = 'https://go.misskey.io/media-guideline';
const draftKey = computed((): string => { const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : ''; let key = channel.value ? `channel:${channel.value.id}` : '';
if (props.renote) { if (renote.value) {
key += `renote:${props.renote.id}`; key += `renote:${renote.value.id}`;
} else if (props.reply) { } else if (reply.value) {
key += `reply:${props.reply.id}`; key += `reply:${reply.value.id}`;
} else { } else {
key += `note:${$i.id}`; key += `note:${draftId.value}`;
} }
return key; return key;
}); });
const placeholder = computed((): string => { const placeholder = computed((): string => {
if (props.renote) { if (renote.value) {
return i18n.ts._postForm.quotePlaceholder; return i18n.ts._postForm.quotePlaceholder;
} else if (props.reply) { } else if (reply.value) {
return i18n.ts._postForm.replyPlaceholder; return i18n.ts._postForm.replyPlaceholder;
} else if (props.channel) { } else if (channel.value) {
return i18n.ts._postForm.channelPlaceholder; return i18n.ts._postForm.channelPlaceholder;
} else { } else {
const xs = [ const xs = [
@ -244,9 +253,9 @@ const placeholder = computed((): string => {
}); });
const submitText = computed((): string => { const submitText = computed((): string => {
return props.renote return renote.value
? i18n.ts.quote ? i18n.ts.quote
: props.reply : reply.value
? i18n.ts.reply ? i18n.ts.reply
: i18n.ts.note; : i18n.ts.note;
}); });
@ -265,8 +274,8 @@ const canPost = computed((): boolean => {
1 <= textLength.value || 1 <= textLength.value ||
1 <= files.value.length || 1 <= files.value.length ||
poll.value != null || poll.value != null ||
props.renote != null || renote.value != null ||
(props.reply != null && quoteId.value != null) (reply.value != null && quoteId.value != null)
) && ) &&
(textLength.value <= maxTextLength.value) && (textLength.value <= maxTextLength.value) &&
(!poll.value || poll.value.choices.length >= 2); (!poll.value || poll.value.choices.length >= 2);
@ -294,13 +303,13 @@ if (props.mention) {
text.value += ' '; text.value += ' ';
} }
if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) { if (reply.value && (reply.value.user.username !== $i.username || (reply.value.user.host != null && reply.value.user.host !== host))) {
text.value = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `; text.value = `@${reply.value.user.username}${reply.value.user.host != null ? '@' + toASCII(reply.value.user.host) : ''} `;
} }
if (props.reply && props.reply.text != null) { if (reply.value && reply.value.text != null) {
const ast = mfm.parse(props.reply.text); const ast = mfm.parse(reply.value.text);
const otherHost = props.reply.user.host; const otherHost = reply.value.user.host;
for (const x of extractMentions(ast)) { for (const x of extractMentions(ast)) {
const mention = x.host ? const mention = x.host ?
@ -323,32 +332,32 @@ if ($i.isSilenced && visibility.value === 'public') {
visibility.value = 'home'; visibility.value = 'home';
} }
if (props.channel) { if (channel.value) {
visibility.value = 'public'; visibility.value = 'public';
localOnly.value = true; // TODO: localOnly.value = true; // TODO:
} }
// //
if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) { if (reply.value && ['home', 'followers', 'specified'].includes(reply.value.visibility)) {
if (props.reply.visibility === 'home' && visibility.value === 'followers') { if (reply.value.visibility === 'home' && visibility.value === 'followers') {
visibility.value = 'followers'; visibility.value = 'followers';
} else if (['home', 'followers'].includes(props.reply.visibility) && visibility.value === 'specified') { } else if (['home', 'followers'].includes(reply.value.visibility) && visibility.value === 'specified') {
visibility.value = 'specified'; visibility.value = 'specified';
} else { } else {
visibility.value = props.reply.visibility; visibility.value = reply.value.visibility;
} }
if (visibility.value === 'specified') { if (visibility.value === 'specified') {
if (props.reply.visibleUserIds) { if (reply.value.visibleUserIds) {
misskeyApi('users/show', { misskeyApi('users/show', {
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply?.userId), userIds: reply.value.visibleUserIds.filter(uid => uid !== $i.id && uid !== reply.value?.userId),
}).then(users => { }).then(users => {
users.forEach(u => pushVisibleUser(u)); users.forEach(u => pushVisibleUser(u));
}); });
} }
if (props.reply.userId !== $i.id) { if (reply.value.userId !== $i.id) {
misskeyApi('users/show', { userId: props.reply.userId }).then(user => { misskeyApi('users/show', { userId: reply.value.userId }).then(user => {
pushVisibleUser(user); pushVisibleUser(user);
}); });
} }
@ -361,9 +370,9 @@ if (props.specified) {
} }
// keep cw when reply // keep cw when reply
if (defaultStore.state.keepCw && props.reply?.cw) { if (defaultStore.state.keepCw && reply.value?.cw) {
useCw.value = true; useCw.value = true;
cw.value = props.reply.cw; cw.value = reply.value.cw;
} }
function watchForDraft() { function watchForDraft() {
@ -465,7 +474,7 @@ function upload(file: File, name?: string): void {
} }
function setVisibility() { function setVisibility() {
if (props.channel) { if (channel.value) {
visibility.value = 'public'; visibility.value = 'public';
localOnly.value = true; // TODO: localOnly.value = true; // TODO:
return; return;
@ -476,7 +485,7 @@ function setVisibility() {
isSilenced: $i.isSilenced, isSilenced: $i.isSilenced,
localOnly: localOnly.value, localOnly: localOnly.value,
src: visibilityButton.value, src: visibilityButton.value,
...(props.reply ? { isReplyVisibilitySpecified: props.reply.visibility === 'specified' } : {}), ...(reply.value ? { isReplyVisibilitySpecified: reply.value.visibility === 'specified' } : {}),
}, { }, {
changeVisibility: v => { changeVisibility: v => {
visibility.value = v; visibility.value = v;
@ -488,7 +497,7 @@ function setVisibility() {
} }
async function toggleLocalOnly() { async function toggleLocalOnly() {
if (props.channel) { if (channel.value) {
visibility.value = 'public'; visibility.value = 'public';
localOnly.value = true; // TODO: localOnly.value = true; // TODO:
return; return;
@ -605,7 +614,7 @@ async function onPaste(ev: ClipboardEvent) {
const paste = ev.clipboardData.getData('text'); const paste = ev.clipboardData.getData('text');
if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) { if (!renote.value && !quoteId.value && paste.startsWith(url + '/notes/')) {
ev.preventDefault(); ev.preventDefault();
os.confirm({ os.confirm({
@ -679,10 +688,32 @@ function onDrop(ev: DragEvent): void {
function saveDraft() { function saveDraft() {
if (props.instant || props.mock) return; if (props.instant || props.mock) return;
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
draftData[draftKey.value] = { draftData[draftKey.value] = {
updatedAt: new Date(), updatedAt: new Date().toISOString(),
channel: channel.value ? {
id: channel.value.id,
name: channel.value.name,
} : undefined,
renote: renote.value ? {
id: renote.value.id,
text: (renote.value.cw ?? renote.value.text)?.substring(0, 100),
user: {
id: renote.value.userId,
username: renote.value.user.username,
host: renote.value.user.host,
},
} : undefined,
reply: reply.value ? {
id: reply.value.id,
text: (reply.value.cw ?? reply.value.text)?.substring(0, 100),
user: {
id: reply.value.userId,
username: reply.value.user.username,
host: reply.value.user.host,
},
} : undefined,
data: { data: {
text: text.value, text: text.value,
useCw: useCw.value, useCw: useCw.value,
@ -700,13 +731,75 @@ function saveDraft() {
} }
function deleteDraft() { function deleteDraft() {
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
delete draftData[draftKey.value]; delete draftData[draftKey.value];
draftId.value = Date.now().toString();
miLocalStorage.setItem('drafts', JSON.stringify(draftData)); miLocalStorage.setItem('drafts', JSON.stringify(draftData));
} }
async function openDrafts() {
const { canceled, selected } = await new Promise<{canceled: boolean, selected: string | undefined}>(resolve => {
os.popup(MkDraftsDialog, {}, {
done: result => {
resolve(typeof result.selected === 'string' ? result : { canceled: true, selected: undefined });
},
}, 'closed');
});
if (canceled) return;
if (selected) {
const channelId = selected.startsWith('channel:') ? selected.match(/channel:(.+?)(renote|reply|note):/)?.[1] : undefined;
const renoteId = selected.includes('renote:') ? selected.match(/renote:(.+)/)?.[1] : undefined;
const replyId = selected.includes('reply:') ? selected.match(/reply:(.+)/)?.[1] : undefined;
channel.value = channelId ? await misskeyApi('channels/show', { channelId }) : null;
renote.value = renoteId ? await misskeyApi('notes/show', { noteId: renoteId }) : null;
reply.value = replyId ? await misskeyApi('notes/show', { noteId: replyId }) : null;
if (!renote.value && !reply.value) {
draftId.value = selected.match(/note:(.+)/)?.[1] ?? Date.now().toString();
} else {
draftId.value = Date.now().toString();
}
loadDraft(true);
}
}
function loadDraft(exactMatch: boolean = false) {
const drafts = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}') as Record<string, NoteDraftItem>;
const scope = exactMatch ? draftKey.value : draftKey.value.replace(`note:${draftId.value}`, 'note:');
const draft = Object.entries(drafts).filter(([k]) => k.startsWith(scope))
.map(r => ({ key: r[0], value: { ...r[1], updatedAt: new Date(r[1].updatedAt).getTime() } }))
.sort((a, b) => b.value.updatedAt - a.value.updatedAt).at(0);
if (draft) {
if (scope !== draft.key) {
draftId.value = draft.key.replace(scope, '');
}
text.value = draft.value.data.text;
useCw.value = draft.value.data.useCw;
cw.value = draft.value.data.cw;
visibility.value = draft.value.data.visibility;
localOnly.value = draft.value.data.localOnly;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
files.value = draft.value.data.files?.filter(f => f?.id && f.type && f.name) || [];
if (draft.value.data.poll) {
poll.value = draft.value.data.poll;
}
if (draft.value.data.visibleUserIds) {
misskeyApi('users/show', { userIds: draft.value.data.visibleUserIds }).then(users => {
users.forEach(u => pushVisibleUser(u));
});
}
}
}
async function post(ev?: MouseEvent) { async function post(ev?: MouseEvent) {
if (useCw.value && (cw.value == null || cw.value.trim() === '')) { if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
os.alert({ os.alert({
@ -764,9 +857,9 @@ async function post(ev?: MouseEvent) {
text: text.value === '' ? null : text.value, text: text.value === '' ? null : text.value,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
fileIds: files.value.length > 0 ? files.value.filter(f => f?.id).map(f => f.id) : undefined, fileIds: files.value.length > 0 ? files.value.filter(f => f?.id).map(f => f.id) : undefined,
replyId: props.reply ? props.reply.id : undefined, replyId: reply.value ? reply.value.id : undefined,
renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined, renoteId: renote.value ? renote.value.id : quoteId.value ? quoteId.value : undefined,
channelId: props.channel ? props.channel.id : undefined, channelId: channel.value ? channel.value.id : undefined,
poll: poll.value, poll: poll.value,
cw: useCw.value ? cw.value ?? '' : null, cw: useCw.value ? cw.value ?? '' : null,
localOnly: localOnly.value, localOnly: localOnly.value,
@ -854,7 +947,7 @@ async function post(ev?: MouseEvent) {
claimAchievement('brainDiver'); claimAchievement('brainDiver');
} }
if (props.renote && (props.renote.userId === $i.id) && text.length > 0) { if (renote.value && (renote.value.userId === $i.id) && text.length > 0) {
claimAchievement('selfQuote'); claimAchievement('selfQuote');
} }
@ -957,25 +1050,8 @@ onMounted(() => {
nextTick(() => { nextTick(() => {
// 稿 // 稿
if (!props.instant && !props.mention && !props.specified && !props.mock) { if (!props.instant && !props.mention && !props.specified && !props.mock && defaultStore.state.autoloadDrafts) {
const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey.value]; loadDraft();
if (draft) {
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;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
files.value = draft.data.files?.filter(f => f?.id && f.type && f.name) || [];
if (draft.data.poll) {
poll.value = draft.data.poll;
}
if (draft.data.visibleUserIds) {
misskeyApi('users/show', { userIds: draft.data.visibleUserIds }).then(users => {
users.forEach(u => pushVisibleUser(u));
});
}
}
} }
// //

View file

@ -42,6 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-if="defaultStore.reactiveState.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton> <MkButton v-if="defaultStore.reactiveState.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
<MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> <MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
</MkFolder> </MkFolder>
<MkSwitch v-model="autoloadDrafts">{{ i18n.ts.autoloadDrafts }}</MkSwitch>
</div> </div>
</FormSection> </FormSection>
@ -302,6 +303,7 @@ const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
const nsfw = computed(defaultStore.makeGetterSetter('nsfw')); const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm')); const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
const showFixedPostFormInChannel = computed(defaultStore.makeGetterSetter('showFixedPostFormInChannel')); const showFixedPostFormInChannel = computed(defaultStore.makeGetterSetter('showFixedPostFormInChannel'));
const autoloadDrafts = computed(defaultStore.makeGetterSetter('autoloadDrafts'));
const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache')); const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache'));
const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker')); const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll')); const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));

View file

@ -84,6 +84,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'useBlurEffect', 'useBlurEffect',
'showFixedPostForm', 'showFixedPostForm',
'showFixedPostFormInChannel', 'showFixedPostFormInChannel',
'autoloadDrafts',
'enableInfiniteScroll', 'enableInfiniteScroll',
'useReactionPickerForContextMenu', 'useReactionPickerForContextMenu',
'showGapBetweenNotesInTimeline', 'showGapBetweenNotesInTimeline',

View file

@ -279,6 +279,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: false, default: false,
}, },
autoloadDrafts: {
where: 'device',
default: true,
},
enableInfiniteScroll: { enableInfiniteScroll: {
where: 'device', where: 'device',
default: true, default: true,

View file

@ -0,0 +1,38 @@
import * as Misskey from 'misskey-js';
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
export type NoteDraftItem = {
updatedAt: string;
channel?: {
id: string;
name: string;
};
renote?: {
id: string;
text: string | null;
user: {
id: string;
username: string;
host: string | null;
};
};
reply?: {
id: string;
text: string | null;
user: {
id: string;
username: string;
host: string | null;
};
};
data: {
text: string;
useCw: boolean;
cw: string | null;
visibility: 'public' | 'followers' | 'home' | 'specified';
localOnly: boolean;
files: Misskey.entities.DriveFile[];
poll: PollEditorModelValue | null;
visibleUserIds?: string[];
};
};