1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-22 05:45:32 +02:00

Merge branch 'develop' into copilot/add-user-mute-settings

This commit is contained in:
Sayamame-beans
2025-11-27 22:58:26 +09:00
committed by GitHub
68 changed files with 5524 additions and 5625 deletions

View File

@@ -24,12 +24,12 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.22.0",
"@syuilo/aiscript": "1.1.2",
"@sentry/vue": "10.26.0",
"@syuilo/aiscript": "1.2.0",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.22",
"@vitejs/plugin-vue": "6.0.2",
"@vue/compiler-sfc": "3.5.24",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.19",
"astring": "1.9.0",
@@ -41,7 +41,7 @@
"chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "13.3.3",
"chromatic": "13.3.4",
"compare-versions": "6.1.1",
"cropperjs": "2.1.0",
"date-fns": "4.1.0",
@@ -58,7 +58,7 @@
"json5": "2.2.3",
"magic-string": "0.30.21",
"matter-js": "0.20.0",
"mediabunny": "1.24.2",
"mediabunny": "1.25.0",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@@ -67,21 +67,21 @@
"punycode.js": "2.3.1",
"qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2",
"rollup": "4.52.5",
"rollup": "4.53.3",
"sanitize-html": "2.17.0",
"sass": "1.93.3",
"shiki": "3.14.0",
"sass": "1.94.1",
"shiki": "3.15.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.181.0",
"three": "0.181.2",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.3",
"v-code-diff": "1.13.1",
"vite": "7.1.11",
"vue": "3.5.22",
"vite": "7.2.2",
"vue": "3.5.24",
"vuedraggable": "next",
"wanakana": "5.3.1"
},
@@ -110,21 +110,21 @@
"@types/estree": "1.0.8",
"@types/matter-js": "0.20.2",
"@types/micromatch": "4.0.10",
"@types/node": "24.9.2",
"@types/node": "24.10.1",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/compiler-core": "3.5.22",
"@vue/runtime-core": "3.5.22",
"@vue/compiler-core": "3.5.24",
"@vue/runtime-core": "3.5.24",
"acorn": "8.15.0",
"cross-env": "10.1.0",
"cypress": "15.5.0",
"cypress": "15.6.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.5.1",
"fast-glob": "3.3.3",
@@ -132,9 +132,9 @@
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.1.1",
"msw": "2.11.6",
"msw": "2.12.2",
"msw-storybook-addon": "2.0.6",
"nodemon": "3.1.10",
"nodemon": "3.1.11",
"prettier": "3.6.2",
"react": "19.2.0",
"react-dom": "19.2.0",
@@ -147,8 +147,8 @@
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.1.2",
"vue-component-type-helpers": "3.1.4",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.1.2"
"vue-tsc": "3.1.4"
}
}

View File

