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:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -712,6 +712,7 @@ function removeVisibleUser(user) {
|
||||
|
||||
function clear() {
|
||||
text.value = '';
|
||||
cw.value = null;
|
||||
files.value = [];
|
||||
poll.value = null;
|
||||
quoteId.value = null;
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -110,7 +110,11 @@ onUnmounted(() => {
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bg {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ const paginator = markRaw(new Paginator('clips/list', {
|
||||
}));
|
||||
|
||||
const favoritesPaginator = markRaw(new Paginator('clips/my-favorites', {
|
||||
// ページネーションに対応していない
|
||||
noPaging: true,
|
||||
}));
|
||||
|
||||
async function create() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -234,7 +234,7 @@ export const PREF_DEF = definePreferences({
|
||||
default: false,
|
||||
},
|
||||
disableShowingAnimatedImages: {
|
||||
default: prefersReducedMotion,
|
||||
default: false,
|
||||
},
|
||||
emojiStyle: {
|
||||
default: 'twemoji', // twemoji / fluentEmoji / native
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user