mirror of
https://github.com/hotomoe/hotomoe
synced 2024-11-24 15:16:13 +09:00
enhance(frontend): クリップボタンをノートアクションに追加できるように
This commit is contained in:
parent
e438091113
commit
5f52b13325
@ -18,6 +18,7 @@
|
|||||||
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加
|
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- クリップボタンをノートアクションに追加できるように
|
||||||
- センシティブワードの一覧にピン留めユーザーのIDが表示される問題を修正
|
- センシティブワードの一覧にピン留めユーザーのIDが表示される問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
@ -460,7 +460,7 @@ aboutX: "{x}について"
|
|||||||
emojiStyle: "絵文字のスタイル"
|
emojiStyle: "絵文字のスタイル"
|
||||||
native: "ネイティブ"
|
native: "ネイティブ"
|
||||||
disableDrawer: "メニューをドロワーで表示しない"
|
disableDrawer: "メニューをドロワーで表示しない"
|
||||||
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する"
|
showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
|
||||||
noHistory: "履歴はありません"
|
noHistory: "履歴はありません"
|
||||||
signinHistory: "ログイン履歴"
|
signinHistory: "ログイン履歴"
|
||||||
enableAdvancedMfm: "高度なMFMを有効にする"
|
enableAdvancedMfm: "高度なMFMを有効にする"
|
||||||
@ -982,6 +982,7 @@ retryAllQueuesNow: "すべてのキューを今すぐ再試行"
|
|||||||
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
|
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
|
||||||
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
|
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
|
||||||
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
|
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
|
||||||
|
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||||
|
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "獲得日時"
|
earnedAt: "獲得日時"
|
||||||
|
5
packages/frontend/src/cache.ts
Normal file
5
packages/frontend/src/cache.ts
Normal file
@ -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);
|
@ -109,6 +109,9 @@
|
|||||||
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)">
|
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)">
|
||||||
<i class="ti ti-minus"></i>
|
<i class="ti ti-minus"></i>
|
||||||
</button>
|
</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()">
|
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="menu()">
|
||||||
<i class="ti ti-dots"></i>
|
<i class="ti ti-dots"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -151,7 +154,7 @@ import { reactionPicker } from '@/scripts/reaction-picker';
|
|||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { i18n } from '@/i18n';
|
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 { useNoteCapture } from '@/scripts/use-note-capture';
|
||||||
import { deepClone } from '@/scripts/clone';
|
import { deepClone } from '@/scripts/clone';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
@ -192,6 +195,7 @@ const menuButton = shallowRef<HTMLElement>();
|
|||||||
const renoteButton = shallowRef<HTMLElement>();
|
const renoteButton = shallowRef<HTMLElement>();
|
||||||
const renoteTime = shallowRef<HTMLElement>();
|
const renoteTime = shallowRef<HTMLElement>();
|
||||||
const reactButton = shallowRef<HTMLElement>();
|
const reactButton = shallowRef<HTMLElement>();
|
||||||
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
@ -392,6 +396,10 @@ function menu(viaKeyboard = false): void {
|
|||||||
}).then(focus);
|
}).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clip() {
|
||||||
|
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClipPage }), clipButton.value).then(focus);
|
||||||
|
}
|
||||||
|
|
||||||
function showRenoteMenu(viaKeyboard = false): void {
|
function showRenoteMenu(viaKeyboard = false): void {
|
||||||
if (!isMyRenote) return;
|
if (!isMyRenote) return;
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
|
@ -114,6 +114,9 @@
|
|||||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||||
<i class="ti ti-minus"></i>
|
<i class="ti ti-minus"></i>
|
||||||
</button>
|
</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()">
|
<button ref="menuButton" class="button _button" @mousedown="menu()">
|
||||||
<i class="ti ti-dots"></i>
|
<i class="ti ti-dots"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -156,7 +159,7 @@ import { reactionPicker } from '@/scripts/reaction-picker';
|
|||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { i18n } from '@/i18n';
|
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 { useNoteCapture } from '@/scripts/use-note-capture';
|
||||||
import { deepClone } from '@/scripts/clone';
|
import { deepClone } from '@/scripts/clone';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
@ -196,6 +199,7 @@ const menuButton = shallowRef<HTMLElement>();
|
|||||||
const renoteButton = shallowRef<HTMLElement>();
|
const renoteButton = shallowRef<HTMLElement>();
|
||||||
const renoteTime = shallowRef<HTMLElement>();
|
const renoteTime = shallowRef<HTMLElement>();
|
||||||
const reactButton = shallowRef<HTMLElement>();
|
const reactButton = shallowRef<HTMLElement>();
|
||||||
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
@ -384,6 +388,10 @@ function menu(viaKeyboard = false): void {
|
|||||||
}).then(focus);
|
}).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clip() {
|
||||||
|
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus);
|
||||||
|
}
|
||||||
|
|
||||||
function showRenoteMenu(viaKeyboard = false): void {
|
function showRenoteMenu(viaKeyboard = false): void {
|
||||||
if (!isMyRenote) return;
|
if (!isMyRenote) return;
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
|
@ -26,6 +26,7 @@ import { i18n } from '@/i18n';
|
|||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from '@/router';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { rolesCache } from '@/cache';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -61,6 +62,7 @@ if (props.id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
rolesCache.delete();
|
||||||
if (role) {
|
if (role) {
|
||||||
os.apiWithDialog('admin/roles/update', {
|
os.apiWithDialog('admin/roles/update', {
|
||||||
roleId: role.id,
|
roleId: role.id,
|
||||||
|
@ -30,6 +30,7 @@ import * as os from '@/os';
|
|||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { url } from '@/config';
|
import { url } from '@/config';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { clipsCache } from '@/cache';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
clipId: string,
|
clipId: string,
|
||||||
@ -108,6 +109,8 @@ const headerActions = $computed(() => clip && isOwned ? [{
|
|||||||
clipId: clip.id,
|
clipId: clip.id,
|
||||||
...result,
|
...result,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clipsCache.delete();
|
||||||
},
|
},
|
||||||
}, ...(clip.isPublic ? [{
|
}, ...(clip.isPublic ? [{
|
||||||
icon: 'ti ti-share',
|
icon: 'ti ti-share',
|
||||||
@ -133,6 +136,8 @@ const headerActions = $computed(() => clip && isOwned ? [{
|
|||||||
await os.apiWithDialog('clips/delete', {
|
await os.apiWithDialog('clips/delete', {
|
||||||
clipId: clip.id,
|
clipId: clip.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clipsCache.delete();
|
||||||
},
|
},
|
||||||
}] : null);
|
}] : null);
|
||||||
|
|
||||||
|
@ -65,6 +65,8 @@ async function create() {
|
|||||||
|
|
||||||
os.apiWithDialog('clips/create', result);
|
os.apiWithDialog('clips/create', result);
|
||||||
|
|
||||||
|
clipsCache.delete();
|
||||||
|
|
||||||
pagingComponent.reload();
|
pagingComponent.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch>
|
<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="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch>
|
||||||
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
|
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
|
||||||
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</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 overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
|
||||||
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
|
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
|
||||||
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
|
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
|
||||||
|
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
|
||||||
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
|
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
|
||||||
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
||||||
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
||||||
|
80
packages/frontend/src/scripts/cache.ts
Normal file
80
packages/frontend/src/scripts/cache.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,81 @@ import { url } from '@/config';
|
|||||||
import { noteActions } from '@/store';
|
import { noteActions } from '@/store';
|
||||||
import { miLocalStorage } from '@/local-storage';
|
import { miLocalStorage } from '@/local-storage';
|
||||||
import { getUserMenu } from '@/scripts/get-user-menu';
|
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: {
|
export function getNoteMenu(props: {
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
@ -208,64 +283,7 @@ export function getNoteMenu(props: {
|
|||||||
type: 'parent',
|
type: 'parent',
|
||||||
icon: 'ti ti-paperclip',
|
icon: 'ti ti-paperclip',
|
||||||
text: i18n.ts.clip,
|
text: i18n.ts.clip,
|
||||||
children: async () => {
|
children: () => getNoteClipMenu(props),
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}))];
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
statePromise.then(state => state.isMutedThread ? {
|
statePromise.then(state => state.isMutedThread ? {
|
||||||
icon: 'ti ti-message-off',
|
icon: 'ti ti-message-off',
|
||||||
|
@ -8,6 +8,7 @@ import { userActions } from '@/store';
|
|||||||
import { $i, iAmModerator } from '@/account';
|
import { $i, iAmModerator } from '@/account';
|
||||||
import { mainRouter } from '@/router';
|
import { mainRouter } from '@/router';
|
||||||
import { Router } from '@/nirax';
|
import { Router } from '@/nirax';
|
||||||
|
import { rolesCache } from '@/cache';
|
||||||
|
|
||||||
export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
|
export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
|
||||||
const meId = $i ? $i.id : null;
|
const meId = $i ? $i.id : null;
|
||||||
@ -147,7 +148,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
|||||||
icon: 'ti ti-badges',
|
icon: 'ti ti-badges',
|
||||||
text: i18n.ts.roles,
|
text: i18n.ts.roles,
|
||||||
children: async () => {
|
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 => ({
|
return roles.filter(r => r.target === 'manual').map(r => ({
|
||||||
text: r.name,
|
text: r.name,
|
||||||
|
@ -290,6 +290,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
showClipButtonInNoteFooter: {
|
||||||
|
where: 'device',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
aiChanMode: {
|
aiChanMode: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: false,
|
||||||
|
Loading…
Reference in New Issue
Block a user