@@ -40,29 +40,77 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string })
CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
LOCALE: values.STR(lang),
SERVER_URL: values.STR(url),
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
utils.assertString(title);
utils.assertString(text);
if (type != null) {
assertStringAndIsIn(type, DIALOG_TYPES);
'Mk:dialog': values.FN_NATIVE(async ([_title, _text, _type]) => {
let title: string | undefined = undefined;
let text: string | undefined = undefined;
let type: typeof DIALOG_TYPES[number] = 'info';
if (_title != null) {
if (utils.isString(_title)) {
title = _title.value;
} else {
utils.assertNull(_title);
}
}
if (_text != null) {
if (utils.isString(_text)) {
text = _text.value;
} else {
utils.assertNull(_text);
}
}
if (_type != null) {
if (utils.isString(_type)) {
assertStringAndIsIn(_type, DIALOG_TYPES);
type = _type.value;
} else {
utils.assertNull(_type);
}
}
await os.alert({
type: type ? type.value : 'info',
title: title.value,
text: text.value,
type,
title,
text,
});
return values.NULL;
}),
'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
utils.assertString(title);
utils.assertString(text);
if (type != null) {
assertStringAndIsIn(type, DIALOG_TYPES);
'Mk:confirm': values.FN_NATIVE(async ([_title, _text, _type]) => {
let title: string | undefined = undefined;
let text: string | undefined = undefined;
let type: typeof DIALOG_TYPES[number] = 'question';
if (_title != null) {
if (utils.isString(_title)) {
title = _title.value;
} else {
utils.assertNull(_title);
}
}
if (_text != null) {
if (utils.isString(_text)) {
text = _text.value;
} else {
utils.assertNull(_text);
}
}
if (_type != null) {
if (utils.isString(_type)) {
assertStringAndIsIn(_type, DIALOG_TYPES);
type = _type.value;
} else {
utils.assertNull(_type);
}
}
const confirm = await os.confirm({
type: type ? type.value : 'question',
title: title.value,
text: text.value,
type,
title,
text,
});
return confirm.canceled ? values.FALSE : values.TRUE;
}),
@@ -76,15 +124,23 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string })
if (ep.value.includes('://') || ep.value.includes('..')) {
throw new errors.AiScriptRuntimeError('invalid endpoint');
}
if (token) {
let actualToken: string | null = null;
if (token != null && !utils.isNull(token)) {
utils.assertString(token);
// バグがあればundefinedもあり得るため念のため
if (typeof token.value !== 'string') throw new Error('invalid token');
if (typeof token.value !== 'string') throw new errors.AiScriptRuntimeError('invalid token');
actualToken = token.value;
}
const actualToken: string | null = token?.value ?? opts.token ?? null;
if (actualToken == null) {
actualToken = opts.token ?? null;
}
if (param == null) {
throw new errors.AiScriptRuntimeError('expected param');
}
utils.assertObject(param);
return misskeyApi(ep.value as keyof Misskey.Endpoints, utils.valToJs(param) as object, actualToken).then(res => {
return utils.jsToVal(res);

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { computed, watch, version as vueVersion } from 'vue';
import { watch, version as vueVersion } from 'vue';
import { compareVersions } from 'compare-versions';
import { version, lang, apiUrl, isSafeMode } from '@@/js/config.js';
import defaultLightTheme from '@@/themes/l-light.json5';
@@ -15,11 +15,11 @@ import directives from '@/directives/index.js';
import components from '@/components/index.js';
import { applyTheme } from '@/theme.js';
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
import { updateI18n, i18n } from '@/i18n.js';
import { i18n } from '@/i18n.js';
import { refreshCurrentAccount, login } from '@/accounts.js';
import { store } from '@/store.js';
import { fetchInstance, instance } from '@/instance.js';
import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js';
import { updateDeviceKind } from '@/utility/device-kind.js';
import { reloadChannel } from '@/utility/unison-reload.js';
import { getUrlWithoutLoginId } from '@/utility/login-id.js';
import { getAccountFromId } from '@/utility/get-account-from-id.js';
@@ -109,13 +109,6 @@ export async function common(createVue: () => Promise<App<Element>>) {
else window.location.reload();
});
// If mobile, insert the viewport meta tag
if (['smartphone', 'tablet'].includes(deviceKind)) {
const viewport = window.document.getElementsByName('viewport').item(0);
viewport.setAttribute('content',
`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
}
//#region Set lang attr
const html = window.document.documentElement;
html.setAttribute('lang', lang);
@@ -160,8 +153,15 @@ export async function common(createVue: () => Promise<App<Element>>) {
});
//#endregion
if (!isSafeMode) {
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
}
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
// NOTE: この処理は必ずサーバーテーマ適用処理より後に来ること(二重applyTheme発火を防ぐため)
// see: https://github.com/misskey-dev/misskey/issues/16562
watch(store.r.darkMode, (darkMode) => {
const theme = (() => {
@@ -178,26 +178,17 @@ export async function common(createVue: () => Promise<App<Element>>) {
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
if (!isSafeMode) {
const darkTheme = prefer.model('darkTheme');
const lightTheme = prefer.model('lightTheme');
watch(darkTheme, (theme) => {
watch(prefer.r.darkTheme, (theme) => {
if (store.s.darkMode) {
applyTheme(theme ?? defaultDarkTheme);
}
});
watch(lightTheme, (theme) => {
watch(prefer.r.lightTheme, (theme) => {
if (!store.s.darkMode) {
applyTheme(theme ?? defaultLightTheme);
}
});
fetchInstanceMetaPromise.then(() => {
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
});
}
watch(prefer.r.overridedDeviceKind, (kind) => {

View File

@@ -102,6 +102,21 @@ async function onClick() {
await misskeyApi('following/delete', {
userId: props.user.id,
});
} else if (hasPendingFollowRequestFromYou.value) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.cancelFollowRequestConfirm({ name: props.user.name || props.user.username }),
});
if (canceled) {
wait.value = false;
return;
}
await misskeyApi('following/requests/cancel', {
userId: props.user.id,
});
hasPendingFollowRequestFromYou.value = false;
} else {
if (prefer.s.alwaysConfirmFollow) {
const { canceled } = await os.confirm({
@@ -115,41 +130,34 @@ async function onClick() {
}
}
if (hasPendingFollowRequestFromYou.value) {
await misskeyApi('following/requests/cancel', {
userId: props.user.id,
});
hasPendingFollowRequestFromYou.value = false;
} else {
await misskeyApi('following/create', {
userId: props.user.id,
withReplies: prefer.s.defaultFollowWithReplies,
});
emit('update:user', {
...props.user,
withReplies: prefer.s.defaultFollowWithReplies,
});
hasPendingFollowRequestFromYou.value = true;
await misskeyApi('following/create', {
userId: props.user.id,
withReplies: prefer.s.defaultFollowWithReplies,
});
emit('update:user', {
...props.user,
withReplies: prefer.s.defaultFollowWithReplies,
});
hasPendingFollowRequestFromYou.value = true;
if ($i == null) {
wait.value = false;
return;
}
if ($i == null) {
wait.value = false;
return;
}
claimAchievement('following1');
claimAchievement('following1');
if ($i.followingCount >= 10) {
claimAchievement('following10');
}
if ($i.followingCount >= 50) {
claimAchievement('following50');
}
if ($i.followingCount >= 100) {
claimAchievement('following100');
}
if ($i.followingCount >= 300) {
claimAchievement('following300');
}
if ($i.followingCount >= 10) {
claimAchievement('following10');
}
if ($i.followingCount >= 50) {
claimAchievement('following50');
}
if ($i.followingCount >= 100) {
claimAchievement('following100');
}
if ($i.followingCount >= 300) {
claimAchievement('following300');
}
}
} catch (err) {

View File

@@ -712,6 +712,7 @@ function removeVisibleUser(user) {
function clear() {
text.value = '';
cw.value = null;
files.value = [];
poll.value = null;
quoteId.value = null;

View File

@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<script lang="ts">
import { defineComponent, h, ref, watch } from 'vue';
import { Comment, defineComponent, h, ref, watch } from 'vue';
import MkRadio from './MkRadio.vue';
import type { VNode } from 'vue';
@@ -35,7 +35,7 @@ export default defineComponent({
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
// vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる)
options = options.filter(vnode => !(typeof vnode.type === 'symbol' && vnode.type.description === 'v-cmt' && vnode.children === 'v-if'));
options = options.filter(vnode => vnode.type !== Comment);
return () => h('div', {
class: [

View File

@@ -110,7 +110,11 @@ onUnmounted(() => {
<style lang="scss" module>
.root {
position: absolute;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.bg {

View File

@@ -63,6 +63,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, onDeactivated, onUnmounted, ref, watch, shallowRef, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import { utils } from '@syuilo/aiscript';
import { compareVersions } from 'compare-versions';
import { url } from '@@/js/config.js';
import type { Ref } from 'vue';
import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
@@ -190,11 +192,21 @@ function start() {
run();
}
function getIsLegacy(version: string | null): boolean {
if (version == null) return false;
try {
return compareVersions(version, '1.0.0') < 0;
} catch {
return false;
}
}
async function run() {
if (aiscript.value) aiscript.value.abort();
if (!flash.value) return;
const isLegacy = !flash.value.script.replaceAll(' ', '').startsWith('///@1.0.0');
const version = utils.getLangVersion(flash.value.script);
const isLegacy = version != null && getIsLegacy(version);
const { Interpreter, Parser, values } = isLegacy ? (await import('@syuilo/aiscript-0-19-0') as any) : await import('@syuilo/aiscript');

View File

@@ -63,14 +63,28 @@ function accept(user: Misskey.entities.UserLite) {
});
}
function reject(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
async function reject(user: Misskey.entities.UserLite) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.rejectFollowRequestConfirm({ name: user.name || user.username }),
});
if (canceled) return;
await os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
paginator.reload();
});
}
function cancel(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => {
async function cancel(user: Misskey.entities.UserLite) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.cancelFollowRequestConfirm({ name: user.name || user.username }),
});
if (canceled) return;
await os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => {
paginator.reload();
});
}

View File

@@ -43,6 +43,8 @@ const paginator = markRaw(new Paginator('clips/list', {
}));
const favoritesPaginator = markRaw(new Paginator('clips/my-favorites', {
// ページネーションに対応していない
noPaging: true,
}));
async function create() {

View File

@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { BroadcastChannel } from 'broadcast-channel';
import type { StorageProvider } from '@/preferences/manager.js';
import { cloudBackup } from '@/preferences/utility.js';
import { miLocalStorage } from '@/local-storage.js';
@@ -12,6 +13,7 @@ import { $i } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { TAB_ID } from '@/tab-id.js';
// クラウド同期用グループ名
const syncGroup = 'default';
const io: StorageProvider = {
@@ -26,7 +28,6 @@ const io: StorageProvider = {
save: (ctx) => {
miLocalStorage.setItem('preferences', JSON.stringify(ctx.profile));
miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`);
},
cloudGet: async (ctx) => {
@@ -99,33 +100,47 @@ const io: StorageProvider = {
export const prefer = new PreferencesManager(io, $i);
let latestSyncedAt = Date.now();
//#region タブ間同期
let latestPreferencesUpdate: {
tabId: string;
timestamp: number;
} | null = null;
function syncBetweenTabs() {
const latest = miLocalStorage.getItem('latestPreferencesUpdate');
if (latest == null) return;
const preferencesChannel = new BroadcastChannel<{
type: 'preferencesUpdate';
tabId: string;
timestamp: number;
}>('preferences');
const latestTab = latest.split('/')[0];
const latestAt = parseInt(latest.split('/')[1]);
if (latestTab === TAB_ID) return;
if (latestAt <= latestSyncedAt) return;
prefer.reloadProfile();
latestSyncedAt = Date.now();
if (_DEV_) console.log('prefer:synced');
}
window.setInterval(syncBetweenTabs, 5000);
window.document.addEventListener('visibilitychange', () => {
if (window.document.visibilityState === 'visible') {
syncBetweenTabs();
}
prefer.on('committed', () => {
latestPreferencesUpdate = {
tabId: TAB_ID,
timestamp: Date.now(),
};
preferencesChannel.postMessage({
type: 'preferencesUpdate',
tabId: TAB_ID,
timestamp: latestPreferencesUpdate.timestamp,
});
});
preferencesChannel.addEventListener('message', (msg) => {
if (msg.type === 'preferencesUpdate') {
if (msg.tabId === TAB_ID) return;
if (latestPreferencesUpdate != null) {
if (msg.timestamp <= latestPreferencesUpdate.timestamp) return;
}
prefer.reloadProfile();
if (_DEV_) console.log('prefer:received update from other tab');
latestPreferencesUpdate = {
tabId: msg.tabId,
timestamp: msg.timestamp,
};
}
});
//#endregion
//#region 定期クラウドバックアップ
let latestBackupAt = 0;
window.setInterval(() => {
@@ -138,6 +153,7 @@ window.setInterval(() => {
latestBackupAt = Date.now();
});
}, 1000 * 60 * 3);
//#endregion
if (_DEV_) {
(window as any).prefer = prefer;

View File

@@ -234,7 +234,7 @@ export const PREF_DEF = definePreferences({
default: false,
},
disableShowingAnimatedImages: {
default: prefersReducedMotion,
default: false,
},
emojiStyle: {
default: 'twemoji', // twemoji / fluentEmoji / native

View File

@@ -4,6 +4,7 @@
*/
import { computed, onUnmounted, ref, watch } from 'vue';
import { EventEmitter } from 'eventemitter3';
import { host, version } from '@@/js/config.js';
import { PREF_DEF } from './def.js';
import type { Ref, WritableComputedRef } from 'vue';
@@ -100,6 +101,14 @@ type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) =>
export type PreferencesDefinition = Record<string, PreferencesDefinitionRecord<any>>;
type PreferencesManagerEvents = {
'committed': <K extends keyof PREF>(ctx: {
key: K;
value: ValueOf<K>;
oldValue: ValueOf<K>;
}) => void;
};
export function definePreferences<T extends Record<string, unknown>>(x: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>
}): {
@@ -180,7 +189,7 @@ function normalizePreferences(preferences: PossiblyNonNormalizedPreferencesProfi
// TODO: PreferencesManagerForGuest のような非ログイン専用のクラスを分離すればthis.currentAccountのnullチェックやaccountがnullであるスコープのレコード挿入などが不要になり綺麗になるかもしれない
// と思ったけど操作アカウントが存在しない場合も考慮する現在の設計の方が汎用的かつ堅牢かもしれない
// NOTE: accountDependentな設定は初期状態であってもアカウントごとのスコープでレコードを作成しておかないと、サーバー同期する際に正しく動作しなくなる
export class PreferencesManager {
export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
private io: StorageProvider;
private currentAccount: { id: string } | null;
public profile: PreferencesProfile;
@@ -201,6 +210,8 @@ export class PreferencesManager {
};
constructor(io: StorageProvider, currentAccount: { id: string } | null) {
super();
this.io = io;
this.currentAccount = currentAccount;
@@ -246,6 +257,12 @@ export class PreferencesManager {
this.rewriteRawState(key, v);
this.emit('committed', {
key,
value: v,
oldValue: this.s[key],
});
const record = this.getMatchedRecordOf(key);
if (parseScope(record[0]).account == null && isAccountDependentKey(key) && currentAccount != null) {

View File

@@ -5,7 +5,5 @@
import { genId } from '@/utility/id.js';
// HMR有効時にバグか知らんけど複数回実行されるのでその対策
export const TAB_ID = window.sessionStorage.getItem('TAB_ID') ?? genId();
window.sessionStorage.setItem('TAB_ID', TAB_ID);
export const TAB_ID = genId();
if (_DEV_) console.log('TAB_ID', TAB_ID);

View File

@@ -158,6 +158,8 @@ export function applyTheme(theme: Theme, persist = true) {
// 様々な理由により startViewTransition は失敗することがある
// ref. https://github.com/misskey-dev/misskey/issues/16562
// FIXME: viewTransitonエラーはtry~catch貫通してそうな気配がする
console.error(err);
window.document.documentElement.classList.remove('_themeChanging_');

View File

@@ -4,40 +4,40 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="azykntjl">
<div class="body">
<div class="left">
<button v-click-anime class="item _button instance" @click="openInstanceMenu">
<img :src="instance.iconUrl ?? '/favicon.ico'" draggable="false"/>
<div :class="[$style.root, acrylic ? $style.acrylic : null]">
<div :class="$style.body">
<div :class="$style.left">
<button v-click-anime :class="[$style.item, $style.instance]" class="_button" @click="openInstanceMenu">
<img :class="$style.instanceIcon" :src="instance.iconUrl ?? '/favicon.ico'" draggable="false"/>
</button>
<MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact>
<i class="ti ti-home ti-fw"></i>
<MkA v-click-anime v-tooltip="i18n.ts.timeline" :class="$style.item" activeClass="active" to="/" exact>
<i :class="$style.itemIcon" class="ti ti-home ti-fw"></i>
</MkA>
<template v-for="item in menu">
<div v-if="item === '-'" class="divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i class="ti-fw" :class="navbarItemDef[item].icon"></i>
<span v-if="navbarItemDef[item].indicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
<div v-if="item === '-'" :class="$style.divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="_button" :class="$style.item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i :class="[$style.itemIcon, navbarItemDef[item].icon]" class="ti-fw"></i>
<span v-if="navbarItemDef[item].indicated" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</component>
</template>
<div class="divider"></div>
<div :class="$style.divider"></div>
<MkA v-if="$i && ($i.isAdmin || $i.isModerator)" v-click-anime v-tooltip="i18n.ts.controlPanel" class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null">
<i class="ti ti-dashboard ti-fw"></i>
<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i>
</MkA>
<button v-click-anime class="item _button" @click="more">
<i class="ti ti-dots ti-fw"></i>
<span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
<button v-click-anime :class="$style.item" class="_button" @click="more">
<i :class="$style.itemIcon" class="ti ti-dots ti-fw"></i>
<span v-if="otherNavItemIndicated" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</button>
</div>
<div class="right">
<MkA v-click-anime v-tooltip="i18n.ts.settings" class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null">
<i class="ti ti-settings ti-fw"></i>
<div :class="$style.right">
<MkA v-click-anime v-tooltip="i18n.ts.settings" :class="$style.item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null">
<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i>
</MkA>
<button v-if="$i" v-click-anime class="item _button account" @click="openAccountMenu">
<MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/>
<button v-if="$i" v-click-anime :class="[$style.item, $style.account]" class="_button" @click="openAccountMenu">
<MkAvatar :user="$i" :class="$style.avatar"/><MkAcct :class="$style.acct" :user="$i"/>
</button>
<div class="post" @click="os.post()">
<MkButton class="button" gradate full rounded>
<div :class="$style.post" @click="os.post()">
<MkButton :class="$style.postButton" gradate rounded>
<i class="ti ti-pencil ti-fw"></i>
</MkButton>
</div>
@@ -61,6 +61,10 @@ import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
const WINDOW_THRESHOLD = 1400;
const props = defineProps<{
acrylic?: boolean;
}>();
const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD);
const menu = ref(prefer.s.menu);
// const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
@@ -100,121 +104,140 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
.azykntjl {
$height: 60px;
$avatar-size: 32px;
$avatar-margin: 8px;
<style lang="scss" module>
.root {
--height: 60px;
position: sticky;
top: 0;
z-index: 1000;
width: 100%;
height: $height;
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
height: var(--height);
contain: strict;
background: var(--MI_THEME-navBg);
> .body {
max-width: 1380px;
margin: 0 auto;
display: flex;
> .right,
> .left {
> .item {
position: relative;
font-size: 0.9em;
display: inline-block;
padding: 0 12px;
line-height: $height;
> i,
> .avatar {
margin-right: 0;
}
> i {
left: 10px;
}
> .avatar {
width: $avatar-size;
height: $avatar-size;
vertical-align: middle;
}
> .indicator {
position: absolute;
top: 0;
left: 0;
color: var(--MI_THEME-navIndicator);
font-size: 8px;
}
&:hover {
text-decoration: none;
color: light-dark(hsl(from var(--MI_THEME-navFg) h s calc(l - 17)), hsl(from var(--MI_THEME-navFg) h s calc(l + 17)));
}
&.active {
color: var(--MI_THEME-navActive);
}
}
> .divider {
display: inline-block;
height: 16px;
margin: 0 10px;
border-right: solid 0.5px var(--MI_THEME-divider);
}
> .instance {
display: inline-block;
position: relative;
width: 56px;
height: 100%;
vertical-align: bottom;
> img {
display: inline-block;
width: 24px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
}
> .post {
display: inline-block;
> .button {
width: 40px;
height: 40px;
padding: 0;
min-width: 0;
}
}
> .account {
display: inline-flex;
align-items: center;
vertical-align: top;
margin-right: 8px;
> .acct {
margin-left: 8px;
}
}
}
> .right {
margin-left: auto;
}
&.acrylic {
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
}
}
.body {
max-width: 1380px;
margin: 0 auto;
display: flex;
overflow: auto;
overflow-y: clip;
white-space: nowrap;
}
.item {
position: relative;
font-size: 0.9em;
display: inline-block;
padding: 0 12px;
line-height: var(--height);
&:hover {
text-decoration: none;
color: light-dark(hsl(from var(--MI_THEME-navFg) h s calc(l - 17)), hsl(from var(--MI_THEME-navFg) h s calc(l + 17)));
}
&.active {
color: var(--MI_THEME-navActive);
}
}
.itemIcon {
margin-right: 0;
left: 10px;
}
.avatar {
margin-right: 0;
width: 32px;
height: 32px;
vertical-align: middle;
}
.acct {
margin-left: 8px;
@media (max-width: 1200px) {
display: none;
}
}
.indicator {
position: absolute;
top: 0;
left: 0;
color: var(--MI_THEME-navIndicator);
font-size: 8px;
}
.divider {
display: inline-block;
height: 16px;
margin: 0 10px;
border-right: solid 0.5px var(--MI_THEME-divider);
}
.instance {
display: inline-block;
position: relative;
width: 56px;
height: 100%;
vertical-align: bottom;
position: sticky;
top: 0;
left: 0;
z-index: 1;
}
.instanceIcon {
display: inline-block;
width: 24px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
.right {
display: flex;
align-items: center;
margin-left: auto;
position: sticky;
top: 0;
right: 0;
z-index: 1;
contain: content;
background: var(--MI_THEME-navBg);
}
.acrylic .right {
background: transparent;
}
.post {
display: inline-block;
margin-right: 8px;
}
.postButton {
width: 40px;
height: 40px;
padding: 0;
min-width: 0;
}
.account {
display: inline-flex;
align-items: center;
vertical-align: top;
margin-right: 8px;
}
</style>

View File

@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XSidebar v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'left'"/>
<div :class="[$style.main, { [$style.withWallpaper]: withWallpaper, [$style.withSidebarAndTitlebar]: !isMobile && prefer.r['deck.navbarPosition'].value === 'left' && prefer.r.showTitlebar.value }]" :style="{ backgroundImage: prefer.s['deck.wallpaper'] != null ? `url(${ prefer.s['deck.wallpaper'] })` : '' }">
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'"/>
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'" :acrylic="withWallpaper"/>
<XReloadSuggestion v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'bottom'"/>
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'bottom'" :acrylic="withWallpaper"/>
<XMobileFooterMenu v-if="isMobile" v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
</div>

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { computed, ref, shallowRef, watch } from 'vue';
import { computed, ref, shallowRef, watch, defineAsyncComponent } from 'vue';
import * as os from '@/os.js';
type TourStep = {
@@ -26,7 +26,7 @@ export function startTour(steps: TourStep[]) {
anchorElementRef.value = step.element;
});
const { dispose } = os.popup(await import('@/components/MkSpot.vue').then(x => x.default), {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSpot.vue')), {
title: titleRef,
description: descriptionRef,
anchorElement: anchorElementRef,