= new Map();
if (options?.schema !== 'UserLite') {
+ profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
+ .then(profiles => new Map(profiles.map(p => [p.userId, p])));
+
const meId = me ? me.id : null;
if (meId) {
userMemos = await this.userMemosRepository.findBy({ userId: meId })
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 59986e9d4..27fd280a8 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -238,7 +238,7 @@ export type SchemaTypeDef =
p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] :
never
) :
- p['items'] extends NonNullable ? SchemaTypeDef[] :
+ p['items'] extends NonNullable ? SchemaType[] :
any[]
) :
p['anyOf'] extends ReadonlyArray ? UnionSchemaType & PartialIntersection> :
diff --git a/packages/backend/src/models/Poll.ts b/packages/backend/src/models/Poll.ts
index c2693dbb1..ca985c8b2 100644
--- a/packages/backend/src/models/Poll.ts
+++ b/packages/backend/src/models/Poll.ts
@@ -8,6 +8,7 @@ import { noteVisibilities } from '@/types.js';
import { id } from './util/id.js';
import { MiNote } from './Note.js';
import type { MiUser } from './User.js';
+import type { MiChannel } from "@/models/Channel.js";
@Entity('poll')
export class MiPoll {
@@ -58,6 +59,14 @@ export class MiPoll {
comment: '[Denormalized]',
})
public userHost: string | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ comment: '[Denormalized]',
+ })
+ public channelId: MiChannel['id'] | null;
//#endregion
constructor(data: Partial) {
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index 87cffdb3e..be800bef4 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -14,14 +14,15 @@ import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataServic
import InstanceChart from '@/core/chart/charts/instance.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js';
import FederationChart from '@/core/chart/charts/federation.js';
-import { getApId, IActivity } from '@/core/activitypub/type.js';
+import { getApId } from '@/core/activitypub/type.js';
+import type { IActivity } from '@/core/activitypub/type.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiUserPublickey } from '@/models/UserPublickey.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
-import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js';
+import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
import { bindThis } from '@/decorators.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
@@ -38,7 +39,7 @@ export class InboxProcessorService {
private apInboxService: ApInboxService,
private federatedInstanceService: FederatedInstanceService,
private fetchInstanceMetadataService: FetchInstanceMetadataService,
- private ldSignatureService: LdSignatureService,
+ private jsonLdService: JsonLdService,
private apPersonService: ApPersonService,
private apDbResolverService: ApDbResolverService,
private instanceChart: InstanceChart,
@@ -133,9 +134,10 @@ export class InboxProcessorService {
throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした');
}
+ const jsonLd = this.jsonLdService.use();
+
// LD-Signature検証
- const ldSignature = this.ldSignatureService.use();
- const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
+ const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
if (!verified) {
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
}
@@ -143,7 +145,7 @@ export class InboxProcessorService {
// アクティビティを正規化
delete activity.signature;
try {
- activity = await ldSignature.compact(activity) as IActivity;
+ activity = await jsonLd.compact(activity) as IActivity;
} catch (e) {
throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`);
}
diff --git a/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts b/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts
index d80e31542..a47019509 100644
--- a/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts
+++ b/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts
@@ -88,13 +88,9 @@ export class ReportAbuseProcessorService {
forwarded: resolver.forward && job.data.targetUserHost !== null && job.data.reporterHost === null,
});
- const activeWebhooks = await this.webhookService.getActiveWebhooks();
- for (const webhook of activeWebhooks) {
- const webhookUser = await this.usersRepository.findOneByOrFail({
- id: webhook.userId,
- });
- const isAdmin = await this.roleService.isAdministrator(webhookUser);
- if (webhook.on.includes('reportAutoResolved') && isAdmin) {
+ const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.on.includes('reportAutoResolved'));
+ for (const webhook of webhooks) {
+ if (await this.roleService.isAdministrator({ id: webhook.userId, isRoot: false })) {
this.queueService.webhookDeliver(webhook, 'reportAutoResolved', {
resolver: resolver,
report: job.data,
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index e37e95c1d..fcd5059a5 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -95,6 +95,7 @@ import * as ep___admin_sso_create from './endpoints/admin/sso/create.js';
import * as ep___admin_sso_delete from './endpoints/admin/sso/delete.js';
import * as ep___admin_sso_list from './endpoints/admin/sso/list.js';
import * as ep___admin_sso_update from './endpoints/admin/sso/update.js';
+import * as ep___announcement from './endpoints/announcement.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@@ -484,6 +485,7 @@ const $admin_sso_create: Provider = { provide: 'ep:admin/sso/create', useClass:
const $admin_sso_delete: Provider = { provide: 'ep:admin/sso/delete', useClass: ep___admin_sso_delete.default };
const $admin_sso_list: Provider = { provide: 'ep:admin/sso/list', useClass: ep___admin_sso_list.default };
const $admin_sso_update: Provider = { provide: 'ep:admin/sso/update', useClass: ep___admin_sso_update.default };
+const $announcement: Provider = { provide: 'ep:announcement', useClass: ep___announcement.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
@@ -877,6 +879,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_sso_delete,
$admin_sso_list,
$admin_sso_update,
+ $announcement,
$announcements,
$antennas_create,
$antennas_delete,
@@ -1264,6 +1267,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_sso_delete,
$admin_sso_list,
$admin_sso_update,
+ $announcement,
$announcements,
$antennas_create,
$antennas_delete,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 862d1d162..57322d3d5 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -95,6 +95,7 @@ import * as ep___admin_sso_create from './endpoints/admin/sso/create.js';
import * as ep___admin_sso_delete from './endpoints/admin/sso/delete.js';
import * as ep___admin_sso_list from './endpoints/admin/sso/list.js';
import * as ep___admin_sso_update from './endpoints/admin/sso/update.js';
+import * as ep___announcement from './endpoints/announcement.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@@ -482,6 +483,7 @@ const eps = [
['admin/sso/delete', ep___admin_sso_delete],
['admin/sso/list', ep___admin_sso_list],
['admin/sso/update', ep___admin_sso_update],
+ ['announcement', ep___announcement],
['announcements', ep___announcements],
['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete],
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index 0fa3fe833..dbd2416d1 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -69,13 +69,9 @@ export default class extends Endpoint { // eslint-
forwarded: ps.forward && report.targetUserHost != null,
}).then(() => this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }));
- const activeWebhooks = await this.webhookService.getActiveWebhooks();
- for (const webhook of activeWebhooks) {
- const webhookUser = await this.usersRepository.findOneByOrFail({
- id: webhook.userId,
- });
- const isAdmin = await this.roleService.isAdministrator(webhookUser);
- if (webhook.on.includes('reportResolved') && isAdmin) {
+ const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.on.includes('reportResolved'));
+ for (const webhook of webhooks) {
+ if (await this.roleService.isAdministrator({ id: webhook.userId, isRoot: false })) {
this.queueService.webhookDeliver(webhook, 'reportResolved', {
updatedReport,
});
diff --git a/packages/backend/src/server/api/endpoints/announcement.ts b/packages/backend/src/server/api/endpoints/announcement.ts
new file mode 100644
index 000000000..98b1746ae
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/announcement.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { EntityNotFoundError } from "typeorm";
+import { ApiError } from "../error.js";
+
+export const meta = {
+ tags: ['meta'],
+
+ requireCredential: false,
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'Announcement',
+ },
+
+ errors: {
+ noSuchAnnouncement: {
+ message: 'No such announcement.',
+ code: 'NO_SUCH_ANNOUNCEMENT',
+ id: 'b57b5e1d-4f49-404a-9edb-46b00268f121',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ announcementId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['announcementId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private announcementService: AnnouncementService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ try {
+ return await this.announcementService.getAnnouncement(ps.announcementId, me);
+ } catch (err) {
+ if (err instanceof EntityNotFoundError) throw new ApiError(meta.errors.noSuchAnnouncement);
+ throw err;
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts
index 5a23e8f0e..ba48b0119 100644
--- a/packages/backend/src/server/api/endpoints/fetch-rss.ts
+++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts
@@ -20,10 +20,185 @@ export const meta = {
res: {
type: 'object',
properties: {
+ image: {
+ type: 'object',
+ optional: true,
+ properties: {
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ url: {
+ type: 'string',
+ optional: false,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ paginationLinks: {
+ type: 'object',
+ optional: true,
+ properties: {
+ self: {
+ type: 'string',
+ optional: true,
+ },
+ first: {
+ type: 'string',
+ optional: true,
+ },
+ next: {
+ type: 'string',
+ optional: true,
+ },
+ last: {
+ type: 'string',
+ optional: true,
+ },
+ prev: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
items: {
type: 'array',
+ optional: false,
items: {
type: 'object',
+ properties: {
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ guid: {
+ type: 'string',
+ optional: true,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
+ pubDate: {
+ type: 'string',
+ optional: true,
+ },
+ creator: {
+ type: 'string',
+ optional: true,
+ },
+ summary: {
+ type: 'string',
+ optional: true,
+ },
+ content: {
+ type: 'string',
+ optional: true,
+ },
+ isoDate: {
+ type: 'string',
+ optional: true,
+ },
+ categories: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
+ contentSnippet: {
+ type: 'string',
+ optional: true,
+ },
+ enclosure: {
+ type: 'object',
+ optional: true,
+ properties: {
+ url: {
+ type: 'string',
+ optional: false,
+ },
+ length: {
+ type: 'number',
+ optional: true,
+ },
+ type: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ feedUrl: {
+ type: 'string',
+ optional: true,
+ },
+ description: {
+ type: 'string',
+ optional: true,
+ },
+ itunes: {
+ type: 'object',
+ optional: true,
+ additionalProperties: true,
+ properties: {
+ image: {
+ type: 'string',
+ optional: true,
+ },
+ owner: {
+ type: 'object',
+ optional: true,
+ properties: {
+ name: {
+ type: 'string',
+ optional: true,
+ },
+ email: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ author: {
+ type: 'string',
+ optional: true,
+ },
+ summary: {
+ type: 'string',
+ optional: true,
+ },
+ explicit: {
+ type: 'string',
+ optional: true,
+ },
+ categories: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
+ keywords: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
},
},
},
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 7e654b3be..df0c501be 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -96,9 +96,10 @@ export default class extends Endpoint {
}
const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential);
+ const keyId = keyInfo.credentialID;
await this.userSecurityKeysRepository.insert({
- id: keyInfo.credentialID,
+ id: keyId,
userId: me.id,
name: ps.name,
publicKey: Buffer.from(keyInfo.credentialPublicKey).toString('base64url'),
@@ -115,7 +116,7 @@ export default class extends Endpoint {
}));
return {
- id: keyInfo.credentialID,
+ id: keyId,
name: ps.name,
};
});
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index 320d9fdb0..2f619380e 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -7,7 +7,7 @@ import { In } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
-import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js';
+import { FilterUnionByProperty, notificationTypes, obsoleteNotificationTypes } from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
@@ -84,27 +84,51 @@ export default class extends Endpoint { // eslint-
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
- const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
- const notificationsRes = await this.redisClient.xrevrange(
- `notificationTimeline:${me.id}`,
- ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
- ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
- 'COUNT', limit);
+ let sinceTime = ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime().toString() : null;
+ let untilTime = ps.untilId ? this.idService.parse(ps.untilId).date.getTime().toString() : null;
- if (notificationsRes.length === 0) {
- return [];
- }
+ let notifications: MiNotification[];
+ for (;;) {
+ let notificationsRes: [id: string, fields: string[]][];
- let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[];
+ // sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
+ if (sinceTime && !untilTime) {
+ notificationsRes = await this.redisClient.xrange(
+ `notificationTimeline:${me.id}`,
+ '(' + sinceTime,
+ '+',
+ 'COUNT', ps.limit);
+ } else {
+ notificationsRes = await this.redisClient.xrevrange(
+ `notificationTimeline:${me.id}`,
+ untilTime ? '(' + untilTime : '+',
+ sinceTime ? '(' + sinceTime : '-',
+ 'COUNT', ps.limit);
+ }
- if (includeTypes && includeTypes.length > 0) {
- notifications = notifications.filter(notification => includeTypes.includes(notification.type));
- } else if (excludeTypes && excludeTypes.length > 0) {
- notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
- }
+ if (notificationsRes.length === 0) {
+ return [];
+ }
- if (notifications.length === 0) {
- return [];
+ notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[];
+
+ if (includeTypes && includeTypes.length > 0) {
+ notifications = notifications.filter(notification => includeTypes.includes(notification.type));
+ } else if (excludeTypes && excludeTypes.length > 0) {
+ notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
+ }
+
+ if (notifications.length !== 0) {
+ // 通知が1件以上ある場合は返す
+ break;
+ }
+
+ // フィルタしたことで通知が0件になった場合、次のページを取得する
+ if (ps.sinceId && !ps.untilId) {
+ sinceTime = notificationsRes[notificationsRes.length - 1][0];
+ } else {
+ untilTime = notificationsRes[notificationsRes.length - 1][0];
+ }
}
// Mark all as read
diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
index ba3857306..4fd6f8682 100644
--- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
+++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
@@ -32,6 +32,7 @@ export const paramDef = {
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
+ excludeChannels: { type: 'boolean', default: false },
},
required: [],
} as const;
@@ -86,6 +87,12 @@ export default class extends Endpoint { // eslint-
query.setParameters(mutingQuery.getParameters());
//#endregion
+ //#region exclude channels
+ if (ps.excludeChannels) {
+ query.andWhere('poll.channelId IS NULL');
+ }
+ //#endregion
+
const polls = await query
.orderBy('poll.noteId', 'DESC')
.limit(ps.limit)
diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
index 4d5886aff..fd952c460 100644
--- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts
+++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
@@ -100,13 +100,9 @@ export default class extends Endpoint { // eslint-
category: ps.category,
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
- const activeWebhooks = await this.webhookService.getActiveWebhooks();
- for (const webhook of activeWebhooks) {
- const webhookUser = await this.usersRepository.findOneByOrFail({
- id: webhook.userId,
- });
- const isAdmin = await this.roleService.isAdministrator(webhookUser);
- if (webhook.on.includes('reportCreated') && isAdmin) {
+ const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.on.includes('reportCreated'));
+ for (const webhook of webhooks) {
+ if (await this.roleService.isAdministrator({ id: webhook.userId, isRoot: false })) {
this.queueService.webhookDeliver(webhook, 'reportCreated', {
report,
});
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index ff18c9187..7ef2c358d 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -122,6 +122,7 @@ export default class extends Endpoint { // eslint-
});
// リクエストされた通りに並べ替え
+ // 順番は保持されるけど数は減ってる可能性がある
const _users: MiUser[] = [];
for (const id of ps.userIds) {
const user = users.find((u) => u.id === id);
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index a9ddaf4f1..027fe75dd 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -199,6 +199,11 @@ export class ClientServerService {
// Authenticate
fastify.addHook('onRequest', async (request, reply) => {
+ if (request.routeOptions.url == null) {
+ reply.code(404).send('Not found');
+ return;
+ }
+
// %71ueueとかでリクエストされたら困るため
const url = decodeURI(request.routeOptions.url ?? '');
if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) {
@@ -434,7 +439,7 @@ export class ClientServerService {
//#endregion
- const renderBase = async (reply: FastifyReply) => {
+ const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=30');
return await reply.view('base', {
@@ -443,6 +448,7 @@ export class ClientServerService {
title: meta.name ?? 'Misskey',
desc: meta.description,
...await this.generateCommonPugData(meta),
+ ...data,
});
};
@@ -775,6 +781,18 @@ export class ClientServerService {
});
//#endregion
+ //region noindex pages
+ // Tags
+ fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => {
+ return await renderBase(reply, { noindex: true });
+ });
+
+ // User with Tags
+ fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
+ return await renderBase(reply, { noindex: true });
+ });
+ //endregion
+
fastify.get('/_info_card_', async (request, reply) => {
const meta = await this.metaService.fetch(true);
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index 0389b9793..cafdc2578 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -50,6 +50,9 @@ html
block title
= title || 'Misskey'
+ if noindex
+ meta(name='robots' content='noindex')
+
block desc
meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts
index 67464e893..1cac498a0 100644
--- a/packages/backend/test/unit/activitypub.ts
+++ b/packages/backend/test/unit/activitypub.ts
@@ -13,6 +13,8 @@ import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
+import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
+import { CONTEXTS } from '@/core/activitypub/misc/contexts.js';
import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -88,6 +90,7 @@ describe('ActivityPub', () => {
let noteService: ApNoteService;
let personService: ApPersonService;
let rendererService: ApRendererService;
+ let jsonLdService: JsonLdService;
let resolver: MockResolver;
const metaInitial = {
@@ -126,6 +129,7 @@ describe('ActivityPub', () => {
personService = app.get(ApPersonService);
rendererService = app.get(ApRendererService);
imageService = app.get(ApImageService);
+ jsonLdService = app.get(JsonLdService);
resolver = new MockResolver(await app.resolve(LoggerService));
// Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error
@@ -321,4 +325,42 @@ describe('ActivityPub', () => {
assert.strictEqual(driveFile, null);
});
});
+
+ describe('JSON-LD', () =>{
+ test('Compaction', async () => {
+ const jsonLd = jsonLdService.use();
+
+ const object = {
+ '@context': [
+ 'https://www.w3.org/ns/activitystreams',
+ {
+ _misskey_quote: 'https://misskey-hub.net/ns#_misskey_quote',
+ unknown: 'https://example.org/ns#unknown',
+ undefined: null,
+ },
+ ],
+ id: 'https://example.com/notes/42',
+ type: 'Note',
+ attributedTo: 'https://example.com/users/1',
+ to: ['https://www.w3.org/ns/activitystreams#Public'],
+ content: 'test test foo',
+ _misskey_quote: 'https://example.com/notes/1',
+ unknown: 'test test bar',
+ undefined: 'test test baz',
+ };
+ const compacted = await jsonLd.compact(object);
+
+ assert.deepStrictEqual(compacted, {
+ '@context': CONTEXTS,
+ id: 'https://example.com/notes/42',
+ type: 'Note',
+ attributedTo: 'https://example.com/users/1',
+ to: 'as:Public',
+ content: 'test test foo',
+ _misskey_quote: 'https://example.com/notes/1',
+ 'https://example.org/ns#unknown': 'test test bar',
+ // undefined: 'test test baz',
+ });
+ });
+ });
});
diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts
index 6522bb42b..60a8cabe1 100644
--- a/packages/frontend/.storybook/fakes.ts
+++ b/packages/frontend/.storybook/fakes.ts
@@ -27,7 +27,7 @@ export function galleryPost(isSensitive = false) {
id: 'somepostid',
createdAt: '2016-12-28T22:49:51.000Z',
updatedAt: '2016-12-28T22:49:51.000Z',
- userid: 'someuserid',
+ userId: 'someuserid',
user: userDetailed(),
title: 'Some post title',
description: 'Some post description',
@@ -75,9 +75,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay',
avatarDecorations: [],
- emojis: [],
+ emojis: {},
bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog',
- bannerColor: '#000000',
bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
birthday: '2014-06-20',
createdAt: '2016-12-28T22:49:51.000Z',
@@ -120,11 +119,16 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
publicReactions: false,
securityKeys: false,
twoFactorEnabled: false,
+ usePasswordLessLogin: false,
twoFactorBackupCodesStock: 'none',
updatedAt: null,
+ lastFetchedAt: null,
uri: null,
url: null,
+ movedTo: null,
+ alsoKnownAs: null,
notify: 'none',
+ memo: null
};
}
diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index 1e925aede..d74c83a50 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -82,23 +82,16 @@ function h(
return Object.assign(props || {}, { type }) as T;
}
-declare global {
- namespace JSX {
- type Element = estree.Node;
- type ElementClass = never;
- type ElementAttributesProperty = never;
- type ElementChildrenAttribute = never;
- type IntrinsicAttributes = never;
- type IntrinsicClassAttributes = never;
- type IntrinsicElements = {
- [T in keyof typeof generator as ToKebab>>]: {
- [K in keyof Omit<
- Parameters<(typeof generator)[T]>[0],
- 'type'
- >]?: Parameters<(typeof generator)[T]>[0][K];
- };
+declare namespace h.JSX {
+ type Element = estree.Node;
+ type IntrinsicElements = {
+ [T in keyof typeof generator as ToKebab>>]: {
+ [K in keyof Omit<
+ Parameters<(typeof generator)[T]>[0],
+ 'type'
+ >]?: Parameters<(typeof generator)[T]>[0][K];
};
- }
+ };
}
function toStories(component: string): Promise {
@@ -388,6 +381,7 @@ function toStories(component: string): Promise {
'/* eslint-disable @typescript-eslint/explicit-function-return-type */\n' +
'/* eslint-disable import/no-default-export */\n' +
'/* eslint-disable import/no-duplicates */\n' +
+ '/* eslint-disable import/order */\n' +
generate(program, { generator }) +
(hasImplStories ? readFileSync(`${implStories}.ts`, 'utf-8') : ''),
{
diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts
index 0a8748857..d3822942c 100644
--- a/packages/frontend/.storybook/main.ts
+++ b/packages/frontend/.storybook/main.ts
@@ -34,7 +34,7 @@ const config = {
disableTelemetry: true,
},
async viteFinal(config) {
- const replacePluginForIsChromatic = config.plugins?.findIndex((plugin) => plugin && (plugin as Partial)?.name === 'replace') ?? -1;
+ const replacePluginForIsChromatic = config.plugins?.findIndex((plugin: Plugin) => plugin && plugin.name === 'replace') ?? -1;
if (~replacePluginForIsChromatic) {
config.plugins?.splice(replacePluginForIsChromatic, 1);
}
diff --git a/packages/frontend/.storybook/mocks.ts b/packages/frontend/.storybook/mocks.ts
index a4c3ee95a..5450c4e5d 100644
--- a/packages/frontend/.storybook/mocks.ts
+++ b/packages/frontend/.storybook/mocks.ts
@@ -6,7 +6,8 @@
import { type SharedOptions, http, HttpResponse } from 'msw';
export const onUnhandledRequest = ((req, print) => {
- if (req.url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(req.url.pathname)) {
+ const url = new URL(req.url);
+ if (url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(url.pathname)) {
return
}
print.warning()
diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html
index 6a06aaaa0..1be207d7a 100644
--- a/packages/frontend/.storybook/preview-head.html
+++ b/packages/frontend/.storybook/preview-head.html
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index 01b21e680..4ee7b27c1 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -26,9 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
-
+
+
+ {{ i18n.ts.createdAt }}:
+
+
+ {{ i18n.ts.updatedAt }}:
+
+
{{ i18n.ts.gotIt }}
diff --git a/packages/frontend/src/pages/contact.vue b/packages/frontend/src/pages/contact.vue
new file mode 100644
index 000000000..3a694a713
--- /dev/null
+++ b/packages/frontend/src/pages/contact.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ {{ instance.maintainerEmail }}
+
+
+
+
+
diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue
index b5c8e7016..cfdb235d3 100644
--- a/packages/frontend/src/pages/explore.featured.vue
+++ b/packages/frontend/src/pages/explore.featured.vue
@@ -29,6 +29,9 @@ const paginationForPolls = {
endpoint: 'notes/polls/recommendation' as const,
limit: 10,
offsetMode: true,
+ params: {
+ excludeChannels: true,
+ },
};
const tab = ref('notes');
diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue
index 680934e7c..37f6558d6 100644
--- a/packages/frontend/src/pages/share.vue
+++ b/packages/frontend/src/pages/share.vue
@@ -64,7 +64,34 @@ async function init() {
// Googleニュース対策
if (text?.startsWith(`${title.value}.\n`)) noteText += text.replace(`${title.value}.\n`, '');
else if (text && title.value !== text) noteText += `${text}\n`;
- if (url) noteText += `${url}`;
+ if (url) {
+ try {
+ // Normalize the URL to URL-encoded and puny-coded from with the URL constructor.
+ //
+ // It's common to use unicode characters in the URL for better visibility of URL
+ // like: https://ja.wikipedia.org/wiki/ミスキー
+ // or like: https://藍.moe/
+ // However, in the MFM, the unicode characters must be URL-encoded to be parsed as `url` node
+ // like: https://ja.wikipedia.org/wiki/%E3%83%9F%E3%82%B9%E3%82%AD%E3%83%BC
+ // or like: https://xn--931a.moe/
+ // Therefore, we need to normalize the URL to URL-encoded form.
+ //
+ // The URL constructor will parse the URL and normalize unicode characters
+ // in the host to punycode and in the path component to URL-encoded form.
+ // (see url.spec.whatwg.org)
+ //
+ // In addition, the current MFM renderer decodes the URL-encoded path and / punycode encoded host name so
+ // this normalization doesn't make the visible URL ugly.
+ // (see MkUrl.vue)
+
+ noteText += new URL(url).href;
+ } catch {
+ // fallback to original URL if the URL is invalid.
+ // note that this is extremely rare since the `url` parameter is designed to share a URL and
+ // the URL constructor will throw TypeError only if failure, which means the URL is not valid.
+ noteText += url;
+ }
+ }
initialText.value = noteText.trim();
if (visibility.value === 'specified') {
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 735b7d2f5..727e5e8af 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index 6c05aad24..d6ba397f1 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -42,11 +42,11 @@ import XTimeline from './welcome.timeline.vue';
import MarqueeText from '@/components/MkMarquee.vue';
import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue';
import misskeysvg from '/client-assets/misskey.svg';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { instance as meta } from '@/instance.js';
-const meta = ref();
const instances = ref();
function getInstanceIcon(instance: Misskey.entities.FederationInstance): string {
@@ -56,10 +56,6 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string
return getProxiedImageUrl(instance.iconUrl, 'preview');
}
-misskeyApi('meta', { detail: true }).then(_meta => {
- meta.value = _meta;
-});
-
misskeyApiGet('federation/instances', {
sort: '+pubSub',
limit: 20,
diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue
index 9ba6a5885..915fe3502 100644
--- a/packages/frontend/src/pages/welcome.vue
+++ b/packages/frontend/src/pages/welcome.vue
@@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
-
-
+
+
@@ -16,13 +16,13 @@ import * as Misskey from 'misskey-js';
import XSetup from './welcome.setup.vue';
import XEntrance from './welcome.entrance.a.vue';
import { instanceName } from '@/config.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { fetchInstance } from '@/instance.js';
-const meta = ref
(null);
+const instance = ref(null);
-misskeyApi('meta', { detail: true }).then(res => {
- meta.value = res;
+fetchInstance(true).then((res) => {
+ instance.value = res;
});
const headerActions = computed(() => []);
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 1cbf61dc2..47d78fbac 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -197,10 +197,16 @@ const routes: RouteDef[] = [{
}, {
path: '/announcements',
component: page(() => import('@/pages/announcements.vue')),
+}, {
+ path: '/announcements/:announcementId',
+ component: page(() => import('@/pages/announcement.vue')),
}, {
path: '/about',
component: page(() => import('@/pages/about.vue')),
hash: 'initialTab',
+}, {
+ path: '/contact',
+ component: page(() => import('@/pages/contact.vue')),
}, {
path: '/about-misskey',
component: page(() => import('@/pages/about-misskey.vue')),
diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts
index f2493264d..fa3fcac2e 100644
--- a/packages/frontend/src/scripts/aiscript/ui.ts
+++ b/packages/frontend/src/scripts/aiscript/ui.ts
@@ -6,6 +6,7 @@
import { utils, values } from '@syuilo/aiscript';
import { v4 as uuid } from 'uuid';
import { ref, Ref } from 'vue';
+import * as Misskey from 'misskey-js';
export type AsUiComponentBase = {
id: string;
@@ -115,23 +116,24 @@ export type AsUiFolder = AsUiComponentBase & {
opened?: boolean;
};
+type PostFormPropsForAsUi = {
+ text: string;
+ cw?: string;
+ visibility?: (typeof Misskey.noteVisibilities)[number];
+ localOnly?: boolean;
+};
+
export type AsUiPostFormButton = AsUiComponentBase & {
type: 'postFormButton';
text?: string;
primary?: boolean;
rounded?: boolean;
- form?: {
- text: string;
- cw?: string;
- };
+ form?: PostFormPropsForAsUi;
};
export type AsUiPostForm = AsUiComponentBase & {
type: 'postForm';
- form?: {
- text: string;
- cw?: string;
- };
+ form?: PostFormPropsForAsUi;
};
export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm;
@@ -447,6 +449,24 @@ function getFolderOptions(def: values.Value | undefined): Omit Promise): Omit {
utils.assertObject(def);
@@ -459,22 +479,11 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu
const form = def.value.get('form');
if (form) utils.assertObject(form);
- const getForm = () => {
- const text = form!.value.get('text');
- utils.assertString(text);
- const cw = form!.value.get('cw');
- if (cw) utils.assertString(cw);
- return {
- text: text.value,
- cw: cw?.value,
- };
- };
-
return {
text: text?.value,
primary: primary?.value,
rounded: rounded?.value,
- form: form ? getForm() : {
+ form: form ? getPostFormProps(form) : {
text: '',
},
};
@@ -486,19 +495,8 @@ function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn
const form = def.value.get('form');
if (form) utils.assertObject(form);
- const getForm = () => {
- const text = form!.value.get('text');
- utils.assertString(text);
- const cw = form!.value.get('cw');
- if (cw) utils.assertString(cw);
- return {
- text: text.value,
- cw: cw?.value,
- };
- };
-
return {
- form: form ? getForm() : {
+ form: form ? getPostFormProps(form) : {
text: '',
},
};
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 87921bc67..e7c9a848e 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -492,10 +492,9 @@ export function getNoteMenu(props: {
};
}
-type Visibility = 'public' | 'home' | 'followers' | 'specified';
+type Visibility = (typeof Misskey.noteVisibilities)[number];
-// defaultStore.state.visibilityがstringなためstringも受け付けている
-function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
+function smallerVisibility(a: Visibility, b: Visibility): Visibility {
if (a === 'specified' || b === 'specified') return 'specified';
if (a === 'followers' || b === 'followers') return 'followers';
if (a === 'home' || b === 'home') return 'home';
@@ -519,6 +518,7 @@ export function getRenoteMenu(props: {
const channelRenoteItems: MenuItem[] = [];
const normalRenoteItems: MenuItem[] = [];
+ const normalExternalChannelRenoteItems: MenuItem[] = [];
if (appearNote.channel) {
channelRenoteItems.push(...[{
@@ -597,12 +597,49 @@ export function getRenoteMenu(props: {
});
},
}]);
+
+ normalExternalChannelRenoteItems.push({
+ type: 'parent',
+ icon: 'ti ti-repeat',
+ text: appearNote.channel ? i18n.ts.renoteToOtherChannel : i18n.ts.renoteToChannel,
+ children: async () => {
+ const channels = await misskeyApi('channels/my-favorites', {
+ limit: 30,
+ });
+ return channels.filter((channel) => {
+ if (!appearNote.channelId) return true;
+ return channel.id !== appearNote.channelId;
+ }).map((channel) => ({
+ text: channel.name,
+ action: () => {
+ const el = props.renoteButton.value;
+ if (el) {
+ const rect = el.getBoundingClientRect();
+ const x = rect.left + (el.offsetWidth / 2);
+ const y = rect.top + (el.offsetHeight / 2);
+ os.popup(MkRippleEffect, { x, y }, {}, 'end');
+ }
+
+ if (!props.mock) {
+ misskeyApi('notes/create', {
+ renoteId: appearNote.id,
+ channelId: channel.id,
+ }).then(() => {
+ os.toast(i18n.tsx.renotedToX({ name: channel.name }));
+ });
+ }
+ },
+ }));
+ },
+ });
}
const renoteItems = [
...normalRenoteItems,
...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] as MenuItem[] : [],
...channelRenoteItems,
+ ...(normalExternalChannelRenoteItems.length > 0 && (normalRenoteItems.length > 0 || channelRenoteItems.length > 0)) ? [{ type: 'divider' }] as MenuItem[] : [],
+ ...normalExternalChannelRenoteItems,
];
return {
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index c14f75f38..3e031d232 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -272,7 +272,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
text: r.name,
action: async () => {
const { canceled, result: period } = await os.select({
- title: i18n.ts.period,
+ title: i18n.ts.period + ': ' + r.name,
items: [{
value: 'indefinitely', text: i18n.ts.indefinitely,
}, {
diff --git a/packages/frontend/src/scripts/rate-limiter.ts b/packages/frontend/src/scripts/rate-limiter.ts
index 35b459579..3382e3b0e 100644
--- a/packages/frontend/src/scripts/rate-limiter.ts
+++ b/packages/frontend/src/scripts/rate-limiter.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: ISC
*
* This file is derived from the project that has licensed under the ISC license.
- * This file SHOULD NOT be considered as a part of the this project that has licensed under AGPL-3.0-only
+ * This file SHOULD NOT be considered as a part of this project that has licensed under AGPL-3.0-only
* Adapted from https://github.com/isaacs/ttlcache/blob/b6002f971e122e3b35e23d00ac6a8365d505c14d/examples/rate-limiter-window.ts
*/
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 3575e8026..e813722a6 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -94,7 +94,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
defaultNoteVisibility: {
where: 'account',
- default: 'public',
+ default: 'public' as (typeof Misskey.noteVisibilities)[number],
},
defaultNoteLocalOnly: {
where: 'account',
@@ -150,7 +150,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
visibility: {
where: 'deviceAccount',
- default: 'public' as 'public' | 'home' | 'followers' | 'specified',
+ default: 'public' as (typeof Misskey.noteVisibilities)[number],
},
localOnly: {
where: 'deviceAccount',
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index 9b510a629..839fa5faf 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -79,7 +79,12 @@ export function openInstanceMenu(ev: MouseEvent) {
text: i18n.ts.tools,
icon: 'ti ti-tool',
children: toolsMenuItems(),
- }, { type: 'divider' }, (instance.impressumUrl) ? {
+ }, { type: 'divider' }, {
+ type: 'link',
+ text: i18n.ts.inquiry,
+ icon: 'ti ti-help-circle',
+ to: '/contact',
+ }, (instance.impressumUrl) ? {
text: i18n.ts.impressum,
icon: 'ti ti-file-invoice',
action: () => {
@@ -98,8 +103,8 @@ export function openInstanceMenu(ev: MouseEvent) {
window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
},
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
- text: i18n.ts.help,
- icon: 'ti ti-help-circle',
+ text: i18n.ts.document,
+ icon: 'ti ti-bulb',
action: () => {
window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener');
},
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index fa1f0eb8c..e4e7505df 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -9,7 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -68,6 +69,7 @@ import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
+import { miLocalStorage } from '@/local-storage.js';
const iconOnly = ref(false);
@@ -173,6 +175,11 @@ function more(ev: MouseEvent) {
aspect-ratio: 1;
}
+ .instanceIconAlt {
+ display: inline-block;
+ width: 85%;
+ }
+
.bottom {
position: sticky;
bottom: 0;
diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue
index b973a4fd6..6e1d06eec 100644
--- a/packages/frontend/src/ui/_common_/statusbar-rss.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue
@@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only