summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-03-24 16:54:37 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-03-24 16:54:37 +0900
commit5f52b1332514d4c2fa02ac55f0e56ff5ff147a96 (patch)
tree38f65fe3f3c59800a3a55eaab690228b2a2c5661
parentrefactor(backend): rename cache class (diff)
downloadmisskey-5f52b1332514d4c2fa02ac55f0e56ff5ff147a96.tar.gz
misskey-5f52b1332514d4c2fa02ac55f0e56ff5ff147a96.tar.bz2
misskey-5f52b1332514d4c2fa02ac55f0e56ff5ff147a96.zip
enhance(frontend): クリップボタンをノートアクションに追加できるように
-rw-r--r--CHANGELOG.md1
-rw-r--r--locales/ja-JP.yml3
-rw-r--r--packages/frontend/src/cache.ts5
-rw-r--r--packages/frontend/src/components/MkNote.vue10
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue10
-rw-r--r--packages/frontend/src/pages/admin/roles.edit.vue2
-rw-r--r--packages/frontend/src/pages/clip.vue5
-rw-r--r--packages/frontend/src/pages/my-clips/index.vue2
-rw-r--r--packages/frontend/src/pages/settings/general.vue2
-rw-r--r--packages/frontend/src/scripts/cache.ts80
-rw-r--r--packages/frontend/src/scripts/get-note-menu.ts134
-rw-r--r--packages/frontend/src/scripts/get-user-menu.ts3
-rw-r--r--packages/frontend/src/store.ts4
13 files changed, 199 insertions, 62 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37e8bf0f41..2b482d0207 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加
### Client
+- クリップボタンをノートアクションに追加できるように
- センシティブワードの一覧にピン留めユーザーのIDが表示される問題を修正
### Server
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index ffcfbd9631..8d2b4384a0 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -460,7 +460,7 @@ aboutX: "{x}について"
emojiStyle: "絵文字のスタイル"
native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示しない"
-showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する"
+showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
noHistory: "履歴はありません"
signinHistory: "ログイン履歴"
enableAdvancedMfm: "高度なMFMを有効にする"
@@ -982,6 +982,7 @@ retryAllQueuesNow: "すべてのキューを今すぐ再試行"
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
+showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
_achievements:
earnedAt: "獲得日時"
diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts
new file mode 100644
index 0000000000..1a8df7fcf6
--- /dev/null
+++ b/packages/frontend/src/cache.ts
@@ -0,0 +1,5 @@
+import * as misskey from 'misskey-js';
+import { Cache } from '@/scripts/cache';
+
+export const clipsCache = new Cache<misskey.entities.Clip[]>(Infinity);
+export const rolesCache = new Cache(Infinity);
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index af81051a54..72c6e55df1 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -109,6 +109,9 @@
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i>
</button>
+ <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
+ <i class="ti ti-paperclip"></i>
+ </button>
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="menu()">
<i class="ti ti-dots"></i>
</button>
@@ -151,7 +154,7 @@ import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import { $i } from '@/account';
import { i18n } from '@/i18n';
-import { getNoteMenu } from '@/scripts/get-note-menu';
+import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
@@ -192,6 +195,7 @@ const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>();
+const clipButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false);
@@ -392,6 +396,10 @@ function menu(viaKeyboard = false): void {
}).then(focus);
}
+async function clip() {
+ os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClipPage }), clipButton.value).then(focus);
+}
+
function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return;
os.popupMenu([{
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index ea72e1b517..715fd3a9a8 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -114,6 +114,9 @@
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i>
</button>
+ <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="button _button" @mousedown="clip()">
+ <i class="ti ti-paperclip"></i>
+ </button>
<button ref="menuButton" class="button _button" @mousedown="menu()">
<i class="ti ti-dots"></i>
</button>
@@ -156,7 +159,7 @@ import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import { $i } from '@/account';
import { i18n } from '@/i18n';
-import { getNoteMenu } from '@/scripts/get-note-menu';
+import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
@@ -196,6 +199,7 @@ const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>();
+const clipButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false);
@@ -384,6 +388,10 @@ function menu(viaKeyboard = false): void {
}).then(focus);
}
+async function clip() {
+ os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus);
+}
+
function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return;
os.popupMenu([{
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index e6896237f8..b1aa03f1f7 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -26,6 +26,7 @@ import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import { useRouter } from '@/router';
import MkButton from '@/components/MkButton.vue';
+import { rolesCache } from '@/cache';
const router = useRouter();
@@ -61,6 +62,7 @@ if (props.id) {
}
async function save() {
+ rolesCache.delete();
if (role) {
os.apiWithDialog('admin/roles/update', {
roleId: role.id,
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 7515a9122a..2b64de088a 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -30,6 +30,7 @@ import * as os from '@/os';
import { definePageMetadata } from '@/scripts/page-metadata';
import { url } from '@/config';
import MkButton from '@/components/MkButton.vue';
+import { clipsCache } from '@/cache';
const props = defineProps<{
clipId: string,
@@ -108,6 +109,8 @@ const headerActions = $computed(() => clip && isOwned ? [{
clipId: clip.id,
...result,
});
+
+ clipsCache.delete();
},
}, ...(clip.isPublic ? [{
icon: 'ti ti-share',
@@ -133,6 +136,8 @@ const headerActions = $computed(() => clip && isOwned ? [{
await os.apiWithDialog('clips/delete', {
clipId: clip.id,
});
+
+ clipsCache.delete();
},
}] : null);
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index 4c23985f3b..1ea4a29079 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -65,6 +65,8 @@ async function create() {
os.apiWithDialog('clips/create', result);
+ clipsCache.delete();
+
pagingComponent.reload();
}
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 2e2c456c07..dd62a32530 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -47,6 +47,7 @@
<div class="_gaps_m">
<div class="_gaps_s">
<MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch>
+ <MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch>
<MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch>
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
@@ -143,6 +144,7 @@ async function reloadAsk() {
const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
+const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
diff --git a/packages/frontend/src/scripts/cache.ts b/packages/frontend/src/scripts/cache.ts
new file mode 100644
index 0000000000..858e5f03bf
--- /dev/null
+++ b/packages/frontend/src/scripts/cache.ts
@@ -0,0 +1,80 @@
+
+export class Cache<T> {
+ private cachedAt: number | null = null;
+ private value: T | undefined;
+ private lifetime: number;
+
+ constructor(lifetime: Cache<never>['lifetime']) {
+ this.lifetime = lifetime;
+ }
+
+ public set(value: T): void {
+ this.cachedAt = Date.now();
+ this.value = value;
+ }
+
+ public get(): T | undefined {
+ if (this.cachedAt == null) return undefined;
+ if ((Date.now() - this.cachedAt) > this.lifetime) {
+ this.value = undefined;
+ this.cachedAt = null;
+ return undefined;
+ }
+ return this.value;
+ }
+
+ public delete() {
+ this.value = undefined;
+ this.cachedAt = null;
+ }
+
+ /**
+ * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
+ * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
+ */
+ public async fetch(fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
+ const cachedValue = this.get();
+ if (cachedValue !== undefined) {
+ if (validator) {
+ if (validator(cachedValue)) {
+ // Cache HIT
+ return cachedValue;
+ }
+ } else {
+ // Cache HIT
+ return cachedValue;
+ }
+ }
+
+ // Cache MISS
+ const value = await fetcher();
+ this.set(value);
+ return value;
+ }
+
+ /**
+ * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
+ * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
+ */
+ public async fetchMaybe(fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
+ const cachedValue = this.get();
+ if (cachedValue !== undefined) {
+ if (validator) {
+ if (validator(cachedValue)) {
+ // Cache HIT
+ return cachedValue;
+ }
+ } else {
+ // Cache HIT
+ return cachedValue;
+ }
+ }
+
+ // Cache MISS
+ const value = await fetcher();
+ if (value !== undefined) {
+ this.set(value);
+ }
+ return value;
+ }
+}
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 9c0ff3d1b2..00f2523bf9 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -10,6 +10,81 @@ import { url } from '@/config';
import { noteActions } from '@/store';
import { miLocalStorage } from '@/local-storage';
import { getUserMenu } from '@/scripts/get-user-menu';
+import { clipsCache } from '@/cache';
+
+export async function getNoteClipMenu(props: {
+ note: misskey.entities.Note;
+ isDeleted: Ref<boolean>;
+ currentClipPage?: Ref<misskey.entities.Clip>;
+}) {
+ const isRenote = (
+ props.note.renote != null &&
+ props.note.text == null &&
+ props.note.fileIds.length === 0 &&
+ props.note.poll == null
+ );
+
+ const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
+
+ const clips = await clipsCache.fetch(() => os.api('clips/list'));
+ return [...clips.map(clip => ({
+ text: clip.name,
+ action: () => {
+ claimAchievement('noteClipped1');
+ os.promiseDialog(
+ os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
+ null,
+ async (err) => {
+ if (err.id === '734806c4-542c-463a-9311-15c512803965') {
+ const confirm = await os.confirm({
+ type: 'warning',
+ text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }),
+ });
+ if (!confirm.canceled) {
+ os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
+ if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true;
+ }
+ } else {
+ os.alert({
+ type: 'error',
+ text: err.message + '\n' + err.id,
+ });
+ }
+ },
+ );
+ },
+ })), null, {
+ icon: 'ti ti-plus',
+ text: i18n.ts.createNew,
+ action: async () => {
+ const { canceled, result } = await os.form(i18n.ts.createNewClip, {
+ name: {
+ type: 'string',
+ label: i18n.ts.name,
+ },
+ description: {
+ type: 'string',
+ required: false,
+ multiline: true,
+ label: i18n.ts.description,
+ },
+ isPublic: {
+ type: 'boolean',
+ label: i18n.ts.public,
+ default: false,
+ },
+ });
+ if (canceled) return;
+
+ const clip = await os.apiWithDialog('clips/create', result);
+
+ clipsCache.delete();
+
+ claimAchievement('noteClipped1');
+ os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
+ },
+ }];
+}
export function getNoteMenu(props: {
note: misskey.entities.Note;
@@ -208,64 +283,7 @@ export function getNoteMenu(props: {
type: 'parent',
icon: 'ti ti-paperclip',
text: i18n.ts.clip,
- children: async () => {
- const clips = await os.api('clips/list');
- return [{
- icon: 'ti ti-plus',
- text: i18n.ts.createNew,
- action: async () => {
- const { canceled, result } = await os.form(i18n.ts.createNewClip, {
- name: {
- type: 'string',
- label: i18n.ts.name,
- },
- description: {
- type: 'string',
- required: false,
- multiline: true,
- label: i18n.ts.description,
- },
- isPublic: {
- type: 'boolean',
- label: i18n.ts.public,
- default: false,
- },
- });
- if (canceled) return;
-
- const clip = await os.apiWithDialog('clips/create', result);
-
- claimAchievement('noteClipped1');
- os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
- },
- }, null, ...clips.map(clip => ({
- text: clip.name,
- action: () => {
- claimAchievement('noteClipped1');
- os.promiseDialog(
- os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
- null,
- async (err) => {
- if (err.id === '734806c4-542c-463a-9311-15c512803965') {
- const confirm = await os.confirm({
- type: 'warning',
- text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }),
- });
- if (!confirm.canceled) {
- os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
- if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true;
- }
- } else {
- os.alert({
- type: 'error',
- text: err.message + '\n' + err.id,
- });
- }
- },
- );
- },
- }))];
- },
+ children: () => getNoteClipMenu(props),
},
statePromise.then(state => state.isMutedThread ? {
icon: 'ti ti-message-off',
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index d7eb331183..dab1bff199 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -8,6 +8,7 @@ import { userActions } from '@/store';
import { $i, iAmModerator } from '@/account';
import { mainRouter } from '@/router';
import { Router } from '@/nirax';
+import { rolesCache } from '@/cache';
export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
const meId = $i ? $i.id : null;
@@ -147,7 +148,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
icon: 'ti ti-badges',
text: i18n.ts.roles,
children: async () => {
- const roles = await os.api('admin/roles/list');
+ const roles = await rolesCache.fetch(() => os.api('admin/roles/list'));
return roles.filter(r => r.target === 'manual').map(r => ({
text: r.name,
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 3d87234f41..c3cf48afc4 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -290,6 +290,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
+ showClipButtonInNoteFooter: {
+ where: 'device',
+ default: false,
+ },
aiChanMode: {
where: 'device',
default: false,