Compare commits

..

No commits in common. "8ea08e59a827ea701ccbb173fae8a1f547b8d8c1" and "16b9b2e73732e7f30e7eac9833914ba574d0ef09" have entirely different histories.

11 changed files with 85 additions and 214 deletions

View File

@ -18,8 +18,6 @@
- OAuth 2.0のサポート - OAuth 2.0のサポート
### Client ### Client
- メニューのスイッチの動作を改善
- Enhance: ユーザーメニューでスイッチでユーザーリストに追加・削除できるように
- Enhance: 自分が押したリアクションのデザインを改善 - Enhance: 自分が押したリアクションのデザインを改善
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正 - Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正 - Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正

View File

@ -35,10 +35,9 @@ 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>
<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)"> <span v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" /> <MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
<span :class="$style.switchText">{{ item.text }}</span> </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>
@ -64,8 +63,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 MkSwitchButton from '@/components/MkSwitch.button.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import { MenuItem, InnerMenuItem, OuterMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu'; import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@ -146,17 +145,17 @@ function onItemMouseLeave(item) {
if (childCloseTimer) window.clearTimeout(childCloseTimer); if (childCloseTimer) window.clearTimeout(childCloseTimer);
} }
let childrenCache = new WeakMap<MenuParent, OuterMenuItem[]>(); let childrenCache = new WeakMap();
async function showChildren(item: MenuParent, ev: MouseEvent) { async function showChildren(item: MenuItem, ev: MouseEvent) {
const children = ref<OuterMenuItem[]>([]); const children = ref([]);
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',
}]; }];
Promise.resolve(item.children()).then(x => { item.children().then(x => {
children.value = x; children.value = x;
childrenCache.set(item, x); childrenCache.set(item, x);
}); });
@ -192,11 +191,6 @@ 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(() => {
@ -363,37 +357,6 @@ 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;
} }

View File

@ -408,16 +408,14 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault(); ev.preventDefault();
react(); react();
} else { } else {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), ev).then(focus);
os.contextMenu(menu, ev).then(focus).finally(cleanup);
} }
} }
function menu(viaKeyboard = false): void { function menu(viaKeyboard = false): void {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), menuButton.value, {
os.popupMenu(menu, menuButton.value, {
viaKeyboard, viaKeyboard,
}).then(focus).finally(cleanup); }).then(focus);
} }
async function clip() { async function clip() {

View File

@ -385,16 +385,14 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault(); ev.preventDefault();
react(); react();
} else { } else {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }); os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), ev).then(focus);
os.contextMenu(menu, ev).then(focus).finally(cleanup);
} }
} }
function menu(viaKeyboard = false): void { function menu(viaKeyboard = false): void {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }); os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
os.popupMenu(menu, menuButton.value, {
viaKeyboard, viaKeyboard,
}).then(focus).finally(cleanup); }).then(focus);
} }
async function clip() { async function clip() {

View File

@ -1,88 +0,0 @@
<!--
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>

View File

@ -12,7 +12,9 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.input" :class="$style.input"
@keydown.enter="toggle" @keydown.enter="toggle"
> >
<XButton :checked="checked" :disabled="disabled" @toggle="toggle" /> <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="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>
@ -23,7 +25,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 XButton from '@/components/MkSwitch.button.vue'; import { i18n } from '@/i18n';
const props = defineProps<{ const props = defineProps<{
modelValue: boolean | Ref<boolean>; modelValue: boolean | Ref<boolean>;
@ -34,6 +36,7 @@ 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;
@ -63,8 +66,17 @@ 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 {
@ -74,6 +86,36 @@ 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;

View File

@ -86,8 +86,7 @@ let top = $ref(0);
let left = $ref(0); let left = $ref(0);
function showMenu(ev: MouseEvent) { function showMenu(ev: MouseEvent) {
const { menu, cleanup } = getUserMenu(user); os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
} }
onMounted(() => { onMounted(() => {

View File

@ -214,8 +214,7 @@ const age = $computed(() => {
}); });
function menu(ev) { function menu(ev) {
const { menu, cleanup } = getUserMenu(props.user, router); os.popupMenu(getUserMenu(props.user, router), ev.currentTarget ?? ev.target);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
} }
function parallaxLoop() { function parallaxLoop() {

View File

@ -16,7 +16,6 @@ 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;
@ -109,8 +108,6 @@ 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',
@ -236,7 +233,7 @@ export function getNoteMenu(props: {
props.translation.value = res; props.translation.value = res;
} }
let menu: MenuItem[]; let menu;
if ($i) { if ($i) {
const statePromise = os.api('notes/state', { const statePromise = os.api('notes/state', {
noteId: appearNote.id, noteId: appearNote.id,
@ -298,7 +295,7 @@ export function getNoteMenu(props: {
action: () => toggleFavorite(true), action: () => toggleFavorite(true),
}), }),
{ {
type: 'parent' as const, type: 'parent',
icon: 'ti ti-paperclip', icon: 'ti ti-paperclip',
text: i18n.ts.clip, text: i18n.ts.clip,
children: () => getNoteClipMenu(props), children: () => getNoteClipMenu(props),
@ -321,17 +318,15 @@ 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' as const, type: 'parent',
icon: 'ti ti-user', icon: 'ti ti-user',
text: i18n.ts.user, text: i18n.ts.user,
children: async () => { children: async () => {
const user = appearNote.userId === $i?.id ? $i : await os.api('users/show', { userId: appearNote.userId }); const user = await os.api('users/show', { userId: appearNote.userId });
const { menu, cleanup } = getUserMenu(user); return getUserMenu(user);
cleanups.push(cleanup);
return menu;
},
}, },
} : undefined,
/* /*
...($i.isModerator || $i.isAdmin ? [ ...($i.isModerator || $i.isAdmin ? [
null, null,
@ -416,13 +411,5 @@ export function getNoteMenu(props: {
}]); }]);
} }
const cleanup = () => { return menu;
if (_DEV_) console.log('note menu cleanup', cleanups);
cleanups.forEach(cleanup => cleanup());
};
return {
menu,
cleanup,
};
} }

View File

@ -4,7 +4,7 @@
*/ */
import { toUnicode } from 'punycode'; import { toUnicode } from 'punycode';
import { defineAsyncComponent, ref, watch } from 'vue'; import { defineAsyncComponent } 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,8 +19,6 @@ 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', {
@ -170,32 +168,17 @@ 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 { return lists.map(list => ({
type: 'switch',
text: list.name, text: list.name,
ref: isListed, action: async () => {
}; await os.apiWithDialog('users/lists/push', {
listId: list.id,
userId: user.id,
}); });
userListsCache.delete();
},
}));
}, },
}, { }, {
type: 'parent', type: 'parent',
@ -328,13 +311,5 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
}))]); }))]);
} }
const cleanup = () => { return menu;
if (_DEV_) console.log('user menu cleanup', cleanups);
cleanups.forEach(cleanup => cleanup());
};
return {
menu,
cleanup,
};
} }

View File

@ -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[] | (() => Promise<OuterMenuItem[]> | OuterMenuItem[]) }; export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] };
export type MenuPending = { type: 'pending' }; export type MenuPending = { type: 'pending' };