1
0

Merge tag '2025.4.0' into merge/2025-03-24

# Conflicts:
#	.github/workflows/storybook.yml
#	locales/index.d.ts
#	package.json
#	packages/backend/src/models/json-schema/role.ts
#	packages/frontend/src/components/MkPageWindow.vue
#	packages/frontend/src/pages/admin/roles.editor.vue
#	packages/frontend/src/pages/admin/roles.vue
#	packages/frontend/src/pages/settings/preferences.vue
#	packages/frontend/src/pages/settings/privacy.vue
#	packages/frontend/src/pages/timeline.vue
#	packages/frontend/src/pref-migrate.ts
#	packages/frontend/src/ui/_common_/common.vue
#	packages/frontend/src/ui/deck.vue
#	packages/frontend/src/ui/universal.vue
#	packages/misskey-js/src/autogen/types.ts
This commit is contained in:
Hazelnoot
2025-04-13 13:04:57 -04:00
98 changed files with 1621 additions and 2087 deletions
@@ -4,6 +4,59 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
>
<div
v-if="drawerMenuShowing"
:class="$style.menuDrawerBg"
class="_modalBg"
@click="drawerMenuShowing = false"
@touchstart.passive="drawerMenuShowing = false"
></div>
</Transition>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''"
>
<div v-if="drawerMenuShowing" :class="$style.menuDrawer">
<XDrawerMenu/>
</div>
</Transition>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''"
>
<div
v-if="widgetsShowing"
:class="$style.widgetsDrawerBg"
class="_modalBg"
@click="widgetsShowing = false"
@touchstart.passive="widgetsShowing = false"
></div>
</Transition>
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveTo : ''"
>
<div v-if="widgetsShowing" :class="$style.widgetsDrawer">
<button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button>
<XWidgets/>
</div>
</Transition>
<component
:is="popup.component"
v-for="popup in popups"
@@ -61,11 +114,16 @@ import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { globalEvents } from '@/events.js';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import { store } from '@/store.js';
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
const SkOneko = defineAsyncComponent(() => import('@/components/SkOneko.vue'));
const XWidgets = defineAsyncComponent(() => import('./widgets.vue'));
const drawerMenuShowing = defineModel<boolean>('drawerMenuShowing');
const widgetsShowing = defineModel<boolean>('widgetsShowing');
const dev = _DEV_;
@@ -108,6 +166,50 @@ function getPointerEvents() {
</script>
<style lang="scss" module>
.transition_menuDrawerBg_enterActive,
.transition_menuDrawerBg_leaveActive {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_menuDrawerBg_enterFrom,
.transition_menuDrawerBg_leaveTo {
opacity: 0;
}
.transition_menuDrawer_enterActive,
.transition_menuDrawer_leaveActive {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_menuDrawer_enterFrom,
.transition_menuDrawer_leaveTo {
opacity: 0;
transform: translateX(-240px);
}
.transition_widgetsDrawerBg_enterActive,
.transition_widgetsDrawerBg_leaveActive {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_widgetsDrawerBg_enterFrom,
.transition_widgetsDrawerBg_leaveTo {
opacity: 0;
}
.transition_widgetsDrawer_enterActive,
.transition_widgetsDrawer_leaveActive {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_widgetsDrawer_enterFrom,
.transition_widgetsDrawer_leaveTo {
opacity: 0;
transform: translateX(-240px);
}
.transition_notification_move,
.transition_notification_enterActive,
.transition_notification_leaveActive {
@@ -122,6 +224,54 @@ function getPointerEvents() {
transform: translateX(-250px);
}
.menuDrawerBg {
z-index: 1001;
}
.menuDrawer {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
height: 100dvh;
width: 240px;
box-sizing: border-box;
contain: strict;
overflow: auto;
overscroll-behavior: contain;
background: var(--MI_THEME-navBg);
}
.widgetsDrawerBg {
z-index: 1001;
}
.widgetsDrawer {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
width: 310px;
height: 100dvh;
padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)) !important;
box-sizing: border-box;
overflow: auto;
overscroll-behavior: contain;
background: var(--MI_THEME-bg);
}
.widgetsCloseButton {
padding: 8px;
display: block;
margin: 0 auto;
}
@media (min-width: 370px) {
.widgetsCloseButton {
display: none;
}
}
.notifications {
position: fixed;
z-index: 3900000;
@@ -0,0 +1,144 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div ref="rootEl" :class="$style.root">
<button :class="$style.item" class="_button" @click="drawerMenuShowing = true">
<div :class="$style.itemInner">
<i :class="$style.itemIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
</div>
</button>
<button :class="$style.item" class="_button" @click="mainRouter.push('/')">
<div :class="$style.itemInner">
<i :class="$style.itemIcon" class="ti ti-home"></i>
</div>
</button>
<button :class="$style.item" class="_button" @click="mainRouter.push('/my/notifications')">
<div :class="$style.itemInner">
<i :class="$style.itemIcon" class="ti ti-bell"></i>
<span v-if="$i?.hasUnreadNotification" :class="$style.itemIndicator" class="_blink">
<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
</span>
</div>
</button>
<button :class="$style.item" class="_button" @click="widgetsShowing = true">
<div :class="$style.itemInner">
<i :class="$style.itemIcon" class="ti ti-apps"></i>
</div>
</button>
<button :class="[$style.item, $style.post]" class="_button" @click="os.post()">
<div :class="$style.itemInner">
<i :class="$style.itemIcon" class="ti ti-pencil"></i>
</div>
</button>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, useTemplateRef, watch } from 'vue';
import { $i } from '@/i.js';
import * as os from '@/os.js';
import { mainRouter } from '@/router.js';
import { navbarItemDef } from '@/navbar.js';
const drawerMenuShowing = defineModel<boolean>('drawerMenuShowing');
const widgetsShowing = defineModel<boolean>('widgetsShowing');
const rootEl = useTemplateRef('rootEl');
const menuIndicated = computed(() => {
for (const def in navbarItemDef) {
if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
if (navbarItemDef[def].indicated) return true;
}
return false;
});
const rootElHeight = ref(0);
watch(rootEl, () => {
if (rootEl.value) {
rootElHeight.value = rootEl.value.offsetHeight;
window.document.body.style.setProperty('--MI-minBottomSpacing', 'var(--MI-minBottomSpacingMobile)');
} else {
rootElHeight.value = 0;
window.document.body.style.setProperty('--MI-minBottomSpacing', '0px');
}
}, {
immediate: true,
});
</script>
<style lang="scss" module>
.root {
padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
grid-gap: 8px;
width: 100%;
box-sizing: border-box;
background: var(--MI_THEME-bg);
border-top: solid 0.5px var(--MI_THEME-divider);
}
.item {
&.post {
.itemInner {
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
color: var(--MI_THEME-fgOnAccent);
&:hover {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
&:active {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
}
}
.itemInner {
position: relative;
padding: 0;
aspect-ratio: 1;
width: 100%;
max-width: 50px;
margin: auto;
align-content: center;
border-radius: 100%;
background: var(--MI_THEME-panel);
color: var(--MI_THEME-fg);
&:hover {
background: var(--MI_THEME-panelHighlight);
}
&:active {
background: hsl(from var(--MI_THEME-panel) h s calc(l - 2));
}
}
.itemIcon {
font-size: 14px;
}
.itemIndicator {
position: absolute;
top: 0;
left: 0;
color: var(--MI_THEME-indicator);
font-size: 16px;
&:has(.itemIndicateValueIcon) {
animation: none;
font-size: 12px;
}
}
</style>
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA :class="$style.item" :activeClass="$style.active" to="/" exact>
<i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
</MkA>
<template v-for="item in menu">
<template v-for="item in prefer.r.menu.value">
<div v-if="item === '-'" :class="$style.divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, toRef } from 'vue';
import { computed, defineAsyncComponent } from 'vue';
import { openInstanceMenu } from './common.js';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
@@ -59,10 +59,9 @@ import { instance } from '@/instance.js';
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { $i } from '@/i.js';
const menu = toRef(prefer.s, 'menu');
const otherMenuItemIndicated = computed(() => {
for (const def in navbarItemDef) {
if (menu.value.includes(def)) continue;
if (prefer.r.menu.value.includes(def)) continue;
if (navbarItemDef[def].indicated) return true;
}
return false;
+2 -3
View File
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact>
<i :class="$style.itemIcon" class="ti ti-home ti-fw" style="viewTransitionName: navbar-homeIcon;"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
</MkA>
<template v-for="item in menu">
<template v-for="item in prefer.r.menu.value">
<div v-if="item === '-'" :class="$style.divider"></div>
<component
:is="navbarItemDef[item].to ? 'MkA' : 'button'"
@@ -120,10 +120,9 @@ const iconOnly = computed(() => {
return forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon');
});
const menu = computed(() => prefer.s.menu);
const otherMenuItemIndicated = computed(() => {
for (const def in navbarItemDef) {
if (menu.value.includes(def)) continue;
if (prefer.r.menu.value.includes(def)) continue;
if (navbarItemDef[def].indicated) return true;
}
return false;
@@ -0,0 +1,81 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<XWidgets :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/>
<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button>
<button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
</div>
</template>
<script lang="ts">
import { computed, ref } from 'vue';
const editMode = ref(false);
</script>
<script lang="ts" setup>
import XWidgets from '@/components/MkWidgets.vue';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
// null = 全てのウィジェットを表示
// left = place: leftだけを表示
// right = rightとnullを表示
place?: 'left' | null | 'right';
}>(), {
place: null,
});
const widgets = computed(() => {
if (props.place === null) return prefer.r.widgets.value;
if (props.place === 'left') return prefer.r.widgets.value.filter(w => w.place === 'left');
return prefer.r.widgets.value.filter(w => w.place !== 'left');
});
function addWidget(widget) {
prefer.commit('widgets', [{
...widget,
place: props.place,
}, ...prefer.s.widgets]);
}
function removeWidget(widget) {
prefer.commit('widgets', prefer.s.widgets.filter(w => w.id !== widget.id));
}
function updateWidget({ id, data }) {
prefer.commit('widgets', prefer.s.widgets.map(w => w.id === id ? {
...w,
data,
place: props.place,
} : w));
}
function updateWidgets(thisWidgets) {
if (props.place === null) {
prefer.commit('widgets', thisWidgets);
return;
}
if (props.place === 'left') {
prefer.commit('widgets', [
...thisWidgets.map(w => ({ ...w, place: 'left' })),
...prefer.s.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)),
]);
return;
}
prefer.commit('widgets', [
...prefer.s.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)),
...thisWidgets.map(w => ({ ...w, place: 'right' })),
]);
}
</script>
<style lang="scss" module>
.edit {
width: 100%;
}
</style>