mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-09 19:43:09 +02:00
enhance(frontend): ユーザーメニューでスイッチでユーザーリストに追加・削除できるように (#11439)
* メニューのトグルをいい感じにする * user list toggle! * add changelog * fix * stop
This commit is contained in:
parent
2b4c8c9e0f
commit
8a72a05958
11 changed files with 214 additions and 85 deletions
|
@ -18,6 +18,8 @@
|
||||||
- OAuth 2.0のサポート
|
- OAuth 2.0のサポート
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- メニューのスイッチの動作を改善
|
||||||
|
- Enhance: ユーザーメニューでスイッチでユーザーリストに追加・削除できるように
|
||||||
- Enhance: 自分が押したリアクションのデザインを改善
|
- Enhance: 自分が押したリアクションのデザインを改善
|
||||||
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
|
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
|
||||||
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
|
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
|
||||||
|
|
|
@ -35,9 +35,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
|
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
|
||||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<span v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||||
<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
|
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" />
|
||||||
</span>
|
<span :class="$style.switchText">{{ item.text }}</span>
|
||||||
|
</button>
|
||||||
<button v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
|
<button v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<span>{{ item.text }}</span>
|
<span>{{ item.text }}</span>
|
||||||
|
@ -63,8 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
||||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
|
import { MenuItem, InnerMenuItem, OuterMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
@ -145,17 +146,17 @@ function onItemMouseLeave(item) {
|
||||||
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
let childrenCache = new WeakMap();
|
let childrenCache = new WeakMap<MenuParent, OuterMenuItem[]>();
|
||||||
async function showChildren(item: MenuItem, ev: MouseEvent) {
|
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
||||||
const children = ref([]);
|
const children = ref<OuterMenuItem[]>([]);
|
||||||
if (childrenCache.has(item)) {
|
if (childrenCache.has(item)) {
|
||||||
children.value = childrenCache.get(item);
|
children.value = childrenCache.get(item)!;
|
||||||
} else {
|
} else {
|
||||||
if (typeof item.children === 'function') {
|
if (typeof item.children === 'function') {
|
||||||
children.value = [{
|
children.value = [{
|
||||||
type: 'pending',
|
type: 'pending',
|
||||||
}];
|
}];
|
||||||
item.children().then(x => {
|
Promise.resolve(item.children()).then(x => {
|
||||||
children.value = x;
|
children.value = x;
|
||||||
childrenCache.set(item, x);
|
childrenCache.set(item, x);
|
||||||
});
|
});
|
||||||
|
@ -191,6 +192,11 @@ function focusDown() {
|
||||||
focusNext(document.activeElement);
|
focusNext(document.activeElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchItem(item: MenuSwitch & { ref: any }) {
|
||||||
|
if (item.disabled) return;
|
||||||
|
item.ref = !item.ref;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.viaKeyboard) {
|
if (props.viaKeyboard) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
@ -357,6 +363,37 @@ onBeforeUnmount(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchDisabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchButton {
|
||||||
|
margin-left: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchText {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-top: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchInput {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -408,14 +408,16 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), ev).then(focus);
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
|
||||||
|
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function menu(viaKeyboard = false): void {
|
function menu(viaKeyboard = false): void {
|
||||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), menuButton.value, {
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
|
||||||
|
os.popupMenu(menu, menuButton.value, {
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
}).then(focus);
|
}).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clip() {
|
async function clip() {
|
||||||
|
|
|
@ -385,14 +385,16 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), ev).then(focus);
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
|
||||||
|
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function menu(viaKeyboard = false): void {
|
function menu(viaKeyboard = false): void {
|
||||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
|
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
|
||||||
|
os.popupMenu(menu, menuButton.value, {
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
}).then(focus);
|
}).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clip() {
|
async function clip() {
|
||||||
|
|
88
packages/frontend/src/components/MkSwitch.button.vue
Normal file
88
packages/frontend/src/components/MkSwitch.button.vue
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff"
|
||||||
|
:class="{
|
||||||
|
[$style.button]: true,
|
||||||
|
[$style.buttonChecked]: checked,
|
||||||
|
[$style.buttonDisabled]: props.disabled
|
||||||
|
}"
|
||||||
|
data-cy-switch-toggle
|
||||||
|
@click.prevent.stop="toggle"
|
||||||
|
>
|
||||||
|
<div :class="{ [$style.knob]: true, [$style.knobChecked]: checked }"></div>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, Ref } from 'vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
checked: boolean | Ref<boolean>;
|
||||||
|
disabled?: boolean;
|
||||||
|
}>(), {
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'toggle'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const checked = toRefs(props).checked;
|
||||||
|
const toggle = () => {
|
||||||
|
emit('toggle');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.button {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 32px;
|
||||||
|
height: 23px;
|
||||||
|
outline: none;
|
||||||
|
background: var(--switchOffBg);
|
||||||
|
background-clip: content-box;
|
||||||
|
border: solid 1px var(--switchOffBg);
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: inherit;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonChecked {
|
||||||
|
background-color: var(--switchOnBg) !important;
|
||||||
|
border-color: var(--switchOnBg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonDisabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knob {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:not(.knobChecked) {
|
||||||
|
left: 3px;
|
||||||
|
background: var(--switchOffFg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.knobChecked {
|
||||||
|
left: 12px;
|
||||||
|
background: var(--switchOnFg);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,9 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:class="$style.input"
|
:class="$style.input"
|
||||||
@keydown.enter="toggle"
|
@keydown.enter="toggle"
|
||||||
>
|
>
|
||||||
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle">
|
<XButton :checked="checked" :disabled="disabled" @toggle="toggle" />
|
||||||
<div :class="$style.knob"></div>
|
|
||||||
</span>
|
|
||||||
<span :class="$style.body">
|
<span :class="$style.body">
|
||||||
<!-- TODO: 無名slotの方は廃止 -->
|
<!-- TODO: 無名slotの方は廃止 -->
|
||||||
<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
|
<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
|
||||||
|
@ -25,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, Ref } from 'vue';
|
import { toRefs, Ref } from 'vue';
|
||||||
import { i18n } from '@/i18n';
|
import XButton from '@/components/MkSwitch.button.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean | Ref<boolean>;
|
modelValue: boolean | Ref<boolean>;
|
||||||
|
@ -36,7 +34,6 @@ const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', v: boolean): void;
|
(ev: 'update:modelValue', v: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let button = $shallowRef<HTMLElement>();
|
|
||||||
const checked = toRefs(props).modelValue;
|
const checked = toRefs(props).modelValue;
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
if (props.disabled) return;
|
if (props.disabled) return;
|
||||||
|
@ -66,17 +63,8 @@ const toggle = () => {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.checked {
|
//&.checked {
|
||||||
> .button {
|
//}
|
||||||
background-color: var(--switchOnBg) !important;
|
|
||||||
border-color: var(--switchOnBg) !important;
|
|
||||||
|
|
||||||
> .knob {
|
|
||||||
left: 12px;
|
|
||||||
background: var(--switchOnFg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
|
@ -86,36 +74,6 @@ const toggle = () => {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 32px;
|
|
||||||
height: 23px;
|
|
||||||
outline: none;
|
|
||||||
background: var(--switchOffBg);
|
|
||||||
background-clip: content-box;
|
|
||||||
border: solid 1px var(--switchOffBg);
|
|
||||||
border-radius: 999px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: inherit;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.knob {
|
|
||||||
position: absolute;
|
|
||||||
top: 3px;
|
|
||||||
left: 3px;
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
background: var(--switchOffFg);
|
|
||||||
border-radius: 999px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
|
|
@ -86,7 +86,8 @@ let top = $ref(0);
|
||||||
let left = $ref(0);
|
let left = $ref(0);
|
||||||
|
|
||||||
function showMenu(ev: MouseEvent) {
|
function showMenu(ev: MouseEvent) {
|
||||||
os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
|
const { menu, cleanup } = getUserMenu(user);
|
||||||
|
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
@ -214,7 +214,8 @@ const age = $computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function menu(ev) {
|
function menu(ev) {
|
||||||
os.popupMenu(getUserMenu(props.user, router), ev.currentTarget ?? ev.target);
|
const { menu, cleanup } = getUserMenu(props.user, router);
|
||||||
|
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parallaxLoop() {
|
function parallaxLoop() {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { defaultStore, 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';
|
import { clipsCache } from '@/cache';
|
||||||
|
import { MenuItem } from '@/types/menu';
|
||||||
|
|
||||||
export async function getNoteClipMenu(props: {
|
export async function getNoteClipMenu(props: {
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
|
@ -108,6 +109,8 @@ export function getNoteMenu(props: {
|
||||||
|
|
||||||
const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
|
const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
|
||||||
|
|
||||||
|
const cleanups = [] as (() => void)[];
|
||||||
|
|
||||||
function del(): void {
|
function del(): void {
|
||||||
os.confirm({
|
os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
@ -233,7 +236,7 @@ export function getNoteMenu(props: {
|
||||||
props.translation.value = res;
|
props.translation.value = res;
|
||||||
}
|
}
|
||||||
|
|
||||||
let menu;
|
let menu: MenuItem[];
|
||||||
if ($i) {
|
if ($i) {
|
||||||
const statePromise = os.api('notes/state', {
|
const statePromise = os.api('notes/state', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
|
@ -295,7 +298,7 @@ export function getNoteMenu(props: {
|
||||||
action: () => toggleFavorite(true),
|
action: () => toggleFavorite(true),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
type: 'parent',
|
type: 'parent' as const,
|
||||||
icon: 'ti ti-paperclip',
|
icon: 'ti ti-paperclip',
|
||||||
text: i18n.ts.clip,
|
text: i18n.ts.clip,
|
||||||
children: () => getNoteClipMenu(props),
|
children: () => getNoteClipMenu(props),
|
||||||
|
@ -318,15 +321,17 @@ export function getNoteMenu(props: {
|
||||||
text: i18n.ts.pin,
|
text: i18n.ts.pin,
|
||||||
action: () => togglePin(true),
|
action: () => togglePin(true),
|
||||||
} : undefined,
|
} : undefined,
|
||||||
appearNote.userId !== $i.id ? {
|
{
|
||||||
type: 'parent',
|
type: 'parent' as const,
|
||||||
icon: 'ti ti-user',
|
icon: 'ti ti-user',
|
||||||
text: i18n.ts.user,
|
text: i18n.ts.user,
|
||||||
children: async () => {
|
children: async () => {
|
||||||
const user = await os.api('users/show', { userId: appearNote.userId });
|
const user = appearNote.userId === $i?.id ? $i : await os.api('users/show', { userId: appearNote.userId });
|
||||||
return getUserMenu(user);
|
const { menu, cleanup } = getUserMenu(user);
|
||||||
|
cleanups.push(cleanup);
|
||||||
|
return menu;
|
||||||
},
|
},
|
||||||
} : undefined,
|
},
|
||||||
/*
|
/*
|
||||||
...($i.isModerator || $i.isAdmin ? [
|
...($i.isModerator || $i.isAdmin ? [
|
||||||
null,
|
null,
|
||||||
|
@ -411,5 +416,13 @@ export function getNoteMenu(props: {
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return menu;
|
const cleanup = () => {
|
||||||
|
if (_DEV_) console.log('note menu cleanup', cleanups);
|
||||||
|
cleanups.forEach(cleanup => cleanup());
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
menu,
|
||||||
|
cleanup,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from 'punycode';
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent, ref, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
|
@ -19,6 +19,8 @@ import { antennasCache, rolesCache, userListsCache } 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;
|
||||||
|
|
||||||
|
const cleanups = [] as (() => void)[];
|
||||||
|
|
||||||
async function toggleMute() {
|
async function toggleMute() {
|
||||||
if (user.isMuted) {
|
if (user.isMuted) {
|
||||||
os.apiWithDialog('mute/delete', {
|
os.apiWithDialog('mute/delete', {
|
||||||
|
@ -168,17 +170,32 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
||||||
text: i18n.ts.addToList,
|
text: i18n.ts.addToList,
|
||||||
children: async () => {
|
children: async () => {
|
||||||
const lists = await userListsCache.fetch(() => os.api('users/lists/list'));
|
const lists = await userListsCache.fetch(() => os.api('users/lists/list'));
|
||||||
|
return lists.map(list => {
|
||||||
|
const isListed = ref(list.userIds.includes(user.id));
|
||||||
|
cleanups.push(watch(isListed, () => {
|
||||||
|
if (isListed.value) {
|
||||||
|
os.apiWithDialog('users/lists/push', {
|
||||||
|
listId: list.id,
|
||||||
|
userId: user.id,
|
||||||
|
}).then(() => {
|
||||||
|
list.userIds.push(user.id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
os.apiWithDialog('users/lists/pull', {
|
||||||
|
listId: list.id,
|
||||||
|
userId: user.id,
|
||||||
|
}).then(() => {
|
||||||
|
list.userIds.splice(list.userIds.indexOf(user.id), 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
return lists.map(list => ({
|
return {
|
||||||
text: list.name,
|
type: 'switch',
|
||||||
action: async () => {
|
text: list.name,
|
||||||
await os.apiWithDialog('users/lists/push', {
|
ref: isListed,
|
||||||
listId: list.id,
|
};
|
||||||
userId: user.id,
|
});
|
||||||
});
|
|
||||||
userListsCache.delete();
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
type: 'parent',
|
type: 'parent',
|
||||||
|
@ -311,5 +328,13 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
||||||
}))]);
|
}))]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return menu;
|
const cleanup = () => {
|
||||||
|
if (_DEV_) console.log('user menu cleanup', cleanups);
|
||||||
|
cleanups.forEach(cleanup => cleanup());
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
menu,
|
||||||
|
cleanup,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export type MenuA = { type: 'a', href: string, target?: string, download?: strin
|
||||||
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
|
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
|
||||||
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
|
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
|
||||||
export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
|
export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
|
||||||
export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] };
|
export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] | (() => Promise<OuterMenuItem[]> | OuterMenuItem[]) };
|
||||||
|
|
||||||
export type MenuPending = { type: 'pending' };
|
export type MenuPending = { type: 'pending' };
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue