From 5059d4d7e1fc98c5a86ebe73997e21b8c9bda0f7 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 9 Jul 2023 01:59:44 +0200 Subject: [PATCH 1/4] refactor(backend): skip fetching notes when the data is same-origin (#11200) * refactor(backend): skip fetching notes when the data is same-origin * Update CHANGELOG.md * sentFrom --- CHANGELOG.md | 1 + .../core/activitypub/models/ApNoteService.ts | 10 +- .../activitypub/models/ApPersonService.ts | 11 +- packages/backend/test/misc/mock-resolver.ts | 19 ++- packages/backend/test/unit/activitypub.ts | 113 ++++++++++++++++-- 5 files changed, 131 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35162f6b0..c28d0b9bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように - 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用) - 全体的なDBクエリのパフォーマンスを向上 +- featuredノートのsignedGet回数を減らしました ## 13.13.2 diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 35865a819..d3359ef90 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -177,7 +177,7 @@ export class ApNoteService { // リプライ const reply: Note | null = note.inReplyTo - ? await this.resolveNote(note.inReplyTo, resolver) + ? await this.resolveNote(note.inReplyTo, { resolver }) .then(x => { if (x == null) { this.logger.warn('Specified inReplyTo, but not found'); @@ -293,9 +293,8 @@ export class ApNoteService { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolveNote(value: string | IObject, resolver?: Resolver): Promise { - const uri = typeof value === 'string' ? value : value.id; - if (uri == null) throw new Error('missing uri'); + public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise { + const uri = getApId(value); // ブロックしていたら中断 const meta = await this.metaService.fetch(); @@ -318,7 +317,8 @@ export class ApNoteService { // リモートサーバーからフェッチしてきて登録 // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 - return await this.createNote(uri, resolver, true); + const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri; + return await this.createNote(createFrom, options.resolver, true); } finally { unlock(); } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index e8b65b3d4..e89ee4632 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -260,7 +260,7 @@ export class ApPersonService implements OnModuleInit { // Create user let user: RemoteUser | null = null; try { - // Start transaction + // Start transaction await this.db.transaction(async transactionalEntityManager => { user = await transactionalEntityManager.save(new User({ id: this.idService.genId(), @@ -306,9 +306,9 @@ export class ApPersonService implements OnModuleInit { } }); } catch (e) { - // duplicate key error + // duplicate key error if (isDuplicateKeyValueError(e)) { - // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 + // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 const u = await this.usersRepository.findOneBy({ uri: person.id }); if (u == null) throw new Error('already registered'); @@ -604,7 +604,10 @@ export class ApPersonService implements OnModuleInit { const featuredNotes = await Promise.all(items .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも .slice(0, 5) - .map(item => limit(() => this.apNoteService.resolveNote(item, _resolver)))); + .map(item => limit(() => this.apNoteService.resolveNote(item, { + resolver: _resolver, + sentFrom: new URL(user.uri), + })))); await this.db.transaction(async transactionalEntityManager => { await transactionalEntityManager.delete(UserNotePining, { userId: user.id }); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index a7bcd859a..9dbe77a7c 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -18,7 +18,8 @@ type MockResponse = { }; export class MockResolver extends Resolver { - private _rs = new Map(); + #responseMap = new Map(); + #remoteGetTrials: string[] = []; constructor(loggerService: LoggerService) { super( @@ -38,18 +39,28 @@ export class MockResolver extends Resolver { ); } - public async _register(uri: string, content: string | Record, type = 'application/activity+json') { - this._rs.set(uri, { + public register(uri: string, content: string | Record, type = 'application/activity+json'): void { + this.#responseMap.set(uri, { type, content: typeof content === 'string' ? content : JSON.stringify(content), }); } + public clear(): void { + this.#responseMap.clear(); + this.#remoteGetTrials.length = 0; + } + + public remoteGetTrials(): string[] { + return this.#remoteGetTrials; + } + @bindThis public async resolve(value: string | IObject): Promise { if (typeof value !== 'string') return value; - const r = this._rs.get(value); + this.#remoteGetTrials.push(value); + const r = this.#responseMap.get(value); if (!r) { throw new Error('Not registed for mock'); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 7cd740a2f..02b900da9 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -11,16 +11,19 @@ import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import type { IActor } from '@/core/activitypub/type.js'; +import type { IActor, ICollection, IPost } from '@/core/activitypub/type.js'; import { Note } from '@/models/index.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { MockResolver } from '../misc/mock-resolver.js'; const host = 'https://host1.test'; -function createRandomActor(): IActor & { id: string } { +type NonTransientIActor = IActor & { id: string }; +type NonTransientIPost = IPost & { id: string }; + +function createRandomActor({ actorHost = host } = {}): NonTransientIActor { const preferredUsername = secureRndstr(8); - const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; + const actorId = `${actorHost}/users/${preferredUsername.toLowerCase()}`; return { '@context': 'https://www.w3.org/ns/activitystreams', @@ -32,13 +35,41 @@ function createRandomActor(): IActor & { id: string } { }; } +function createRandomNote(actor: NonTransientIActor): NonTransientIPost { + const id = secureRndstr(8); + const noteId = `${new URL(actor.id).origin}/notes/${id}`; + + return { + id: noteId, + type: 'Note', + attributedTo: actor.id, + content: 'test test foo', + }; +} + +function createRandomNotes(actor: NonTransientIActor, length: number): NonTransientIPost[] { + return new Array(length).fill(null).map(() => createRandomNote(actor)); +} + +function createRandomFeaturedCollection(actor: NonTransientIActor, length: number): ICollection { + const items = createRandomNotes(actor, length); + + return { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: actor.outbox as string, + totalItems: items.length, + items, + }; +} + describe('ActivityPub', () => { let noteService: ApNoteService; let personService: ApPersonService; let rendererService: ApRendererService; let resolver: MockResolver; - beforeEach(async () => { + beforeAll(async () => { const app = await Test.createTestingModule({ imports: [GlobalModule, CoreModule], }).compile(); @@ -53,7 +84,11 @@ describe('ActivityPub', () => { // Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error const federatedInstanceService = app.get(FederatedInstanceService); - jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => {})); + jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => { })); + }); + + beforeEach(() => { + resolver.clear(); }); describe('Parse minimum object', () => { @@ -69,7 +104,7 @@ describe('ActivityPub', () => { }; test('Minimum Actor', async () => { - resolver._register(actor.id, actor); + resolver.register(actor.id, actor); const user = await personService.createPerson(actor.id, resolver); @@ -79,8 +114,8 @@ describe('ActivityPub', () => { }); test('Minimum Note', async () => { - resolver._register(actor.id, actor); - resolver._register(post.id, post); + resolver.register(actor.id, actor); + resolver.register(post.id, post); const note = await noteService.createNote(post.id, resolver, true); @@ -97,7 +132,7 @@ describe('ActivityPub', () => { name: secureRndstr(129), }; - resolver._register(actor.id, actor); + resolver.register(actor.id, actor); const user = await personService.createPerson(actor.id, resolver); @@ -110,7 +145,7 @@ describe('ActivityPub', () => { name: '', }; - resolver._register(actor.id, actor); + resolver.register(actor.id, actor); const user = await personService.createPerson(actor.id, resolver); @@ -126,4 +161,62 @@ describe('ActivityPub', () => { } as Note); }); }); + + describe('Featured', () => { + test('Fetch featured notes from IActor', async () => { + const actor = createRandomActor(); + actor.featured = `${actor.id}/collections/featured`; + + const featured = createRandomFeaturedCollection(actor, 5); + + resolver.register(actor.id, actor); + resolver.register(actor.featured, featured); + + await personService.createPerson(actor.id, resolver); + + // All notes in `featured` are same-origin, no need to fetch notes again + assert.deepStrictEqual(resolver.remoteGetTrials(), [actor.id, actor.featured]); + + // Created notes without resolving anything + for (const item of featured.items as IPost[]) { + const note = await noteService.fetchNote(item); + assert.ok(note); + assert.strictEqual(note.text, 'test test foo'); + assert.strictEqual(note.uri, item.id); + } + }); + + test('Fetch featured notes from IActor pointing to another remote server', async () => { + const actor1 = createRandomActor(); + actor1.featured = `${actor1.id}/collections/featured`; + const actor2 = createRandomActor({ actorHost: 'https://host2.test' }); + + const actor2Note = createRandomNote(actor2); + const featured = createRandomFeaturedCollection(actor1, 0); + (featured.items as IPost[]).push({ + ...actor2Note, + content: 'test test bar', // fraud! + }); + + resolver.register(actor1.id, actor1); + resolver.register(actor1.featured, featured); + resolver.register(actor2.id, actor2); + resolver.register(actor2Note.id, actor2Note); + + await personService.createPerson(actor1.id, resolver); + + // actor2Note is from a different server and needs to be fetched again + assert.deepStrictEqual( + resolver.remoteGetTrials(), + [actor1.id, actor1.featured, actor2Note.id, actor2.id], + ); + + const note = await noteService.fetchNote(actor2Note.id); + assert.ok(note); + + // Reflects the original content instead of the fraud + assert.strictEqual(note.text, 'test test foo'); + assert.strictEqual(note.uri, actor2Note.id); + }); + }); }); From 59046d583dc1ef216760b83012ef9d876407ae38 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 9 Jul 2023 10:19:07 +0200 Subject: [PATCH 2/4] refactor(locales, sw): use es module (#11204) * refactor(locales): use es module * fix sw build * fix gulp * try fixing storybook * Revert "try fixing storybook" This reverts commit 5f2a4eee016776381a7d80407e28d129c252228f. * try fixing storybook 2 * Update main.ts * Update build.js * Update main.ts * Update changes.ts * fix sw lint * Update build.js --- gulpfile.js => gulpfile.mjs | 14 +++++++------- locales/generateDTS.js | 8 ++++---- locales/index.js | 8 ++++---- locales/package.json | 3 +++ packages/frontend/.storybook/changes.ts | 9 ++++++--- packages/frontend/.storybook/main.ts | 9 +++++++-- packages/frontend/.storybook/package.json | 3 +++ packages/frontend/.storybook/preload-locale.ts | 7 +++---- packages/frontend/.storybook/preload-theme.ts | 7 +++---- packages/frontend/.storybook/preview.ts | 8 ++++---- packages/frontend/.storybook/tsconfig.json | 2 ++ packages/sw/{.eslintrc.js => .eslintrc.cjs} | 0 packages/sw/build.js | 9 ++++++--- packages/sw/package.json | 3 ++- 14 files changed, 54 insertions(+), 36 deletions(-) rename gulpfile.js => gulpfile.mjs (88%) create mode 100644 locales/package.json create mode 100644 packages/frontend/.storybook/package.json rename packages/sw/{.eslintrc.js => .eslintrc.cjs} (100%) diff --git a/gulpfile.js b/gulpfile.mjs similarity index 88% rename from gulpfile.js rename to gulpfile.mjs index 6507aad60..9556eb795 100644 --- a/gulpfile.js +++ b/gulpfile.mjs @@ -2,14 +2,14 @@ * Gulp tasks */ -const fs = require('fs'); -const gulp = require('gulp'); -const replace = require('gulp-replace'); -const terser = require('gulp-terser'); -const cssnano = require('gulp-cssnano'); +import * as fs from 'node:fs'; +import gulp from 'gulp'; +import replace from 'gulp-replace'; +import terser from 'gulp-terser'; +import cssnano from 'gulp-cssnano'; -const locales = require('./locales'); -const meta = require('./package.json'); +import locales from './locales/index.js'; +import meta from './package.json' assert { type: "json" }; gulp.task('copy:backend:views', () => gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views')) diff --git a/locales/generateDTS.js b/locales/generateDTS.js index 5949aee7c..bc9827632 100644 --- a/locales/generateDTS.js +++ b/locales/generateDTS.js @@ -1,6 +1,6 @@ -const fs = require('fs'); -const yaml = require('js-yaml'); -const ts = require('typescript'); +import * as fs from 'node:fs'; +import * as yaml from 'js-yaml'; +import * as ts from 'typescript'; function createMembers(record) { return Object.entries(record) @@ -14,7 +14,7 @@ function createMembers(record) { )); } -module.exports = function generateDTS() { +export default function generateDTS() { const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8')); const members = createMembers(locale); const elements = [ diff --git a/locales/index.js b/locales/index.js index 2248bb6ac..7801f1275 100644 --- a/locales/index.js +++ b/locales/index.js @@ -2,8 +2,8 @@ * Languages Loader */ -const fs = require('fs'); -const yaml = require('js-yaml'); +import * as fs from 'node:fs'; +import * as yaml from 'js-yaml'; const merge = (...args) => args.reduce((a, c) => ({ ...a, @@ -51,9 +51,9 @@ const primaries = { // 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); -const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {}); +const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); -module.exports = Object.entries(locales) +export default Object.entries(locales) .reduce((a, [k ,v]) => (a[k] = (() => { const [lang] = k.split('-'); switch (k) { diff --git a/locales/package.json b/locales/package.json new file mode 100644 index 000000000..bedb411a9 --- /dev/null +++ b/locales/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/frontend/.storybook/changes.ts b/packages/frontend/.storybook/changes.ts index fc0f0c286..a1275132b 100644 --- a/packages/frontend/.storybook/changes.ts +++ b/packages/frontend/.storybook/changes.ts @@ -1,7 +1,10 @@ import fs from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; import path from 'node:path'; import micromatch from 'micromatch'; -import main from './main'; +import main from './main.js'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); interface Stats { readonly modules: readonly { @@ -13,8 +16,8 @@ interface Stats { }[]; } -fs.readFile( - path.resolve(__dirname, '../storybook-static/preview-stats.json') +await fs.readFile( + new URL('../storybook-static/preview-stats.json', import.meta.url) ).then((buffer) => { const stats: Stats = JSON.parse(buffer.toString()); const keys = new Set(stats.modules.map((stat) => stat.id)); diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts index 1d0ce5ab6..b64979980 100644 --- a/packages/frontend/.storybook/main.ts +++ b/packages/frontend/.storybook/main.ts @@ -1,7 +1,11 @@ import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import type { StorybookConfig } from '@storybook/vue3-vite'; import { type Plugin, mergeConfig } from 'vite'; import turbosnap from 'vite-plugin-turbosnap'; + +const dirname = fileURLToPath(new URL('.', import.meta.url)); + const config = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ @@ -9,7 +13,7 @@ const config = { '@storybook/addon-interactions', '@storybook/addon-links', '@storybook/addon-storysource', - resolve(__dirname, '../node_modules/storybook-addon-misskey-theme'), + resolve(dirname, '../node_modules/storybook-addon-misskey-theme'), ], framework: { name: '@storybook/vue3-vite', @@ -28,7 +32,8 @@ const config = { } return mergeConfig(config, { plugins: [ - turbosnap({ + // XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8 + (turbosnap as any as typeof turbosnap['default'])({ rootDir: config.root ?? process.cwd(), }), ], diff --git a/packages/frontend/.storybook/package.json b/packages/frontend/.storybook/package.json new file mode 100644 index 000000000..bedb411a9 --- /dev/null +++ b/packages/frontend/.storybook/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/frontend/.storybook/preload-locale.ts b/packages/frontend/.storybook/preload-locale.ts index a54164742..636931967 100644 --- a/packages/frontend/.storybook/preload-locale.ts +++ b/packages/frontend/.storybook/preload-locale.ts @@ -1,9 +1,8 @@ import { writeFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import * as locales from '../../../locales'; +import * as locales from '../../../locales/index.js'; -writeFile( - resolve(__dirname, 'locale.ts'), +await writeFile( + new URL('locale.ts', import.meta.url), `export default ${JSON.stringify(locales['ja-JP'], undefined, 2)} as const;`, 'utf8', ) diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts index 1ff8f71ec..42fbeff73 100644 --- a/packages/frontend/.storybook/preload-theme.ts +++ b/packages/frontend/.storybook/preload-theme.ts @@ -1,6 +1,5 @@ import { readFile, writeFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import * as JSON5 from 'json5'; +import JSON5 from 'json5'; const keys = [ '_dark', @@ -26,9 +25,9 @@ const keys = [ 'd-u0', ] -Promise.all(keys.map((key) => readFile(resolve(__dirname, `../src/themes/${key}.json5`), 'utf8'))).then((sources) => { +await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { writeFile( - resolve(__dirname, './themes.ts'), + new URL('./themes.ts', import.meta.url), `export default ${JSON.stringify( Object.fromEntries(sources.map((source, i) => [keys[i], JSON5.parse(source)])), undefined, diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts index e887acaa2..67c81c666 100644 --- a/packages/frontend/.storybook/preview.ts +++ b/packages/frontend/.storybook/preview.ts @@ -3,10 +3,10 @@ import { FORCE_REMOUNT } from '@storybook/core-events'; import { type Preview, setup } from '@storybook/vue3'; import isChromatic from 'chromatic/isChromatic'; import { initialize, mswDecorator } from 'msw-storybook-addon'; -import { userDetailed } from './fakes'; -import locale from './locale'; -import { commonHandlers, onUnhandledRequest } from './mocks'; -import themes from './themes'; +import { userDetailed } from './fakes.js'; +import locale from './locale.js'; +import { commonHandlers, onUnhandledRequest } from './mocks.js'; +import themes from './themes.js'; import '../src/style.scss'; const appInitialized = Symbol(); diff --git a/packages/frontend/.storybook/tsconfig.json b/packages/frontend/.storybook/tsconfig.json index 2db2f1eab..02465f5af 100644 --- a/packages/frontend/.storybook/tsconfig.json +++ b/packages/frontend/.storybook/tsconfig.json @@ -1,5 +1,7 @@ { "compilerOptions": { + "target": "es2022", + "module": "Node16", "strict": true, "allowUnusedLabels": false, "allowUnreachableCode": false, diff --git a/packages/sw/.eslintrc.js b/packages/sw/.eslintrc.cjs similarity index 100% rename from packages/sw/.eslintrc.js rename to packages/sw/.eslintrc.cjs diff --git a/packages/sw/build.js b/packages/sw/build.js index ad16fb949..e92619764 100644 --- a/packages/sw/build.js +++ b/packages/sw/build.js @@ -1,10 +1,13 @@ // @ts-check -const esbuild = require('esbuild'); -const locales = require('../../locales'); -const meta = require('../../package.json'); +import { fileURLToPath } from 'node:url'; +import * as esbuild from 'esbuild'; +import locales from '../../locales/index.js'; +import meta from '../../package.json' assert { type: "json" }; const watch = process.argv[2]?.includes('watch'); +const __dirname = fileURLToPath(new URL('.', import.meta.url)) + console.log('Starting SW building...'); /** @type {esbuild.BuildOptions} */ diff --git a/packages/sw/package.json b/packages/sw/package.json index a2bead3dd..a60f8230c 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -19,5 +19,6 @@ "eslint": "8.44.0", "eslint-plugin-import": "2.27.5", "typescript": "5.1.6" - } + }, + "type": "module" } From 9dd53527ca513f42c0ca9145e8122d898231f835 Mon Sep 17 00:00:00 2001 From: yupix Date: Sun, 9 Jul 2023 17:20:50 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E3=83=97=E3=83=AD=E3=83=95?= =?UTF-8?q?=E3=82=A3=E3=83=BC=E3=83=ABURL=E3=82=92=E3=82=B3=E3=83=94?= =?UTF-8?q?=E3=83=BC=20=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=20close=20#11190=20(#11205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + packages/frontend/src/scripts/get-user-menu.ts | 10 +++++++++- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c28d0b9bf..e0d2fc7c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように - フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように - 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように +- プロフィールURLをコピーできるボタンを追加 #11190 - Fix: サーバーメトリクスが90度傾いている - Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正 - Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index dacab0cca..42dddebbf 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -59,6 +59,7 @@ export interface Locale { "copyNoteId": string; "copyFileId": string; "copyFolderId": string; + "copyProfileUrl": string; "searchUser": string; "reply": string; "loadMore": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 29f157b84..16014da51 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -56,6 +56,7 @@ copyUserId: "ユーザーIDをコピー" copyNoteId: "ノートIDをコピー" copyFileId: "ファイルIDをコピー" copyFolderId: "フォルダーIDをコピー" +copyProfileUrl: "プロフィールURLをコピー" searchUser: "ユーザーを検索" reply: "返信" loadMore: "もっと見る" diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index c884ed76c..636a6543d 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -2,13 +2,14 @@ import { defineAsyncComponent } from 'vue'; import * as misskey from 'misskey-js'; import { i18n } from '@/i18n'; import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { host } from '@/config'; +import { host, url } from '@/config'; import * as os from '@/os'; import { defaultStore, userActions } from '@/store'; import { $i, iAmModerator } from '@/account'; import { mainRouter } from '@/router'; import { Router } from '@/nirax'; import { rolesCache, userListsCache } from '@/cache'; +import { toUnicode } from 'punycode'; export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) { const meId = $i ? $i.id : null; @@ -137,6 +138,13 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router action: () => { copyToClipboard(`${user.host ?? host}/@${user.username}.atom`); }, + }, { + icon: 'ti ti-share', + text: i18n.ts.copyProfileUrl, + action: () => { + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}` + copyToClipboard(`${url}/${canonical}`); + }, }, { icon: 'ti ti-mail', text: i18n.ts.sendMessage, From 53b1684c39df28017e1f798ec5826569efcf143d Mon Sep 17 00:00:00 2001 From: eni <129673786+enitama@users.noreply.github.com> Date: Sun, 9 Jul 2023 21:24:05 +0900 Subject: [PATCH 4/4] fix(frontend): use system-ui for system font (#11177) * fix(frontend): correct system font stack This was originally set to Hiragino Maru Gothic Pro, which is the same as the current default font. * just use system-ui per code review https://github.com/misskey-dev/misskey/pull/11177#discussion_r1257260039 --------- Co-authored-by: Kagami Sascha Rosylight --- CHANGELOG.md | 1 + packages/frontend/src/style.scss | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d2fc7c3..8e57baf2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Fix: ページ遷移でスクロール位置が保持されない問題を修正 - Fix: フォルダーのページネーションが機能しない #11180 - Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正 +- Fix: システムフォント設定が正しく反映されない問題を修正 ### Server - JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index b4b80a4bb..bd74db7c8 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -72,7 +72,7 @@ html { } &.useSystemFont { - font-family: 'Hiragino Maru Gothic Pro', sans-serif; + font-family: system-ui; } }