Merge remote-tracking branch 'misskey-dev/develop' into develop

This commit is contained in:
mattyatea 2023-09-17 17:09:11 +09:00
commit 8953b68f12
61 changed files with 558 additions and 368 deletions

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class ServerIconsAndManifest1694850832075 {
name = 'ServerIconsAndManifest1694850832075'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "app192IconUrl" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "app512IconUrl" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "manifestJsonOverride" character varying(8192) NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "manifestJsonOverride"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "app512IconUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "app192IconUrl"`);
}
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class ClippedCount1694915420864 {
name = 'ClippedCount1694915420864'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "clippedCount" smallint NOT NULL DEFAULT '0'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "clippedCount"`);
}
}

View file

@ -68,21 +68,34 @@ export async function masterMain() {
process.exit(1);
}
if (envOption.onlyServer) {
await server();
} else if (envOption.onlyQueue) {
await jobQueue();
} else {
await server();
}
bootLogger.succ('Misskey initialized');
if (!envOption.disableClustering) {
if (envOption.disableClustering) {
if (envOption.onlyServer) {
await server();
} else if (envOption.onlyQueue) {
await jobQueue();
} else {
await server();
await jobQueue();
}
} else {
if (envOption.onlyServer) {
// nop
} else if (envOption.onlyQueue) {
// nop
} else {
await server();
}
await spawnWorkers(config.clusterLimit);
}
bootLogger.succ(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on port ${config.port} on ${config.url}`, null, true);
if (envOption.onlyQueue) {
bootLogger.succ('Queue started', null, true);
} else {
bootLogger.succ(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on port ${config.port} on ${config.url}`, null, true);
}
}
function showEnvironment(): void {

View file

@ -340,6 +340,8 @@ export class NoteEntityService implements OnModuleInit {
url: note.url ?? undefined,
...(opts.detail ? {
clippedCount: note.clippedCount,
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
detail: false,
_hint_: options?._hint_,

View file

@ -107,6 +107,18 @@ export class MiMeta {
})
public iconUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public app192IconUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public app512IconUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
@ -444,6 +456,12 @@ export class MiMeta {
})
public serverRules: string[];
@Column('varchar', {
length: 8192,
default: '{}',
})
public manifestJsonOverride: string;
@Column('varchar', {
length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
})

View file

@ -107,6 +107,11 @@ export class MiNote {
})
public repliesCount: number;
@Column('smallint', {
default: 0,
})
public clippedCount: number;
@Column('jsonb', {
default: {},
})

View file

@ -85,6 +85,14 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
app192IconUrl: {
type: 'string',
optional: false, nullable: true,
},
app512IconUrl: {
type: 'string',
optional: false, nullable: true,
},
enableEmail: {
type: 'boolean',
optional: false, nullable: false,
@ -278,6 +286,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
manifestJsonOverride: {
type: 'string',
optional: true, nullable: false,
},
policies: {
type: 'object',
optional: false, nullable: false,
@ -331,6 +343,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
notFoundImageUrl: instance.notFoundImageUrl,
infoImageUrl: instance.infoImageUrl,
iconUrl: instance.iconUrl,
app192IconUrl: instance.app192IconUrl,
app512IconUrl: instance.app512IconUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
defaultLightTheme: instance.defaultLightTheme,
@ -383,6 +397,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableServerMachineStats: instance.enableServerMachineStats,
enableIdenticonGeneration: instance.enableIdenticonGeneration,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
manifestJsonOverride: instance.manifestJsonOverride,
};
});
}

View file

@ -39,6 +39,8 @@ export const paramDef = {
infoImageUrl: { type: 'string', nullable: true },
notFoundImageUrl: { type: 'string', nullable: true },
iconUrl: { type: 'string', nullable: true },
app192IconUrl: { type: 'string', nullable: true },
app512IconUrl: { type: 'string', nullable: true },
backgroundImageUrl: { type: 'string', nullable: true },
logoImageUrl: { type: 'string', nullable: true },
name: { type: 'string', nullable: true },
@ -104,6 +106,7 @@ export const paramDef = {
enableIdenticonGeneration: { type: 'boolean' },
serverRules: { type: 'array', items: { type: 'string' } },
preservedUsernames: { type: 'array', items: { type: 'string' } },
manifestJsonOverride: { type: 'string' },
},
required: [],
} as const;
@ -153,6 +156,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.iconUrl = ps.iconUrl;
}
if (ps.app192IconUrl !== undefined) {
set.app192IconUrl = ps.app192IconUrl;
}
if (ps.app512IconUrl !== undefined) {
set.app512IconUrl = ps.app512IconUrl;
}
if (ps.serverErrorImageUrl !== undefined) {
set.serverErrorImageUrl = ps.serverErrorImageUrl;
}
@ -421,6 +432,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.preservedUsernames = ps.preservedUsernames;
}
if (ps.manifestJsonOverride !== undefined) {
set.manifestJsonOverride = ps.manifestJsonOverride;
}
await this.metaService.update(set);
this.moderationLogService.insertModerationLog(me, 'updateMeta');
});

View file

@ -8,7 +8,7 @@ import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import type { ClipNotesRepository, ClipsRepository } from '@/models/_.js';
import type { ClipNotesRepository, ClipsRepository, NotesRepository } from '@/models/_.js';
import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
@ -72,6 +72,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.clipNotesRepository)
private clipNotesRepository: ClipNotesRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private idService: IdService,
private roleService: RoleService,
private getterService: GetterService,
@ -115,9 +118,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
clipId: clip.id,
});
await this.clipsRepository.update(clip.id, {
this.clipsRepository.update(clip.id, {
lastClippedAt: new Date(),
});
this.notesRepository.increment({ id: note.id }, 'clippedCount', 1);
});
}
}

View file

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ClipNotesRepository, ClipsRepository } from '@/models/_.js';
import type { ClipNotesRepository, ClipsRepository, NotesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
@ -52,6 +52,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.clipNotesRepository)
private clipNotesRepository: ClipNotesRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
@ -73,6 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
noteId: note.id,
clipId: clip.id,
});
this.notesRepository.decrement({ id: note.id }, 'clippedCount', 1);
});
}
}

View file

@ -51,45 +51,6 @@ const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
const viteOut = `${_dirname}/../../../../../built/_vite_/`;
const manifest = {
'short_name': 'Misskey',
'name': 'Misskey',
'start_url': '/',
'display': 'standalone',
'background_color': '#313a42',
'theme_color': '#86b300',
'icons': [
{
'src': '/static-assets/icons/192.png',
'sizes': '192x192',
'type': 'image/png',
'purpose': 'maskable',
},
{
'src': '/static-assets/icons/512.png',
'sizes': '512x512',
'type': 'image/png',
'purpose': 'maskable',
},
{
'src': '/static-assets/splash.png',
'sizes': '300x300',
'type': 'image/png',
'purpose': 'any',
},
],
'share_target': {
'action': '/share/',
'method': 'GET',
'enctype': 'application/x-www-form-urlencoded',
'params': {
'title': 'title',
'text': 'text',
'url': 'url',
},
},
};
@Injectable()
export class ClientServerService {
private logger: Logger;
@ -148,16 +109,60 @@ export class ClientServerService {
@bindThis
private async manifestHandler(reply: FastifyReply) {
const res = deepClone(manifest);
const instance = await this.metaService.fetch(true);
res.short_name = instance.name ?? 'Misskey';
res.name = instance.name ?? 'Misskey';
if (instance.themeColor) res.theme_color = instance.themeColor;
let manifest = {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'short_name': instance.name || 'Misskey',
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'name': instance.name || 'Misskey',
'start_url': '/',
'display': 'standalone',
'background_color': '#313a42',
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'theme_color': instance.themeColor || '#86b300',
'icons': [{
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'src': instance.app192IconUrl || '/static-assets/icons/192.png',
'sizes': '192x192',
'type': 'image/png',
'purpose': 'maskable',
}, {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'src': instance.app512IconUrl || '/static-assets/icons/512.png',
'sizes': '512x512',
'type': 'image/png',
'purpose': 'maskable',
}, {
'src': '/static-assets/splash.png',
'sizes': '300x300',
'type': 'image/png',
'purpose': 'any',
}],
'share_target': {
'action': '/share/',
'method': 'GET',
'enctype': 'application/x-www-form-urlencoded',
'params': {
'title': 'title',
'text': 'text',
'url': 'url',
},
},
};
manifest = {
...manifest,
...JSON.parse(instance.manifestJsonOverride === '' ? '{}' : instance.manifestJsonOverride),
};
reply.header('Cache-Control', 'max-age=300');
return (res);
return (manifest);
}
@bindThis
@ -165,6 +170,7 @@ export class ClientServerService {
return {
instanceName: meta.name ?? 'Misskey',
icon: meta.iconUrl,
appleTouchIcon: meta.app512IconUrl,
themeColor: meta.themeColor,
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',

View file

@ -28,7 +28,7 @@ html
meta(property='og:site_name' content= instanceName || 'Misskey')
meta(name='viewport' content='width=device-width, initial-scale=1')
link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
link(rel='manifest' href='/manifest.json')
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
link(rel='prefetch' href=serverErrorImageUrl)

View file

@ -721,7 +721,7 @@ describe('クリップ', () => {
await addNote({ clipId: aliceClip.id, noteId: aliceNote.id });
const res = await show({ clipId: aliceClip.id });
assert.strictEqual(res.lastClippedAt, new Date(res.lastClippedAt ?? '').toISOString());
assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), [aliceNote]);
assert.deepStrictEqual((await notes({ clipId: aliceClip.id })).map(x => x.id), [aliceNote.id]);
// 他人の非公開ノートも突っ込める
await addNote({ clipId: aliceClip.id, noteId: bobHomeNote.id });
@ -861,8 +861,8 @@ describe('クリップ', () => {
bobNote, bobHomeNote,
];
assert.deepStrictEqual(
res.sort(compareBy(s => s.id)),
expects.sort(compareBy(s => s.id)));
res.sort(compareBy(s => s.id)).map(x => x.id),
expects.sort(compareBy(s => s.id)).map(x => x.id));
});
test('を始端IDとlimitで取得できる。', async () => {
@ -881,8 +881,8 @@ describe('クリップ', () => {
// Promise.allで返ってくる配列はID順で並んでないのでソートして厳密比較
const expects = [noteList[3], noteList[4], noteList[5]];
assert.deepStrictEqual(
res.sort(compareBy(s => s.id)),
expects.sort(compareBy(s => s.id)));
res.sort(compareBy(s => s.id)).map(x => x.id),
expects.sort(compareBy(s => s.id)).map(x => x.id));
});
test('をID範囲指定で取得できる。', async () => {
@ -901,8 +901,8 @@ describe('クリップ', () => {
// Promise.allで返ってくる配列はID順で並んでないのでソートして厳密比較
const expects = [noteList[2], noteList[3]];
assert.deepStrictEqual(
res.sort(compareBy(s => s.id)),
expects.sort(compareBy(s => s.id)));
res.sort(compareBy(s => s.id)).map(x => x.id),
expects.sort(compareBy(s => s.id)).map(x => x.id));
});
test.todo('Remoteのートもクリップできる。どうテストしよう');
@ -911,7 +911,7 @@ describe('クリップ', () => {
const bobClip = await create({ isPublic: true }, { user: bob } );
await addNote({ clipId: bobClip.id, noteId: aliceNote.id }, { user: bob });
const res = await notes({ clipId: bobClip.id });
assert.deepStrictEqual(res, [aliceNote]);
assert.deepStrictEqual(res.map(x => x.id), [aliceNote.id]);
});
test('はPublicなクリップなら認証なしでも取得できる。(非公開ートはhideされて返ってくる)', async () => {
@ -928,8 +928,8 @@ describe('クリップ', () => {
hiddenNote(aliceFollowersNote), hiddenNote(aliceSpecifiedNote),
];
assert.deepStrictEqual(
res.sort(compareBy(s => s.id)),
expects.sort(compareBy(s => s.id)));
res.sort(compareBy(s => s.id)).map(x => x.id),
expects.sort(compareBy(s => s.id)).map(x => x.id));
});
test.todo('ブロック、ミュートされたユーザーからの設定取得etc.');

View file

@ -21,7 +21,7 @@
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-replace": "5.0.2",
"@rollup/pluginutils": "5.0.4",
"@syuilo/aiscript": "0.15.0",
"@syuilo/aiscript": "0.16.0",
"@tabler/icons-webfont": "2.32.0",
"@vitejs/plugin-vue": "4.3.4",
"@vue-macros/reactivity-transform": "0.3.23",

View file

@ -86,9 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkReactionsViewer :note="appearNote" :maxNumber="16">
<template #more>
<button class="_button" :class="$style.reactionDetailsButton" @click="showReactions">
{{ i18n.ts.more }}
</button>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template>
</MkReactionsViewer>
<footer :class="$style.footer">
@ -457,7 +455,7 @@ function showRenoteMenu(viaKeyboard = false): void {
} else {
os.popupMenu([
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
null,
null,
getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
$i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
], renoteTime.value, {
@ -488,12 +486,6 @@ function readPromo() {
});
isDeleted.value = true;
}
function showReactions(): void {
os.popup(defineAsyncComponent(() => import('@/components/MkReactedUsersDialog.vue')), {
noteId: appearNote.id,
}, {}, 'closed');
}
</script>
<style lang="scss" module>
@ -941,7 +933,7 @@ function showReactions(): void {
opacity: 0.7;
}
.reactionDetailsButton {
.reactionOmitted {
display: inline-block;
height: 32px;
margin: 2px;
@ -950,9 +942,5 @@ function showReactions(): void {
border-radius: 4px;
background: transparent;
opacity: .8;
&:hover {
background: var(--X5);
}
}
</style>

View file

@ -11,7 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
v-hotkey="keymap"
:class="$style.root"
>
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
<div v-if="appearNote.reply.replyId">
<div v-if="!conversationLoaded" style="padding: 16px">
<MkButton style="margin: 0 auto;" primary rounded @click="loadConversation">{{ i18n.ts.loadConversation }}</MkButton>
</div>
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
</div>
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
<div v-if="isRenote" :class="$style.renote">
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
@ -125,7 +130,43 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</footer>
</article>
<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
<div :class="$style.tabs">
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button>
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button>
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button>
</div>
<div>
<div v-if="tab === 'replies'" :class="$style.tab_replies">
<div v-if="!repliesLoaded" style="padding: 16px">
<MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton>
</div>
<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
</div>
<div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
<MkPagination :pagination="renotesPagination">
<template #default="{ items }">
<MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
<MkUserCardMini :user="item.user" :withChart="false"/>
</MkA>
</template>
</MkPagination>
</div>
<div v-else-if="tab === 'reactions'" :class="$style.tab_reactions">
<div :class="$style.reactionTabs">
<button v-for="reaction in Object.keys(appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = reaction">
<MkReactionIcon :reaction="reaction"/>
<span style="margin-left: 4px;">{{ appearNote.reactions[reaction] }}</span>
</button>
</div>
<MkPagination :pagination="reactionsPagination">
<template #default="{ items }">
<MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
<MkUserCardMini :user="item.user" :withChart="false"/>
</MkA>
</template>
</MkPagination>
</div>
</div>
</div>
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small">
@ -169,6 +210,10 @@ import { claimAchievement } from '@/scripts/achievements';
import { MenuItem } from '@/types/menu';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
const props = defineProps<{
note: Misskey.entities.Note;
@ -224,6 +269,26 @@ const keymap = {
's': () => showContent.value !== showContent.value,
};
let tab = $ref('replies');
let reactionTabType = $ref(null);
const renotesPagination = $computed(() => ({
endpoint: 'notes/renotes',
limit: 10,
params: {
noteId: appearNote.id,
},
}));
const reactionsPagination = $computed(() => ({
endpoint: 'notes/reactions',
limit: 10,
params: {
noteId: appearNote.id,
type: reactionTabType,
},
}));
useNoteCapture({
rootEl: el,
note: $$(appearNote),
@ -426,14 +491,20 @@ function blur() {
el.value.blur();
}
os.api('notes/children', {
noteId: appearNote.id,
limit: 30,
}).then(res => {
replies.value = res;
});
const repliesLoaded = ref(false);
function loadReplies() {
repliesLoaded.value = true;
os.api('notes/children', {
noteId: appearNote.id,
limit: 30,
}).then(res => {
replies.value = res;
});
}
if (appearNote.replyId) {
const conversationLoaded = ref(false);
function loadConversation() {
conversationLoaded.value = true;
os.api('notes/conversation', {
noteId: appearNote.replyId,
}).then(res => {
@ -640,10 +711,52 @@ if (appearNote.replyId) {
}
}
.reply {
.reply:not(:first-child) {
border-top: solid 0.5px var(--divider);
}
.tabs {
border-top: solid 0.5px var(--divider);
border-bottom: solid 0.5px var(--divider);
display: flex;
}
.tab {
flex: 1;
padding: 12px 8px;
border-top: solid 2px transparent;
border-bottom: solid 2px transparent;
}
.tabActive {
border-bottom: solid 2px var(--accent);
}
.tab_renotes {
padding: 16px;
}
.tab_reactions {
padding: 16px;
}
.reactionTabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.reactionTab {
padding: 4px 6px;
border: solid 1px var(--divider);
border-radius: 6px;
}
.reactionTabActive {
border-color: var(--accent);
}
@container (max-width: 500px) {
.root {
font-size: 0.9em;

View file

@ -1,104 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
@close="dialog.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.reactionsList }}</template>
<MkSpacer :marginMin="20" :marginMax="28">
<div v-if="note" class="_gaps">
<div v-if="reactions.length === 0" class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div>
</div>
<template v-else>
<div :class="$style.tabs">
<button v-for="reaction in reactions" :key="reaction" :class="[$style.tab, { [$style.tabActive]: tab === reaction }]" class="_button" @click="tab = reaction">
<MkReactionIcon :reaction="reaction"/>
<span style="margin-left: 4px;">{{ note.reactions[reaction] }}</span>
</button>
</div>
<MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()">
<MkUserCardMini :user="user" :withChart="false"/>
</MkA>
</template>
</div>
<div v-else>
<MkLoading/>
</div>
</MkSpacer>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { onMounted, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { userPage } from '@/filters/user';
import { i18n } from '@/i18n';
import * as os from '@/os';
import { infoImageUrl } from '@/instance';
const emit = defineEmits<{
(ev: 'closed'): void,
}>();
const props = defineProps<{
noteId: Misskey.entities.Note['id'];
}>();
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
let note = $ref<Misskey.entities.Note>();
let tab = $ref<string>();
let reactions = $ref<string[]>();
let users = $ref();
watch($$(tab), async () => {
const res = await os.api('notes/reactions', {
noteId: props.noteId,
type: tab,
limit: 30,
});
users = res.map(x => x.user);
});
onMounted(() => {
os.api('notes/show', {
noteId: props.noteId,
}).then((res) => {
reactions = Object.keys(res.reactions);
tab = reactions[0];
note = res;
});
});
</script>
<style lang="scss" module>
.tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tab {
padding: 4px 6px;
border: solid 1px var(--divider);
border-radius: 6px;
}
.tabActive {
border-color: var(--accent);
}
</style>

View file

@ -1,71 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
@close="dialog.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.renotesList }}</template>
<MkSpacer :marginMin="20" :marginMax="28">
<div v-if="renotes" class="_gaps">
<div v-if="renotes.length === 0" class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div>
</div>
<template v-else>
<MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()">
<MkUserCardMini :user="user" :withChart="false"/>
</MkA>
</template>
</div>
<div v-else>
<MkLoading/>
</div>
</MkSpacer>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { userPage } from '@/filters/user';
import { i18n } from '@/i18n';
import * as os from '@/os';
import { infoImageUrl } from '@/instance';
const emit = defineEmits<{
(ev: 'closed'): void,
}>();
const props = defineProps<{
noteId: Misskey.entities.Note['id'];
}>();
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
let note = $ref<Misskey.entities.Note>();
let renotes = $ref();
let users = $ref();
onMounted(async () => {
const res = await os.api('notes/renotes', {
noteId: props.noteId,
limit: 30,
});
renotes = res;
users = res.map(x => x.user);
});
</script>
<style lang="scss" module>
</style>

View file

@ -12,7 +12,29 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkInput v-model="iconUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.iconUrl }}</template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
</MkInput>
<MkInput v-model="app192IconUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
<template #caption>
<div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
<div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '192x192px' }) }}</strong></div>
</template>
</MkInput>
<MkInput v-model="app512IconUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
<template #caption>
<div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
<div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '512x512px' }) }}</strong></div>
</template>
</MkInput>
<MkInput v-model="bannerUrl">
@ -53,6 +75,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
<MkTextarea v-model="manifestJsonOverride">
<template #label>{{ i18n.ts._serverSettings.manifestJsonOverride }}</template>
</MkTextarea>
</div>
</FormSuspense>
</MkSpacer>
@ -69,6 +95,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { } from 'vue';
import JSON5 from 'json5';
import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
@ -77,13 +104,16 @@ import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import { fetchInstance } from '@/instance';
import { instance, fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import MkButton from '@/components/MkButton.vue';
import MkColorInput from '@/components/MkColorInput.vue';
import { host } from '@/config';
let iconUrl: string | null = $ref(null);
let app192IconUrl: string | null = $ref(null);
let app512IconUrl: string | null = $ref(null);
let bannerUrl: string | null = $ref(null);
let backgroundImageUrl: string | null = $ref(null);
let themeColor: any = $ref(null);
@ -92,10 +122,13 @@ let defaultDarkTheme: any = $ref(null);
let serverErrorImageUrl: string | null = $ref(null);
let infoImageUrl: string | null = $ref(null);
let notFoundImageUrl: string | null = $ref(null);
let manifestJsonOverride: string = $ref('{}');
async function init() {
const meta = await os.api('admin/meta');
iconUrl = meta.iconUrl;
app192IconUrl = meta.app192IconUrl;
app512IconUrl = meta.app512IconUrl;
bannerUrl = meta.bannerUrl;
backgroundImageUrl = meta.backgroundImageUrl;
themeColor = meta.themeColor;
@ -104,11 +137,14 @@ async function init() {
serverErrorImageUrl = meta.serverErrorImageUrl;
infoImageUrl = meta.infoImageUrl;
notFoundImageUrl = meta.notFoundImageUrl;
manifestJsonOverride = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t');
}
function save() {
os.apiWithDialog('admin/update-meta', {
iconUrl,
app192IconUrl,
app512IconUrl,
bannerUrl,
backgroundImageUrl,
themeColor: themeColor === '' ? null : themeColor,
@ -117,6 +153,7 @@ function save() {
infoImageUrl,
notFoundImageUrl,
serverErrorImageUrl,
manifestJsonOverride: manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride)),
}).then(() => {
fetchInstance();
});

View file

@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import { useRouter } from '@/router';
const PRESET_DEFAULT = `/// @ 0.15.0
const PRESET_DEFAULT = `/// @ 0.16.0
var name = ""
@ -62,7 +62,7 @@ Ui:render([
])
`;
const PRESET_OMIKUJI = `/// @ 0.15.0
const PRESET_OMIKUJI = `/// @ 0.16.0
//
//
@ -105,7 +105,7 @@ Ui:render([
])
`;
const PRESET_SHUFFLE = `/// @ 0.15.0
const PRESET_SHUFFLE = `/// @ 0.16.0
//
let string = "ペペロンチーノ"
@ -184,7 +184,7 @@ var cursor = 0
do()
`;
const PRESET_QUIZ = `/// @ 0.15.0
const PRESET_QUIZ = `/// @ 0.16.0
let title = '地理クイズ'
let qas = [{
@ -297,7 +297,7 @@ qaEls.push(Ui:C:container({
Ui:render(qaEls)
`;
const PRESET_TIMELINE = `/// @ 0.15.0
const PRESET_TIMELINE = `/// @ 0.16.0
// API
@fetch() {

View file

@ -94,13 +94,14 @@ function fetchNote() {
noteId: props.noteId,
}).then(res => {
note = res;
Promise.all([
// 2023-10-01notes/clips
if (note.clippedCount > 0 || new Date(note.createdAt).getTime() < new Date('2023-10-01').getTime()) {
os.api('notes/clips', {
noteId: note.id,
}),
]).then(([_clips]) => {
clips = _clips;
});
}).then((_clips) => {
clips = _clips;
});
}
}).catch(err => {
error = err;
});

View file

@ -238,18 +238,6 @@ export function getNoteMenu(props: {
os.pageWindow(`/notes/${appearNote.id}`);
}
function showReactions(): void {
os.popup(defineAsyncComponent(() => import('@/components/MkReactedUsersDialog.vue')), {
noteId: appearNote.id,
}, {}, 'closed');
}
function showRenotes(): void {
os.popup(defineAsyncComponent(() => import('@/components/MkRenotedUsersDialog.vue')), {
noteId: appearNote.id,
}, {}, 'closed');
}
async function translate(): Promise<void> {
if (props.translation.value != null) return;
props.translating.value = true;
@ -279,14 +267,6 @@ export function getNoteMenu(props: {
icon: 'ti ti-info-circle',
text: i18n.ts.details,
action: openDetail,
}, {
icon: 'ti ti-repeat',
text: i18n.ts.renotesList,
action: showRenotes,
}, {
icon: 'ti ti-icons',
text: i18n.ts.reactionsList,
action: showReactions,
}, {
icon: 'ti ti-copy',
text: i18n.ts.copyContent,

View file

@ -112,8 +112,8 @@ export function getConfig(): UserConfig {
build: {
target: [
'chrome108',
'firefox109',
'chrome116',
'firefox116',
'safari16',
],
manifest: 'manifest.json',

View file

@ -29,6 +29,9 @@ type Ad = TODO_2;
// @public (undocumented)
type AdminInstanceMetadata = DetailedInstanceMetadata & {
blockedHosts: string[];
app192IconUrl: string | null;
app512IconUrl: string | null;
manifestJsonOverride: string;
};
// @public (undocumented)
@ -2536,6 +2539,7 @@ type Note = {
reactions: Record<string, number>;
renoteCount: number;
repliesCount: number;
clippedCount?: number;
poll?: {
expiresAt: DateString | null;
multiple: boolean;

View file

@ -175,6 +175,7 @@ export type Note = {
reactions: Record<string, number>;
renoteCount: number;
repliesCount: number;
clippedCount?: number;
poll?: {
expiresAt: DateString | null;
multiple: boolean;
@ -353,6 +354,9 @@ export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata;
export type AdminInstanceMetadata = DetailedInstanceMetadata & {
// TODO: There are more fields.
blockedHosts: string[];
app192IconUrl: string | null;
app512IconUrl: string | null;
manifestJsonOverride: string;
};
export type ServerInfo = {