Merge branch 'develop' into system-accounts
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@analytics/google-analytics": "1.1.0",
|
||||
"@discordapp/twemoji": "15.1.0",
|
||||
"@github/webauthn-json": "2.1.1",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
@@ -29,11 +30,12 @@
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
|
||||
"analytics": "0.8.16",
|
||||
"astring": "1.9.0",
|
||||
"broadcast-channel": "7.0.0",
|
||||
"buraha": "0.0.1",
|
||||
"canvas-confetti": "1.9.3",
|
||||
"chart.js": "4.4.7",
|
||||
"chart.js": "4.4.8",
|
||||
"chartjs-adapter-date-fns": "3.0.0",
|
||||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
@@ -56,10 +58,10 @@
|
||||
"misskey-reversi": "workspace:*",
|
||||
"photoswipe": "5.4.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.34.7",
|
||||
"rollup": "4.34.8",
|
||||
"sanitize-html": "2.14.0",
|
||||
"sass": "1.85.0",
|
||||
"shiki": "2.4.1",
|
||||
"shiki": "3.0.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.173.0",
|
||||
@@ -68,47 +70,47 @@
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.7.3",
|
||||
"uuid": "11.0.5",
|
||||
"uuid": "11.1.0",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "6.1.0",
|
||||
"vite": "6.1.1",
|
||||
"vue": "3.5.13",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.2.0",
|
||||
"@storybook/addon-actions": "8.5.6",
|
||||
"@storybook/addon-essentials": "8.5.6",
|
||||
"@storybook/addon-interactions": "8.5.6",
|
||||
"@storybook/addon-links": "8.5.6",
|
||||
"@storybook/addon-mdx-gfm": "8.5.6",
|
||||
"@storybook/addon-storysource": "8.5.6",
|
||||
"@storybook/blocks": "8.5.6",
|
||||
"@storybook/components": "8.5.6",
|
||||
"@storybook/core-events": "8.5.6",
|
||||
"@storybook/manager-api": "8.5.6",
|
||||
"@storybook/preview-api": "8.5.6",
|
||||
"@storybook/react": "8.5.6",
|
||||
"@storybook/react-vite": "8.5.6",
|
||||
"@storybook/test": "8.5.6",
|
||||
"@storybook/theming": "8.5.6",
|
||||
"@storybook/types": "8.5.6",
|
||||
"@storybook/vue3": "8.5.6",
|
||||
"@storybook/vue3-vite": "8.5.6",
|
||||
"@storybook/addon-actions": "8.5.8",
|
||||
"@storybook/addon-essentials": "8.5.8",
|
||||
"@storybook/addon-interactions": "8.5.8",
|
||||
"@storybook/addon-links": "8.5.8",
|
||||
"@storybook/addon-mdx-gfm": "8.5.8",
|
||||
"@storybook/addon-storysource": "8.5.8",
|
||||
"@storybook/blocks": "8.5.8",
|
||||
"@storybook/components": "8.5.8",
|
||||
"@storybook/core-events": "8.5.8",
|
||||
"@storybook/manager-api": "8.5.8",
|
||||
"@storybook/preview-api": "8.5.8",
|
||||
"@storybook/react": "8.5.8",
|
||||
"@storybook/react-vite": "8.5.8",
|
||||
"@storybook/test": "8.5.8",
|
||||
"@storybook/theming": "8.5.8",
|
||||
"@storybook/types": "8.5.8",
|
||||
"@storybook/vue3": "8.5.8",
|
||||
"@storybook/vue3-vite": "8.5.8",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
"@types/estree": "1.0.6",
|
||||
"@types/matter-js": "0.19.8",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "22.13.4",
|
||||
"@types/node": "22.13.5",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.5.14",
|
||||
"@typescript-eslint/eslint-plugin": "8.24.0",
|
||||
"@typescript-eslint/parser": "8.24.0",
|
||||
"@vitest/coverage-v8": "3.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "8.24.1",
|
||||
"@typescript-eslint/parser": "8.24.1",
|
||||
"@vitest/coverage-v8": "3.0.6",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"acorn": "8.14.0",
|
||||
"cross-env": "7.0.3",
|
||||
@@ -116,24 +118,24 @@
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"fast-glob": "3.3.3",
|
||||
"happy-dom": "17.1.0",
|
||||
"happy-dom": "17.1.4",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.7.0",
|
||||
"msw": "2.7.1",
|
||||
"msw-storybook-addon": "2.0.4",
|
||||
"nodemon": "3.1.9",
|
||||
"prettier": "3.5.1",
|
||||
"prettier": "3.5.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "2.0.10",
|
||||
"storybook": "8.5.6",
|
||||
"storybook": "8.5.8",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "3.0.5",
|
||||
"vitest": "3.0.6",
|
||||
"vitest-fetch-mock": "0.4.3",
|
||||
"vue-component-type-helpers": "2.2.2",
|
||||
"vue-component-type-helpers": "2.2.4",
|
||||
"vue-eslint-parser": "9.4.3",
|
||||
"vue-tsc": "2.2.2"
|
||||
"vue-tsc": "2.2.4"
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import { defineAsyncComponent, reactive, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { apiUrl } from '@@/js/config.js';
|
||||
import type { MenuItem, MenuButton } from '@/types/menu.js';
|
||||
import { defaultMemoryStorage } from '@/memory-storage';
|
||||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
@@ -40,6 +41,8 @@ export function incNotesCount() {
|
||||
export async function signout() {
|
||||
if (!$i) return;
|
||||
|
||||
defaultMemoryStorage.clear();
|
||||
|
||||
waiting();
|
||||
document.cookie.split(';').forEach((cookie) => {
|
||||
const cookieName = cookie.split('=')[0].trim();
|
||||
@@ -107,7 +110,7 @@ export async function removeAccount(idOrToken: Account['id']) {
|
||||
}
|
||||
|
||||
function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Account> {
|
||||
document.cookie = "token=; path=/; max-age=0";
|
||||
document.cookie = 'token=; path=/; max-age=0';
|
||||
document.cookie = `token=${token}; path=/queue; max-age=86400; SameSite=Strict; Secure`; // bull dashboardの認証とかで使う
|
||||
|
||||
return new Promise((done, fail) => {
|
||||
|
107
packages/frontend/src/analytics.ts
Normal file
107
packages/frontend/src/analytics.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { AnalyticsInstance, AnalyticsPlugin } from 'analytics';
|
||||
|
||||
/**
|
||||
* analytics moduleを読み込まなくても動作するようにするためのラッパー
|
||||
*/
|
||||
class AnalyticsProxy implements AnalyticsInstance {
|
||||
private analytics?: AnalyticsInstance;
|
||||
|
||||
constructor(analytics?: AnalyticsInstance) {
|
||||
if (analytics) {
|
||||
this.analytics = analytics;
|
||||
}
|
||||
}
|
||||
|
||||
public setAnalytics(analytics: AnalyticsInstance) {
|
||||
if (this.analytics) {
|
||||
throw new Error('Analytics instance already exists.');
|
||||
}
|
||||
this.analytics = analytics;
|
||||
}
|
||||
|
||||
public identify(...args: Parameters<AnalyticsInstance['identify']>) {
|
||||
return this.analytics?.identify(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public track(...args: Parameters<AnalyticsInstance['track']>) {
|
||||
return this.analytics?.track(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public page(...args: Parameters<AnalyticsInstance['page']>) {
|
||||
return this.analytics?.page(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public user(...args: Parameters<AnalyticsInstance['user']>) {
|
||||
return this.analytics?.user(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public reset(...args: Parameters<AnalyticsInstance['reset']>) {
|
||||
return this.analytics?.reset(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public ready(...args: Parameters<AnalyticsInstance['ready']>) {
|
||||
return this.analytics?.ready(...args) ?? function () { void 0; };
|
||||
}
|
||||
|
||||
public on(...args: Parameters<AnalyticsInstance['on']>) {
|
||||
return this.analytics?.on(...args) ?? function () { void 0; };
|
||||
}
|
||||
|
||||
public once(...args: Parameters<AnalyticsInstance['once']>) {
|
||||
return this.analytics?.once(...args) ?? function () { void 0; };
|
||||
}
|
||||
|
||||
public getState(...args: Parameters<AnalyticsInstance['getState']>) {
|
||||
return this.analytics?.getState(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public get storage() {
|
||||
return this.analytics?.storage ?? {
|
||||
getItem: () => null,
|
||||
setItem: () => void 0,
|
||||
removeItem: () => void 0,
|
||||
};
|
||||
}
|
||||
|
||||
public get plugins() {
|
||||
return this.analytics?.plugins ?? {
|
||||
enable: (p, c) => Promise.resolve(c ? c() : void 0),
|
||||
disable: (p, c) => Promise.resolve(c ? c() : void 0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const analytics = new AnalyticsProxy();
|
||||
|
||||
export async function initAnalytics(instance: Misskey.entities.MetaDetailed) {
|
||||
// アナリティクスプロバイダに関する設定がひとつもない場合は、アナリティクスモジュールを読み込まない
|
||||
if (!instance.googleAnalyticsMeasurementId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { default: Analytics } = await import('analytics');
|
||||
const plugins: AnalyticsPlugin[] = [];
|
||||
|
||||
// Google Analytics
|
||||
if (instance.googleAnalyticsMeasurementId) {
|
||||
const { default: googleAnalytics } = await import('@analytics/google-analytics');
|
||||
|
||||
plugins.push(googleAnalytics({
|
||||
measurementIds: [instance.googleAnalyticsMeasurementId],
|
||||
debug: _DEV_,
|
||||
}));
|
||||
}
|
||||
|
||||
analytics.setAnalytics(Analytics({
|
||||
app: 'misskey',
|
||||
version: _VERSION_,
|
||||
debug: _DEV_,
|
||||
plugins,
|
||||
}));
|
||||
}
|
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { computed, watch, version as vueVersion } from 'vue';
|
||||
import type { App } from 'vue';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { version, lang, updateLocale, locale } from '@@/js/config.js';
|
||||
import type { App } from 'vue';
|
||||
import widgets from '@/widgets/index.js';
|
||||
import directives from '@/directives/index.js';
|
||||
import components from '@/components/index.js';
|
||||
@@ -21,6 +21,7 @@ import { reloadChannel } from '@/scripts/unison-reload.js';
|
||||
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { analytics, initAnalytics } from '@/analytics.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||
import { setupRouter } from '@/router/main.js';
|
||||
@@ -241,6 +242,19 @@ export async function common(createVue: () => App<Element>) {
|
||||
await fetchCustomEmojis();
|
||||
} catch (err) { /* empty */ }
|
||||
|
||||
// analytics
|
||||
fetchInstanceMetaPromise.then(async () => {
|
||||
await initAnalytics(instance);
|
||||
|
||||
if ($i) {
|
||||
analytics.identify($i.id);
|
||||
}
|
||||
|
||||
analytics.page({
|
||||
path: window.location.pathname,
|
||||
});
|
||||
});
|
||||
|
||||
const app = createVue();
|
||||
|
||||
setupRouter(app, createMainRouter);
|
||||
|
@@ -49,6 +49,7 @@ import sanitizeHtml from 'sanitize-html';
|
||||
import { emojilist, getEmojiName } from '@@/js/emojilist.js';
|
||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
|
||||
import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
|
||||
import type { EmojiDef } from '@/scripts/search-emoji.js';
|
||||
import contains from '@/scripts/contains.js';
|
||||
import { acct } from '@/filters/user.js';
|
||||
import * as os from '@/os.js';
|
||||
@@ -58,7 +59,6 @@ import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { searchEmoji } from '@/scripts/search-emoji.js';
|
||||
import type { EmojiDef } from '@/scripts/search-emoji.js';
|
||||
|
||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
||||
|
||||
@@ -198,8 +198,10 @@ function exec() {
|
||||
users.value = JSON.parse(cache);
|
||||
fetching.value = false;
|
||||
} else {
|
||||
const [username, host] = props.q.toString().split('@');
|
||||
misskeyApi('users/search-by-username-and-host', {
|
||||
username: props.q,
|
||||
username: username,
|
||||
host: host,
|
||||
limit: 10,
|
||||
detail: false,
|
||||
}).then(searchedUsers => {
|
||||
|
@@ -257,7 +257,7 @@ import type { Keymap } from '@/scripts/hotkey.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
initialTab: string;
|
||||
initialTab?: string;
|
||||
}>(), {
|
||||
initialTab: 'replies',
|
||||
});
|
||||
|
@@ -32,6 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import type { PageMetadata } from '@/scripts/page-metadata.js';
|
||||
import RouterView from '@/components/global/RouterView.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { popout as _popout } from '@/scripts/popout.js';
|
||||
@@ -39,11 +40,11 @@ import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { useScrollPositionManager } from '@/nirax.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import type { PageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { openingWindowsCount } from '@/os.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { useRouterFactory } from '@/router/supplier.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import { analytics } from '@/analytics.js';
|
||||
|
||||
const props = defineProps<{
|
||||
initialPath: string;
|
||||
@@ -99,6 +100,14 @@ windowRouter.addListener('replace', ctx => {
|
||||
history.value.push({ path: ctx.path, key: ctx.key });
|
||||
});
|
||||
|
||||
windowRouter.addListener('change', ctx => {
|
||||
console.log('windowRouter: change', ctx.path);
|
||||
analytics.page({
|
||||
path: ctx.path,
|
||||
title: ctx.path,
|
||||
});
|
||||
});
|
||||
|
||||
windowRouter.init();
|
||||
|
||||
provide('router', windowRouter);
|
||||
@@ -160,6 +169,11 @@ function popout() {
|
||||
useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter);
|
||||
|
||||
onMounted(() => {
|
||||
analytics.page({
|
||||
path: props.initialPath,
|
||||
title: props.initialPath,
|
||||
});
|
||||
|
||||
openingWindowsCount.value++;
|
||||
if (openingWindowsCount.value >= 3) {
|
||||
claimAchievement('open3windows');
|
||||
|
@@ -4,14 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a :class="$style.link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
|
||||
<div :class="$style.root"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a v-if="href" :class="$style.link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
defineProps<{
|
||||
href: string;
|
||||
href?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
57
packages/frontend/src/memory-storage.ts
Normal file
57
packages/frontend/src/memory-storage.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export type MemoryStorage = {
|
||||
has: (key: string) => boolean;
|
||||
getItem: <T>(key: string) => T | null;
|
||||
setItem: (key: string, value: unknown) => void;
|
||||
removeItem: (key: string) => void;
|
||||
clear: () => void;
|
||||
size: number;
|
||||
};
|
||||
|
||||
class MemoryStorageImpl implements MemoryStorage {
|
||||
private readonly storage: Map<string, unknown>;
|
||||
|
||||
constructor() {
|
||||
this.storage = new Map();
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this.storage.has(key);
|
||||
}
|
||||
|
||||
getItem<T>(key: string): T | null {
|
||||
return this.storage.has(key) ? this.storage.get(key) as T : null;
|
||||
}
|
||||
|
||||
setItem(key: string, value: unknown): void {
|
||||
this.storage.set(key, value);
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
this.storage.delete(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.storage.clear();
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.storage.size;
|
||||
}
|
||||
}
|
||||
|
||||
export function createMemoryStorage(): MemoryStorage {
|
||||
return new MemoryStorageImpl();
|
||||
}
|
||||
|
||||
/**
|
||||
* SessionStorageよりも更に短い期間でクリアされるストレージです
|
||||
* - ブラウザの再読み込みやタブの閉じると内容が揮発します
|
||||
* - このストレージは他のタブと共有されません
|
||||
* - アカウント切り替えやログアウトを行うと内容が揮発します
|
||||
*/
|
||||
export const defaultMemoryStorage: MemoryStorage = createMemoryStorage();
|
@@ -8,20 +8,34 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<FormSuspense :p="init">
|
||||
<MkFolder>
|
||||
<template #label>DeepL Translation</template>
|
||||
<div class="_gaps_m">
|
||||
<MkFolder>
|
||||
<template #label>Google Analytics<span class="_beta">{{ i18n.ts.beta }}</span></template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="deeplAuthKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>DeepL Auth Key</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="deeplIsPro">
|
||||
<template #label>Pro account</template>
|
||||
</MkSwitch>
|
||||
<MkButton primary @click="save_deepl">Save</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="googleAnalyticsMeasurementId">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Measurement ID</template>
|
||||
</MkInput>
|
||||
<MkButton primary @click="save_googleAnalytics">Save</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>DeepL Translation</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="deeplAuthKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>DeepL Auth Key</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="deeplIsPro">
|
||||
<template #label>Pro account</template>
|
||||
</MkSwitch>
|
||||
<MkButton primary @click="save_deepl">Save</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
@@ -44,10 +58,13 @@ import MkFolder from '@/components/MkFolder.vue';
|
||||
const deeplAuthKey = ref<string>('');
|
||||
const deeplIsPro = ref<boolean>(false);
|
||||
|
||||
const googleAnalyticsMeasurementId = ref<string>('');
|
||||
|
||||
async function init() {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
deeplAuthKey.value = meta.deeplAuthKey;
|
||||
deeplAuthKey.value = meta.deeplAuthKey ?? '';
|
||||
deeplIsPro.value = meta.deeplIsPro;
|
||||
googleAnalyticsMeasurementId.value = meta.googleAnalyticsMeasurementId ?? '';
|
||||
}
|
||||
|
||||
function save_deepl() {
|
||||
@@ -59,6 +76,14 @@ function save_deepl() {
|
||||
});
|
||||
}
|
||||
|
||||
function save_googleAnalytics() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
googleAnalyticsMeasurementId: googleAnalyticsMeasurementId.value,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
@@ -9,6 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps">
|
||||
<div :class="$style.inputs">
|
||||
<MkButton style="margin-left: auto" @click="resetQuery">{{ i18n.ts.reset }}</MkButton>
|
||||
</div>
|
||||
<div :class="$style.inputs">
|
||||
<MkSelect v-model="sort" style="flex: 1;">
|
||||
<template #label>{{ i18n.ts.sort }}</template>
|
||||
@@ -57,8 +60,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, shallowRef, ref } from 'vue';
|
||||
import { computed, shallowRef, ref, watchEffect } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import { defaultMemoryStorage } from '@/memory-storage';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
@@ -69,13 +74,22 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import { dateString } from '@/filters/date.js';
|
||||
|
||||
const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
type SearchQuery = {
|
||||
sort?: string;
|
||||
state?: string;
|
||||
origin?: string;
|
||||
username?: string;
|
||||
hostname?: string;
|
||||
};
|
||||
|
||||
const sort = ref('+createdAt');
|
||||
const state = ref('all');
|
||||
const origin = ref('local');
|
||||
const searchUsername = ref('');
|
||||
const searchHost = ref('');
|
||||
const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
const storedQuery = JSON.parse(defaultMemoryStorage.getItem('admin-users-query') ?? '{}') as SearchQuery;
|
||||
|
||||
const sort = ref(storedQuery.sort ?? '+createdAt');
|
||||
const state = ref(storedQuery.state ?? 'all');
|
||||
const origin = ref(storedQuery.origin ?? 'local');
|
||||
const searchUsername = ref(storedQuery.username ?? '');
|
||||
const searchHost = ref(storedQuery.hostname ?? '');
|
||||
const pagination = {
|
||||
endpoint: 'admin/show-users' as const,
|
||||
limit: 10,
|
||||
@@ -119,6 +133,14 @@ function show(user) {
|
||||
os.pageWindow(`/admin/user/${user.id}`);
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
sort.value = '+createdAt';
|
||||
state.value = 'all';
|
||||
origin.value = 'local';
|
||||
searchUsername.value = '';
|
||||
searchHost.value = '';
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
icon: 'ti ti-search',
|
||||
text: i18n.ts.search,
|
||||
@@ -137,6 +159,16 @@ const headerActions = computed(() => [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
watchEffect(() => {
|
||||
defaultMemoryStorage.setItem('admin-users-query', JSON.stringify({
|
||||
sort: sort.value,
|
||||
state: state.value,
|
||||
origin: origin.value,
|
||||
username: searchUsername.value,
|
||||
hostname: searchHost.value,
|
||||
}));
|
||||
});
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.users,
|
||||
icon: 'ti ti-users',
|
||||
|
@@ -63,6 +63,7 @@ import { dateString } from '@/filters/date.js';
|
||||
import MkClipPreview from '@/components/MkClipPreview.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { getAppearNote } from '@/scripts/get-appear-note.js';
|
||||
import { serverContext, assertServerContext } from '@/server-context.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
@@ -132,10 +133,11 @@ function fetchNote() {
|
||||
noteId: props.noteId,
|
||||
}).then(res => {
|
||||
note.value = res;
|
||||
const appearNote = getAppearNote(res);
|
||||
// 古いノートは被クリップ数をカウントしていないので、2023-10-01以前のものは強制的にnotes/clipsを叩く
|
||||
if (note.value.clippedCount > 0 || new Date(note.value.createdAt).getTime() < new Date('2023-10-01').getTime()) {
|
||||
if ((appearNote.clippedCount ?? 0) > 0 || new Date(appearNote.createdAt).getTime() < new Date('2023-10-01').getTime()) {
|
||||
misskeyApi('notes/clips', {
|
||||
noteId: note.value.id,
|
||||
noteId: appearNote.id,
|
||||
}).then((_clips) => {
|
||||
clips.value = _clips;
|
||||
});
|
||||
@@ -170,7 +172,7 @@ definePageMetadata(() => ({
|
||||
avatar: note.value.user,
|
||||
path: `/notes/${note.value.id}`,
|
||||
share: {
|
||||
title: i18n.tsx.noteOf({ user: note.value.user.name }),
|
||||
title: i18n.tsx.noteOf({ user: note.value.user.name ?? note.value.user.username }),
|
||||
text: note.value.text,
|
||||
},
|
||||
} : {},
|
||||
|
@@ -7,6 +7,7 @@ import { EventEmitter } from 'eventemitter3';
|
||||
import type { IRouter, Resolved, RouteDef, RouterEvent, RouterFlag } from '@/nirax.js';
|
||||
|
||||
import type { App, ShallowRef } from 'vue';
|
||||
import { analytics } from '@/analytics.js';
|
||||
|
||||
/**
|
||||
* {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。
|
||||
@@ -29,6 +30,14 @@ export function setupRouter(app: App, routerFactory: ((path: string) => IRouter)
|
||||
window.history.replaceState({ key: ctx.key }, '', ctx.path);
|
||||
});
|
||||
|
||||
mainRouter.addListener('change', ctx => {
|
||||
console.log('mainRouter: change', ctx.path);
|
||||
analytics.page({
|
||||
path: ctx.path,
|
||||
title: ctx.path,
|
||||
});
|
||||
});
|
||||
|
||||
mainRouter.init();
|
||||
|
||||
setMainRouter(mainRouter);
|
||||
|
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { nextTick, ref, defineAsyncComponent } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import getCaretCoordinates from 'textarea-caret';
|
||||
import { toASCII } from 'punycode.js';
|
||||
import type { Ref } from 'vue';
|
||||
import { popup } from '@/os.js';
|
||||
|
||||
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
|
||||
@@ -98,15 +98,21 @@ export class Autocomplete {
|
||||
|
||||
const isMention = mentionIndex !== -1;
|
||||
const isHashtag = hashtagIndex !== -1;
|
||||
const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam?.includes(' ');
|
||||
const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam.includes(' ');
|
||||
const isMfmTag = mfmTagIndex !== -1 && !isMfmParam;
|
||||
const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
|
||||
|
||||
let opened = false;
|
||||
|
||||
if (isMention && this.onlyType.includes('user')) {
|
||||
const username = text.substring(mentionIndex + 1);
|
||||
if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) {
|
||||
// ユーザのサジェスト中に@を入力すると、その位置から新たにユーザ名を取りなおそうとしてしまう
|
||||
// この動きはリモートユーザのサジェストを阻害するので、@を検知したらその位置よりも前の@を探し、
|
||||
// ホスト名を含むリモートのユーザ名を全て拾えるようにする
|
||||
const mentionIndexAlt = text.lastIndexOf('@', mentionIndex - 1);
|
||||
const username = mentionIndexAlt === -1
|
||||
? text.substring(mentionIndex + 1)
|
||||
: text.substring(mentionIndexAlt + 1);
|
||||
if (username !== '' && username.match(/^[a-zA-Z0-9_@.]+$/)) {
|
||||
this.open('user', username);
|
||||
opened = true;
|
||||
} else if (username === '') {
|
||||
|
@@ -54,10 +54,6 @@ export async function lookup(router?: Router) {
|
||||
title = i18n.ts._remoteLookupErrors._responseInvalid.title;
|
||||
text = i18n.ts._remoteLookupErrors._responseInvalid.description;
|
||||
break;
|
||||
case 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a':
|
||||
title = i18n.ts._remoteLookupErrors._responseInvalid.title;
|
||||
text = i18n.ts._remoteLookupErrors._responseInvalidIdHostNotMatch.description;
|
||||
break;
|
||||
case 'dc94d745-1262-4e63-a17d-fecaa57efc82':
|
||||
title = i18n.ts._remoteLookupErrors._noSuchObject.title;
|
||||
text = i18n.ts._remoteLookupErrors._noSuchObject.description;
|
||||
|
Reference in New Issue
Block a user