mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 15:34:13 +00: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 => {
 | 
				
			||||||
			return lists.map(list => ({
 | 
									const isListed = ref(list.userIds.includes(user.id));
 | 
				
			||||||
				text: list.name,
 | 
									cleanups.push(watch(isListed, () => {
 | 
				
			||||||
				action: async () => {
 | 
										if (isListed.value) {
 | 
				
			||||||
					await os.apiWithDialog('users/lists/push', {
 | 
											os.apiWithDialog('users/lists/push', {
 | 
				
			||||||
							listId: list.id,
 | 
												listId: list.id,
 | 
				
			||||||
							userId: user.id,
 | 
												userId: user.id,
 | 
				
			||||||
 | 
											}).then(() => {
 | 
				
			||||||
 | 
												list.userIds.push(user.id);
 | 
				
			||||||
						});
 | 
											});
 | 
				
			||||||
					userListsCache.delete();
 | 
										} else {
 | 
				
			||||||
				},
 | 
											os.apiWithDialog('users/lists/pull', {
 | 
				
			||||||
 | 
												listId: list.id,
 | 
				
			||||||
 | 
												userId: user.id,
 | 
				
			||||||
 | 
											}).then(() => {
 | 
				
			||||||
 | 
												list.userIds.splice(list.userIds.indexOf(user.id), 1);
 | 
				
			||||||
 | 
											});
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
				}));
 | 
									}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										type: 'switch',
 | 
				
			||||||
 | 
										text: list.name,
 | 
				
			||||||
 | 
										ref: isListed,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	}, {
 | 
						}, {
 | 
				
			||||||
		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…
	
	Add table
		
		Reference in a new issue