Compare commits

...

11 Commits

Author SHA1 Message Date
syuilo 542ecffadb Update tips.ts 2025-06-06 21:04:11 +09:00
syuilo 490e0a7382 Merge branch 'develop' into post-form-uploader 2025-06-06 21:04:08 +09:00
syuilo be35fe468b refactor(frontend): refactor tips 2025-06-06 21:03:35 +09:00
syuilo 4b9b3ced01 enhance(frontend): improve MkTip usability 2025-06-06 20:59:01 +09:00
syuilo 4c959152de add tip 2025-06-06 20:48:50 +09:00
syuilo ef51d7b9a1 wip 2025-06-06 17:59:37 +09:00
syuilo 171632365d wip 2025-06-06 17:48:27 +09:00
syuilo 012213fc64 Update MkPostForm.vue 2025-06-06 16:18:14 +09:00
syuilo 5db01c1c06 wip 2025-06-06 16:16:40 +09:00
syuilo c8d2b36ec3 wip 2025-06-06 16:05:55 +09:00
syuilo 3098c24d28 Update MkPostForm.vue 2025-06-06 15:58:49 +09:00
11 changed files with 248 additions and 104 deletions

8
locales/index.d.ts vendored
View File

@ -9584,6 +9584,14 @@ export interface Locale extends ILocale {
"disableFederationDescription": string;
};
"_postForm": {
/**
*
*/
"quitInspiteOfThereAreUnuploadedFilesConfirm": string;
/**
* 稿
*/
"uploaderTip": string;
/**
* ...
*/

View File

@ -2522,6 +2522,8 @@ _visibility:
disableFederationDescription: "他サーバーへの配信を行いません"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされていないファイルがありますが、破棄してフォームを閉じますか?"
uploaderTip: "ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。"
replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..."
channelPlaceholder: "チャンネルに投稿..."

View File

@ -73,7 +73,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<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"/>
<div v-if="uploader.items.value.length > 0" style="padding: 12px;">
<MkUploaderItems :items="uploader.items.value" @showMenu="(item, ev) => uploader.showMenu(ev, item)"/>
<MkTip k="postFormUploader">
{{ i18n.ts._postForm.uploaderTip }}
</MkTip>
<MkUploaderItems :items="uploader.items.value" @showMenu="(item, ev) => showPerUploadItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerUploadItemMenuViaContextmenu(item, ev)"/>
</div>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
@ -81,19 +84,17 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<footer :class="$style.footer">
<div :class="$style.footerLeft">
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFromPc"><i class="ti ti-photo-plus"></i></button>
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFromDrive"><i class="ti ti-cloud-download"></i></button>
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.upload + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromPc"><i class="ti ti-photo-plus"></i></button>
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.fromDrive + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromDrive"><i class="ti ti-cloud-download"></i></button>
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
</div>
<div :class="$style.footerRight">
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
<!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button>-->
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
</div>
</footer>
<datalist id="hashtags">
@ -114,6 +115,7 @@ import type { ShallowRef } from 'vue';
import type { PostFormProps } from '@/types/post-form.js';
import type { MenuItem } from '@/types/menu.js';
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
import type { UploaderItem } from '@/composables/use-uploader.js';
import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
import XTextCounter from '@/components/MkPostForm.TextCounter.vue';
@ -125,7 +127,7 @@ import { formatTimeString } from '@/utility/format-time-string.js';
import { Autocomplete } from '@/utility/autocomplete.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { selectFile } from '@/utility/drive.js';
import { chooseDriveFile } from '@/utility/drive.js';
import { store } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
@ -208,9 +210,12 @@ const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(
const postFormActions = getPluginHandlers('post_form_action');
const uploader = useUploader({
multiple: props.multiple,
folderId: props.folderId,
features: props.features,
multiple: true,
});
uploader.events.on('itemUploaded', ctx => {
files.value.push(ctx.item.uploaded!);
uploader.removeItem(ctx.item);
});
const draftKey = computed((): string => {
@ -270,7 +275,7 @@ const cwTextLength = computed((): number => {
const maxCwTextLength = 100;
const canPost = computed((): boolean => {
return !props.mock && !posting.value && !posted.value && !uploader.uploading.value &&
return !props.mock && !posting.value && !posted.value && !uploader.uploading.value && uploader.readyForUpload.value &&
(
1 <= textLength.value ||
1 <= files.value.length ||
@ -456,6 +461,14 @@ function chooseFileFromPc(ev: MouseEvent) {
});
}
function chooseFileFromDrive(ev: MouseEvent) {
if (props.mock) return;
chooseDriveFile({ multiple: true }).then(driveFiles => {
files.value.push(...driveFiles);
});
}
function detachFile(id) {
files.value = files.value.filter(x => x.id !== id);
}
@ -579,6 +592,10 @@ function showOtherSettings() {
toggleReactionAcceptance();
},
}, { type: 'divider' }, {
type: 'switch',
text: i18n.ts.preview,
ref: showPreview,
}, {
icon: 'ti ti-trash',
text: i18n.ts.reset,
danger: true,
@ -1064,6 +1081,16 @@ function openAccountMenu(ev: MouseEvent) {
}, ev);
}
function showPerUploadItemMenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.contextMenu(menu, ev);
}
onMounted(() => {
if (props.autofocus) {
focus();
@ -1132,8 +1159,23 @@ onMounted(() => {
});
});
async function canClose() {
if (!uploader.allItemsUploaded.value) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts._postForm.quitInspiteOfThereAreUnuploadedFilesConfirm,
okText: i18n.ts.yes,
cancelText: i18n.ts.no,
});
if (canceled) return false;
}
return true;
}
defineExpose({
clear,
canClose,
});
</script>

View File

@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkModal
ref="modal"
:preferType="'dialog'"
@click="modal?.close()"
@click="_close()"
@closed="onModalClosed()"
@esc="modal?.close()"
@esc="_close()"
>
<MkPostForm
ref="form"
@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
autofocus
freezeAfterPosted
@posted="onPosted"
@cancel="modal?.close()"
@esc="modal?.close()"
@cancel="_close()"
@esc="_close()"
/>
</MkModal>
</template>
@ -43,6 +43,7 @@ const emit = defineEmits<{
}>();
const modal = useTemplateRef('modal');
const form = useTemplateRef('form');
function onPosted() {
modal.value?.close({
@ -50,6 +51,12 @@ function onPosted() {
});
}
async function _close() {
const canClose = await form.value?.canClose();
if (!canClose) return;
modal.value?.close();
}
function onModalClosed() {
emit('closed');
}

View File

@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._uploader.tip }}
</MkTip>
<MkUploaderItems :items="items" @showMenu="(item, ev) => uploader.showMenu(ev, item)"/>
<MkUploaderItems :items="items" @showMenu="(item, ev) => showPerItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerItemMenuViaContextmenu(item, ev)"/>
<div v-if="props.multiple">
<MkButton style="margin: auto;" :iconOnly="true" rounded @click="chooseFile($event)"><i class="ti ti-plus"></i></MkButton>
@ -39,8 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #footer>
<div class="_buttonsCenter">
<MkButton v-if="isUploading" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
<MkButton v-else-if="!firstUploadAttempted" primary rounded @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
<MkButton v-if="uploader.uploading.value" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
<MkButton v-else-if="!firstUploadAttempted" primary rounded :disabled="!uploader.readyForUpload.value" @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
<MkButton v-if="canRetry" rounded @click="upload()"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
<MkButton v-if="canDone" rounded @click="done()"><i class="ti ti-arrow-right"></i> {{ i18n.ts.done }}</MkButton>
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import type { UploaderFeatures } from '@/composables/use-uploader.js';
import type { UploaderFeatures, UploaderItem } from '@/composables/use-uploader.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
@ -93,8 +93,7 @@ onMounted(() => {
const items = uploader.items;
const firstUploadAttempted = ref(false);
const isUploading = computed(() => items.value.some(item => item.uploading));
const canRetry = computed(() => firstUploadAttempted.value && !items.value.some(item => item.uploading || item.preprocessing) && items.value.some(item => item.uploaded == null));
const canRetry = computed(() => firstUploadAttempted.value && uploader.readyForUpload.value);
const canDone = computed(() => items.value.some(item => item.uploaded != null));
const overallProgress = computed(() => {
const max = items.value.length;
@ -152,7 +151,7 @@ async function abortWithConfirm() {
}
async function done() {
if (items.value.some(item => item.uploaded == null)) {
if (!uploader.allItemsUploaded.value) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts._uploader.doneConfirm,
@ -170,6 +169,16 @@ async function chooseFile(ev: MouseEvent) {
const newFiles = await os.chooseFileFromPc({ multiple: true });
uploader.addFiles(newFiles);
}
function showPerItemMenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
function showPerItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.contextMenu(menu, ev);
}
</script>
<style lang="scss" module>

View File

@ -11,12 +11,13 @@ SPDX-License-Identifier: AGPL-3.0-only
v-panel
:class="[$style.item, { [$style.itemWaiting]: item.preprocessing, [$style.itemCompleted]: item.uploaded, [$style.itemFailed]: item.uploadFailed }]"
:style="{ '--p': item.progress != null ? `${item.progress.value / item.progress.max * 100}%` : '0%' }"
@contextmenu.prevent.stop="onContextmenu(item, $event)"
>
<div :class="$style.itemInner">
<div :class="$style.itemActionWrapper">
<MkButton :iconOnly="true" rounded @click="emit('showMenu', item, $event)"><i class="ti ti-dots"></i></MkButton>
</div>
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }"></div>
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }" @click="onThumbnailClick(item, $event)"></div>
<div :class="$style.itemBody">
<div><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div>
<div :class="$style.itemInfo">
@ -38,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { isLink } from '@@/js/is-link.js';
import type { UploaderItem } from '@/composables/use-uploader.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
@ -49,7 +51,19 @@ const props = defineProps<{
const emit = defineEmits<{
(ev: 'showMenu', item: UploaderItem, event: MouseEvent): void;
(ev: 'showMenuViaContextmenu', item: UploaderItem, event: MouseEvent): void;
}>();
function onContextmenu(item: UploaderItem, ev: MouseEvent) {
if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== '') return;
emit('showMenuViaContextmenu', item, ev);
}
function onThumbnailClick(item: UploaderItem, ev: MouseEvent) {
// TODO: preview when item is image
}
</script>
<style lang="scss" module>

View File

@ -7,7 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="!store.r.tips.value[props.k]" :class="[$style.root, { [$style.warn]: warn }]" class="_selectable _gaps_s">
<div style="font-weight: bold;"><i class="ti ti-bulb"></i> {{ i18n.ts.tip }}:</div>
<div><slot></slot></div>
<MkButton primary rounded small @click="closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
<div>
<MkButton inline primary rounded small @click="_closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
<button class="_button" style="padding: 8px; margin-left: 4px;" @click="showMenu"><i class="ti ti-dots"></i></button>
</div>
</div>
</template>
@ -15,19 +18,30 @@ SPDX-License-Identifier: AGPL-3.0-only
import { i18n } from '@/i18n.js';
import { store } from '@/store.js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { TIPS, hideAllTips, closeTip } from '@/tips.js';
const props = withDefaults(defineProps<{
k: keyof (typeof store['s']['tips']);
k: typeof TIPS[number];
warn?: boolean;
}>(), {
warn: false,
});
function closeTip() {
store.set('tips', {
...store.r.tips.value,
[props.k]: true,
});
function _closeTip() {
closeTip(props.k);
}
function showMenu(ev: MouseEvent) {
os.popupMenu([{
icon: 'ti ti-bulb-off',
text: i18n.ts.hideAllTips,
danger: true,
action: () => {
hideAllTips();
os.success();
},
}], ev.currentTarget ?? ev.target);
}
</script>

View File

@ -6,6 +6,7 @@
import * as Misskey from 'misskey-js';
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
import isAnimated from 'is-file-animated';
import { EventEmitter } from 'eventemitter3';
import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import { genId } from '@/utility/id.js';
@ -97,6 +98,10 @@ export function useUploader(options: {
} = {}) {
const $i = ensureSignin();
const events = new EventEmitter<{
'itemUploaded': (ctx: { item: UploaderItem; }) => void;
}>();
const uploaderFeatures = computed<Required<UploaderFeatures>>(() => {
return {
effect: options.features?.effect ?? true,
@ -142,25 +147,31 @@ export function useUploader(options: {
items.value.splice(items.value.indexOf(item), 1);
}
function showMenu(ev: MouseEvent, item: UploaderItem) {
function getMenu(item: UploaderItem): MenuItem[] {
const menu: MenuItem[] = [];
menu.push({
icon: 'ti ti-cursor-text',
text: i18n.ts.rename,
action: async () => {
const { result, canceled } = await os.inputText({
type: 'text',
title: i18n.ts.rename,
placeholder: item.name,
default: item.name,
});
if (canceled) return;
if (result.trim() === '') return;
if (
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({
icon: 'ti ti-cursor-text',
text: i18n.ts.rename,
action: async () => {
const { result, canceled } = await os.inputText({
type: 'text',
title: i18n.ts.rename,
placeholder: item.name,
default: item.name,
});
if (canceled) return;
if (result.trim() === '') return;
item.name = result;
},
});
item.name = result;
},
});
}
if (
uploaderFeatures.value.crop &&
@ -332,6 +343,12 @@ export function useUploader(options: {
if (!item.preprocessing && !item.uploading && !item.uploaded) {
menu.push({
type: 'divider',
}, {
icon: 'ti ti-upload',
text: i18n.ts.upload,
action: () => {
uploadOne(item);
},
}, {
icon: 'ti ti-x',
text: i18n.ts.remove,
@ -354,7 +371,46 @@ export function useUploader(options: {
});
}
os.popupMenu(menu, ev.currentTarget ?? ev.target);
return menu;
}
async function uploadOne(item: UploaderItem): Promise<void> {
item.uploadFailed = false;
item.uploading = true;
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
name: item.uploadName ?? item.name,
folderId: options.folderId,
onProgress: (progress) => {
if (item.progress == null) {
item.progress = { max: progress.total, value: progress.loaded };
} else {
item.progress.value = progress.loaded;
item.progress.max = progress.total;
}
},
});
item.abort = () => {
item.abort = null;
abort();
item.uploading = false;
item.uploadFailed = true;
};
await filePromise.then((file) => {
item.uploaded = file;
item.abort = null;
events.emit('itemUploaded', { item });
}).catch(err => {
item.uploadFailed = true;
item.progress = null;
if (!(err instanceof UploadAbortedError)) {
throw err;
}
}).finally(() => {
item.uploading = false;
});
}
async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
@ -371,41 +427,7 @@ export function useUploader(options: {
continue;
}
item.uploadFailed = false;
item.uploading = true;
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
name: item.uploadName ?? item.name,
folderId: options.folderId,
onProgress: (progress) => {
if (item.progress == null) {
item.progress = { max: progress.total, value: progress.loaded };
} else {
item.progress.value = progress.loaded;
item.progress.max = progress.total;
}
},
});
item.abort = () => {
item.abort = null;
abort();
item.uploading = false;
item.uploadFailed = true;
};
await filePromise.then((file) => {
item.uploaded = file;
item.abort = null;
}).catch(err => {
item.uploadFailed = true;
item.progress = null;
if (!(err instanceof UploadAbortedError)) {
throw err;
}
}).finally(() => {
item.uploading = false;
});
await uploadOne(item);
}
}
@ -503,8 +525,11 @@ export function useUploader(options: {
removeItem,
abortAll,
upload,
showMenu,
getMenu,
uploading: computed(() => items.value.some(item => item.uploading)),
readyForUpload: computed(() => items.value.length > 0 && items.value.some(item => item.uploaded == null) && !items.value.some(item => item.uploading || item.preprocessing)),
allItemsUploaded: computed(() => items.value.every(item => item.uploaded != null)),
events,
};
}

View File

@ -161,7 +161,7 @@ import { prefer } from '@/preferences.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js';
import { store, TIPS } from '@/store.js';
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
const $i = ensureSignin();
@ -205,16 +205,12 @@ function migrate() {
}
function resetAllTips() {
store.set('tips', {});
_resetAllTips();
os.success();
}
function hideAllTips() {
const v = {};
for (const k of TIPS) {
v[k] = true;
}
store.set('tips', v);
_hideAllTips();
os.success();
}

View File

@ -10,22 +10,11 @@ import darkTheme from '@@/themes/d-green-lime.json5';
import { hemisphere } from '@@/js/intl-const.js';
import type { DeviceKind } from '@/utility/device-kind.js';
import type { Plugin } from '@/plugin.js';
import type { TIPS } from '@/tips.js';
import { miLocalStorage } from '@/local-storage.js';
import { Pizzax } from '@/lib/pizzax.js';
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
export const TIPS = [
'drive',
'uploader',
'clips',
'userLists',
'tl.home',
'tl.local',
'tl.social',
'tl.global',
'abuses',
] as const;
/**
* (not)
*/

View File

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { store } from '@/store.js';
export const TIPS = [
'drive',
'uploader',
'postFormUploader',
'clips',
'userLists',
'tl.home',
'tl.local',
'tl.social',
'tl.global',
'abuses',
] as const;
export function closeTip(tip: typeof TIPS[number]) {
store.set('tips', {
...store.r.tips.value,
[tip]: true,
});
}
export function resetAllTips() {
store.set('tips', {});
}
export function hideAllTips() {
const v = {};
for (const k of TIPS) {
v[k] = true;
}
store.set('tips', v);
}