Merge branch 'develop' into system-accounts

This commit is contained in:
syuilo
2025-02-26 18:04:05 +09:00
74 changed files with 2513 additions and 1456 deletions

View File

@@ -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"
}
}

View File

@@ -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) => {

View 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,
}));
}

View File

@@ -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);

View File

@@ -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 => {

View File

@@ -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',
});

View File

@@ -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');

View File

@@ -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>

View 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();

View File

@@ -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(() => []);

View File

@@ -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',

View File

@@ -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,
},
} : {},

View File

@@ -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);

View File

@@ -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 === '') {

View File

@@ -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;