Merge branch 'misskey-develop' into merge/2025-03-24
# Conflicts: # .github/workflows/api-misskey-js.yml # .github/workflows/changelog-check.yml # .github/workflows/check-misskey-js-autogen.yml # .github/workflows/get-api-diff.yml # .github/workflows/lint.yml # .github/workflows/locale.yml # .github/workflows/on-release-created.yml # .github/workflows/storybook.yml # .github/workflows/test-backend.yml # .github/workflows/test-federation.yml # .github/workflows/test-frontend.yml # .github/workflows/test-misskey-js.yml # .github/workflows/test-production.yml # .github/workflows/validate-api-json.yml # package.json # packages/backend/package.json # packages/backend/src/server/api/ApiCallService.ts # packages/backend/src/server/api/endpoints/drive/files/create.ts # packages/frontend-shared/js/url.ts # packages/frontend/package.json # packages/frontend/src/components/MkFileCaptionEditWindow.vue # packages/frontend/src/components/MkInfo.vue # packages/frontend/src/components/MkLink.vue # packages/frontend/src/components/MkNote.vue # packages/frontend/src/components/MkNotes.vue # packages/frontend/src/components/MkPageWindow.vue # packages/frontend/src/components/MkReactionsViewer.vue # packages/frontend/src/components/MkTimeline.vue # packages/frontend/src/components/MkUrlPreview.vue # packages/frontend/src/components/MkUserPopup.vue # packages/frontend/src/components/global/MkPageHeader.vue # packages/frontend/src/components/global/MkUrl.vue # packages/frontend/src/components/global/PageWithHeader.vue # packages/frontend/src/pages/about-misskey.vue # packages/frontend/src/pages/announcements.vue # packages/frontend/src/pages/antenna-timeline.vue # packages/frontend/src/pages/channel.vue # packages/frontend/src/pages/instance-info.vue # packages/frontend/src/pages/note.vue # packages/frontend/src/pages/page.vue # packages/frontend/src/pages/role.vue # packages/frontend/src/pages/tag.vue # packages/frontend/src/pages/timeline.vue # packages/frontend/src/pages/user-list-timeline.vue # packages/frontend/src/pages/user/followers.vue # packages/frontend/src/pages/user/following.vue # packages/frontend/src/pages/user/home.vue # packages/frontend/src/pages/user/index.vue # packages/frontend/src/ui/deck.vue # packages/misskey-js/generator/package.json # pnpm-lock.yaml # scripts/changelog-checker/package-lock.json # scripts/changelog-checker/package.json
This commit is contained in:
commit
9c301fa5aa
255 changed files with 4773 additions and 4085 deletions
|
|
@ -37,17 +37,17 @@
|
|||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@swc/core-darwin-arm64": "1.11.18",
|
||||
"@swc/core-darwin-x64": "1.11.18",
|
||||
"@swc/core-darwin-arm64": "1.11.22",
|
||||
"@swc/core-darwin-x64": "1.11.22",
|
||||
"@swc/core-freebsd-x64": "1.3.11",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.11.18",
|
||||
"@swc/core-linux-arm64-gnu": "1.11.18",
|
||||
"@swc/core-linux-arm64-musl": "1.11.18",
|
||||
"@swc/core-linux-x64-gnu": "1.11.18",
|
||||
"@swc/core-linux-x64-musl": "1.11.18",
|
||||
"@swc/core-win32-arm64-msvc": "1.11.18",
|
||||
"@swc/core-win32-ia32-msvc": "1.11.18",
|
||||
"@swc/core-win32-x64-msvc": "1.11.18",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.11.22",
|
||||
"@swc/core-linux-arm64-gnu": "1.11.22",
|
||||
"@swc/core-linux-arm64-musl": "1.11.22",
|
||||
"@swc/core-linux-x64-gnu": "1.11.22",
|
||||
"@swc/core-linux-x64-musl": "1.11.22",
|
||||
"@swc/core-win32-arm64-msvc": "1.11.22",
|
||||
"@swc/core-win32-ia32-msvc": "1.11.22",
|
||||
"@swc/core-win32-x64-msvc": "1.11.22",
|
||||
"bufferutil": "4.0.9",
|
||||
"slacc-android-arm-eabi": "0.0.10",
|
||||
"slacc-android-arm64": "0.0.10",
|
||||
|
|
@ -65,8 +65,8 @@
|
|||
"utf-8-validate": "6.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.782.0",
|
||||
"@aws-sdk/lib-storage": "3.782.0",
|
||||
"@aws-sdk/client-s3": "3.797.0",
|
||||
"@aws-sdk/lib-storage": "3.797.0",
|
||||
"@discordapp/twemoji": "15.1.0",
|
||||
"@fastify/accepts": "5.0.2",
|
||||
"@fastify/cookie": "11.0.2",
|
||||
|
|
@ -78,17 +78,17 @@
|
|||
"@fastify/view": "10.0.2",
|
||||
"@misskey-dev/sharp-read-bmp": "1.3.0",
|
||||
"@misskey-dev/summaly": "5.2.1",
|
||||
"@nestjs/common": "11.0.16",
|
||||
"@nestjs/core": "11.0.15",
|
||||
"@nestjs/testing": "11.0.15",
|
||||
"@nestjs/common": "11.1.0",
|
||||
"@nestjs/core": "11.1.0",
|
||||
"@nestjs/testing": "11.1.0",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "8.55.0",
|
||||
"@sentry/profiling-node": "8.55.0",
|
||||
"@simplewebauthn/server": "12.0.0",
|
||||
"@sinonjs/fake-timers": "11.3.1",
|
||||
"@smithy/node-http-handler": "2.5.0",
|
||||
"@swc/cli": "0.6.0",
|
||||
"@swc/core": "1.11.18",
|
||||
"@swc/cli": "0.7.3",
|
||||
"@swc/core": "1.11.22",
|
||||
"@transfem-org/sfm-js": "0.24.6",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"@types/redis-info": "3.0.3",
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.3",
|
||||
"bullmq": "5.48.1",
|
||||
"bullmq": "5.51.1",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"canvas": "^3.1.0",
|
||||
"cbor": "9.0.2",
|
||||
|
|
@ -127,18 +127,18 @@
|
|||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-link-header": "1.1.3",
|
||||
"ioredis": "5.6.0",
|
||||
"ioredis": "5.6.1",
|
||||
"ip-cidr": "4.0.2",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"is-svg": "5.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "26.0.0",
|
||||
"jsdom": "26.1.0",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.3",
|
||||
"jsrsasign": "11.1.0",
|
||||
"juice": "11.0.1",
|
||||
"megalodon": "workspace:*",
|
||||
"meilisearch": "0.49.0",
|
||||
"meilisearch": "0.50.0",
|
||||
"microformats-parser": "2.0.2",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "workspace:*",
|
||||
|
|
@ -148,14 +148,14 @@
|
|||
"nanoid": "5.1.5",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.10.0",
|
||||
"nodemailer": "6.10.1",
|
||||
"oauth": "0.10.2",
|
||||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.4.0",
|
||||
"parse5": "7.2.1",
|
||||
"pg": "8.14.1",
|
||||
"parse5": "7.3.0",
|
||||
"pg": "8.15.6",
|
||||
"pkce-challenge": "4.1.0",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.2",
|
||||
"sanitize-html": "2.15.0",
|
||||
"sanitize-html": "2.16.0",
|
||||
"secure-json-parse": "3.0.2",
|
||||
"sharp": "0.34.1",
|
||||
"slacc": "0.0.10",
|
||||
|
|
@ -194,10 +194,10 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@nestjs/platform-express": "10.4.15",
|
||||
"@sentry/vue": "9.12.0",
|
||||
"@nestjs/platform-express": "10.4.17",
|
||||
"@sentry/vue": "9.14.0",
|
||||
"@simplewebauthn/types": "12.0.0",
|
||||
"@swc/jest": "0.2.37",
|
||||
"@swc/jest": "0.2.38",
|
||||
"@types/accepts": "1.3.7",
|
||||
"@types/archiver": "6.0.3",
|
||||
"@types/bcryptjs": "2.4.6",
|
||||
|
|
@ -214,12 +214,12 @@
|
|||
"@types/jsrsasign": "10.5.15",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "22.14.0",
|
||||
"@types/node": "22.15.2",
|
||||
"@types/nodemailer": "6.4.17",
|
||||
"@types/oauth": "0.9.6",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.11.11",
|
||||
"@types/pg": "8.11.14",
|
||||
"@types/proxy-addr": "^2.0.3",
|
||||
"@types/pug": "2.0.10",
|
||||
"@types/qrcode": "1.5.5",
|
||||
|
|
@ -230,14 +230,15 @@
|
|||
"@types/semver": "7.7.0",
|
||||
"@types/simple-oauth2": "5.0.7",
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
"@types/supertest": "6.0.3",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/tmp": "0.2.6",
|
||||
"@types/uuid": "^9.0.4",
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.29.1",
|
||||
"@typescript-eslint/parser": "8.29.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.0",
|
||||
"@typescript-eslint/parser": "8.31.0",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
|
|
@ -245,8 +246,9 @@
|
|||
"fkill": "9.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"nodemon": "3.1.9",
|
||||
"nodemon": "3.1.10",
|
||||
"pid-port": "1.0.2",
|
||||
"simple-oauth2": "5.1.0"
|
||||
"simple-oauth2": "5.1.0",
|
||||
"supertest": "7.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -515,9 +515,16 @@ export class DriveService {
|
|||
|
||||
const policies = await this.roleService.getUserPolicies(user.id);
|
||||
const driveCapacity = 1024 * 1024 * policies.driveCapacityMb;
|
||||
const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb;
|
||||
this.registerLogger.debug('drive capacity override applied');
|
||||
this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
|
||||
|
||||
if (maxFileSize < info.size) {
|
||||
if (isLocalUser) {
|
||||
throw new IdentifiableError('f9e4e5f3-4df4-40b5-b400-f236945f7073', 'Max file size exceeded.');
|
||||
}
|
||||
}
|
||||
|
||||
// If usage limit exceeded
|
||||
if (driveCapacity < usage + info.size) {
|
||||
if (isLocalUser) {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import { DI } from '@/di-symbols.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
|
@ -30,6 +32,7 @@ type TimelineOptions = {
|
|||
alwaysIncludeMyNotes?: boolean;
|
||||
ignoreAuthorFromBlock?: boolean;
|
||||
ignoreAuthorFromMute?: boolean;
|
||||
ignoreAuthorFromInstanceBlock?: boolean;
|
||||
excludeNoFiles?: boolean;
|
||||
excludeReplies?: boolean;
|
||||
excludeBots?: boolean;
|
||||
|
|
@ -43,9 +46,13 @@ export class FanoutTimelineEndpointService {
|
|||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private cacheService: CacheService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +132,19 @@ export class FanoutTimelineEndpointService {
|
|||
};
|
||||
}
|
||||
|
||||
{
|
||||
const parentFilter = filter;
|
||||
filter = (note) => {
|
||||
if (!ps.ignoreAuthorFromInstanceBlock) {
|
||||
if (this.utilityService.isBlockedHost(this.meta.blockedHosts, note.userHost)) return false;
|
||||
}
|
||||
if (note.userId !== note.renoteUserId && this.utilityService.isBlockedHost(this.meta.blockedHosts, note.renoteUserHost)) return false;
|
||||
if (note.userId !== note.replyUserId && this.utilityService.isBlockedHost(this.meta.blockedHosts, note.replyUserHost)) return false;
|
||||
|
||||
return parentFilter(note);
|
||||
};
|
||||
}
|
||||
|
||||
const redisTimeline: MiNote[] = [];
|
||||
let readFromRedis = 0;
|
||||
let lastSuccessfulRate = 1; // rateをキャッシュする?
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export const webpDefault: sharp.WebpOptions = {
|
|||
smartSubsample: true,
|
||||
mixed: true,
|
||||
effort: 2,
|
||||
loop: 0,
|
||||
};
|
||||
|
||||
export const avifDefault: sharp.AvifOptions = {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { Brackets, ObjectLiteral } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js';
|
||||
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository, MiMeta } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { SelectQueryBuilder } from 'typeorm';
|
||||
|
|
@ -36,6 +36,9 @@ export class QueryService {
|
|||
@Inject(DI.renoteMutingsRepository)
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
|
@ -251,4 +254,37 @@ export class QueryService {
|
|||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateBlockedHostQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean): void {
|
||||
let nonBlockedHostQuery: (part: string) => string;
|
||||
if (this.meta.blockedHosts.length === 0) {
|
||||
nonBlockedHostQuery = () => '1=1';
|
||||
} else {
|
||||
nonBlockedHostQuery = (match: string) => `${match} NOT ILIKE ALL(ARRAY[:...blocked])`;
|
||||
q.setParameters({ blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) });
|
||||
}
|
||||
|
||||
if (excludeAuthor) {
|
||||
const instanceSuspension = (user: string) => new Brackets(qb => qb
|
||||
.where(`note.${user}Id IS NULL`) // no corresponding user
|
||||
.orWhere(`note.userId = note.${user}Id`)
|
||||
.orWhere(`note.${user}Host IS NULL`) // local
|
||||
.orWhere(nonBlockedHostQuery(`note.${user}Host`)));
|
||||
|
||||
q
|
||||
.andWhere(instanceSuspension('replyUser'))
|
||||
.andWhere(instanceSuspension('renoteUser'));
|
||||
} else {
|
||||
const instanceSuspension = (user: string) => new Brackets(qb => qb
|
||||
.where(`note.${user}Id IS NULL`) // no corresponding user
|
||||
.orWhere(`note.${user}Host IS NULL`) // local
|
||||
.orWhere(nonBlockedHostQuery(`note.${user}Host`)));
|
||||
|
||||
q
|
||||
.andWhere(instanceSuspension('user'))
|
||||
.andWhere(instanceSuspension('replyUser'))
|
||||
.andWhere(instanceSuspension('renoteUser'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export type RolePolicies = {
|
|||
canUseTranslator: boolean;
|
||||
canHideAds: boolean;
|
||||
driveCapacityMb: number;
|
||||
maxFileSizeMb: number;
|
||||
alwaysMarkNsfw: boolean;
|
||||
canUpdateBioMedia: boolean;
|
||||
pinLimit: number;
|
||||
|
|
@ -86,6 +87,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
canUseTranslator: true,
|
||||
canHideAds: false,
|
||||
driveCapacityMb: 100,
|
||||
maxFileSizeMb: 10,
|
||||
alwaysMarkNsfw: false,
|
||||
canUpdateBioMedia: true,
|
||||
pinLimit: 5,
|
||||
|
|
@ -399,6 +401,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||
maxFileSizeMb: calc('maxFileSizeMb', vs => Math.max(...vs)),
|
||||
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
|
||||
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
||||
|
|
|
|||
|
|
@ -300,6 +300,7 @@ export class SearchService {
|
|||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
|
|
@ -366,9 +367,14 @@ export class SearchService {
|
|||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
])
|
||||
: [new Set<string>(), new Set<string>()];
|
||||
const notes = (await this.notesRepository.findBy({
|
||||
id: In(res.hits.map(x => x.id)),
|
||||
})).filter(note => {
|
||||
|
||||
const query = this.notesRepository.createQueryBuilder('note');
|
||||
|
||||
query.where('note.id IN (:...noteIds)', { noteIds: res.hits.map(x => x.id) });
|
||||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
|
||||
const notes = (await query.getMany()).filter(note => {
|
||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -228,6 +228,10 @@ export const packedRolePoliciesSchema = {
|
|||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
maxFileSizeMb: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
alwaysMarkNsfw: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async launch(): Promise<void> {
|
||||
public async launch() {
|
||||
const fastify = Fastify({
|
||||
trustProxy: true,
|
||||
logger: false,
|
||||
|
|
@ -135,8 +135,8 @@ export class ServerService implements OnApplicationShutdown {
|
|||
reply.header('content-type', 'text/plain; charset=utf-8');
|
||||
reply.header('link', `<${encodeURI(location)}>; rel="canonical"`);
|
||||
done(null, [
|
||||
"Refusing to relay remote ActivityPub object lookup.",
|
||||
"",
|
||||
'Refusing to relay remote ActivityPub object lookup.',
|
||||
'',
|
||||
`Please remove 'application/activity+json' and 'application/ld+json' from the Accept header or fetch using the authoritative URL at ${location}.`,
|
||||
].join('\n'));
|
||||
});
|
||||
|
|
@ -304,6 +304,7 @@ export class ServerService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
await fastify.ready();
|
||||
return fastify;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import * as stream from 'node:stream/promises';
|
||||
import { Transform } from 'node:stream';
|
||||
import { type MultipartFile } from '@fastify/multipart';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { AttachmentFile } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
|
|
@ -16,7 +19,7 @@ import type Logger from '@/logger.js';
|
|||
import type { MiMeta, UserIpsRepository } from '@/models/_.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { type RolePolicies, RoleService } from '@/core/RoleService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
|
||||
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
|
||||
|
|
@ -191,18 +194,6 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
return;
|
||||
}
|
||||
|
||||
const [path, cleanup] = await createTemp();
|
||||
await stream.pipeline(multipartData.file, fs.createWriteStream(path));
|
||||
|
||||
// ファイルサイズが制限を超えていた場合
|
||||
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
|
||||
if (multipartData.file.truncated) {
|
||||
cleanup();
|
||||
reply.code(413);
|
||||
reply.send();
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = {} as Record<string, unknown>;
|
||||
for (const [k, v] of Object.entries(multipartData.fields)) {
|
||||
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
|
||||
|
|
@ -217,10 +208,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
return;
|
||||
}
|
||||
this.authenticateService.authenticate(token).then(([user, app]) => {
|
||||
this.call(endpoint, user, app, fields, {
|
||||
name: multipartData.filename,
|
||||
path: path,
|
||||
}, request, reply).then((res) => {
|
||||
this.call(endpoint, user, app, fields, multipartData, request, reply).then((res) => {
|
||||
this.send(reply, res);
|
||||
}).catch((err: ApiError) => {
|
||||
this.#sendApiError(reply, err);
|
||||
|
|
@ -290,10 +278,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
user: MiLocalUser | null | undefined,
|
||||
token: MiAccessToken | null | undefined,
|
||||
data: any,
|
||||
file: {
|
||||
name: string;
|
||||
path: string;
|
||||
} | null,
|
||||
multipartFile: MultipartFile | null,
|
||||
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
|
|
@ -369,6 +354,37 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
// Cast non JSON input
|
||||
if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) {
|
||||
for (const k of Object.keys(ep.params.properties)) {
|
||||
const param = ep.params.properties![k];
|
||||
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
|
||||
try {
|
||||
data[k] = JSON.parse(data[k]);
|
||||
} catch (e) {
|
||||
throw new ApiError({
|
||||
message: 'Invalid param.',
|
||||
code: 'INVALID_PARAM',
|
||||
id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
|
||||
}, {
|
||||
param: k,
|
||||
reason: `cannot cast to ${param.type}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind))
|
||||
|| (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) {
|
||||
throw new ApiError({
|
||||
message: 'Your app does not have the necessary permissions to use this endpoint.',
|
||||
code: 'PERMISSION_DENIED',
|
||||
kind: 'permission',
|
||||
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
|
||||
});
|
||||
}
|
||||
|
||||
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user!.id)) {
|
||||
const myRoles = await this.roleService.getUserRoles(user!.id);
|
||||
if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
|
||||
|
|
@ -402,49 +418,91 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind))
|
||||
|| (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) {
|
||||
throw new ApiError({
|
||||
message: 'Your app does not have the necessary permissions to use this endpoint.',
|
||||
code: 'PERMISSION_DENIED',
|
||||
kind: 'permission',
|
||||
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
|
||||
});
|
||||
}
|
||||
|
||||
// Cast non JSON input
|
||||
if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) {
|
||||
for (const k of Object.keys(ep.params.properties)) {
|
||||
const param = ep.params.properties![k];
|
||||
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
|
||||
try {
|
||||
data[k] = JSON.parse(data[k]);
|
||||
} catch (e) {
|
||||
throw new ApiError({
|
||||
message: 'Invalid param.',
|
||||
code: 'INVALID_PARAM',
|
||||
id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
|
||||
}, {
|
||||
param: k,
|
||||
reason: `cannot cast to ${param.type}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let attachmentFile: AttachmentFile | null = null;
|
||||
let cleanup = () => {};
|
||||
if (ep.meta.requireFile && request.method === 'POST' && multipartFile) {
|
||||
const policies = await this.roleService.getUserPolicies(user!.id);
|
||||
const result = await this.handleAttachmentFile(
|
||||
Math.min((policies.maxFileSizeMb * 1024 * 1024), this.config.maxFileSize),
|
||||
multipartFile,
|
||||
);
|
||||
attachmentFile = result.attachmentFile;
|
||||
cleanup = result.cleanup;
|
||||
}
|
||||
|
||||
// API invoking
|
||||
if (this.config.sentryForBackend) {
|
||||
return await Sentry.startSpan({
|
||||
name: 'API: ' + ep.name,
|
||||
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
|
||||
}, () => {
|
||||
return ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
|
||||
.finally(() => cleanup());
|
||||
});
|
||||
} else {
|
||||
return await ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
|
||||
return await ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
|
||||
.finally(() => cleanup());
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async handleAttachmentFile(
|
||||
fileSizeLimit: number,
|
||||
multipartFile: MultipartFile,
|
||||
) {
|
||||
function createTooLongError() {
|
||||
return new ApiError({
|
||||
httpStatusCode: 413,
|
||||
kind: 'client',
|
||||
message: 'File size is too large.',
|
||||
code: 'FILE_SIZE_TOO_LARGE',
|
||||
id: 'ff827ce8-9b4b-4808-8511-422222a3362f',
|
||||
});
|
||||
}
|
||||
|
||||
function createLimitStream(limit: number) {
|
||||
let total = 0;
|
||||
|
||||
return new Transform({
|
||||
transform(chunk, _, callback) {
|
||||
total += chunk.length;
|
||||
if (total > limit) {
|
||||
callback(createTooLongError());
|
||||
} else {
|
||||
callback(null, chunk);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [path, cleanup] = await createTemp();
|
||||
try {
|
||||
await stream.pipeline(
|
||||
multipartFile.file,
|
||||
createLimitStream(fileSizeLimit),
|
||||
fs.createWriteStream(path),
|
||||
);
|
||||
|
||||
// ファイルサイズが制限を超えていた場合
|
||||
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
|
||||
if (multipartFile.file.truncated) {
|
||||
throw createTooLongError();
|
||||
}
|
||||
} catch (err) {
|
||||
cleanup();
|
||||
throw err;
|
||||
}
|
||||
|
||||
return {
|
||||
attachmentFile: {
|
||||
name: multipartFile.filename,
|
||||
path,
|
||||
},
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
clearInterval(this.userIpHistoriesClearIntervalId);
|
||||
|
|
|
|||
|
|
@ -21,23 +21,23 @@ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
|||
|
||||
export type Response = Record<string, any> | void;
|
||||
|
||||
type File = {
|
||||
export type AttachmentFile = {
|
||||
name: string | null;
|
||||
path: string;
|
||||
};
|
||||
|
||||
// TODO: paramsの型をT['params']のスキーマ定義から推論する
|
||||
type Executor<T extends IEndpointMeta, Ps extends Schema> =
|
||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
||||
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
||||
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
||||
|
||||
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
|
||||
public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
|
||||
public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
|
||||
|
||||
constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
|
||||
const validate = ajv.compile(paramDef);
|
||||
|
||||
this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
|
||||
this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => {
|
||||
let cleanup: undefined | (() => void) = undefined;
|
||||
|
||||
if (meta.requireFile) {
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
|
||||
// https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
|
||||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ export const meta = {
|
|||
|
||||
kind: 'write:chat',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchMessage: {
|
||||
message: 'No such message.',
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ export const meta = {
|
|||
|
||||
kind: 'write:chat',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchMessage: {
|
||||
message: 'No such message.',
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ export const meta = {
|
|||
|
||||
kind: 'write:chat',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchMessage: {
|
||||
message: 'No such message.',
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ export const meta = {
|
|||
|
||||
kind: 'write:chat',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ export const meta = {
|
|||
|
||||
kind: 'write:chat',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ export const meta = {
|
|||
|
||||
kind: 'write:chat',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ export const meta = {
|
|||
|
||||
kind: 'write:chat',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ export const meta = {
|
|||
|
||||
kind: 'write:chat',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
|
||||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) {
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -62,6 +62,12 @@ export const meta = {
|
|||
code: 'COMMENT_TOO_LONG',
|
||||
id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5',
|
||||
},
|
||||
|
||||
maxFileSizeExceeded: {
|
||||
message: 'Cannot upload the file because it exceeds the maximum file size.',
|
||||
code: 'MAX_FILE_SIZE_EXCEEDED',
|
||||
id: 'b9d8c348-33f0-4673-b9a9-5d4da058977a',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
@ -128,6 +134,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (err instanceof IdentifiableError) {
|
||||
if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate);
|
||||
if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace);
|
||||
if (err.id === 'f9e4e5f3-4df4-40b5-b400-f236945f7073') throw new ApiError(meta.errors.maxFileSizeExceeded);
|
||||
}
|
||||
throw new ApiError();
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
|
@ -58,6 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private cacheService: CacheService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private featuredService: FeaturedService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let noteIds: string[];
|
||||
|
|
@ -100,6 +102,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
|
||||
const notes = (await query.getMany()).filter(note => {
|
||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}));
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}));
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
|
@ -55,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private noteEntityService: NoteEntityService,
|
||||
private featuredService: FeaturedService,
|
||||
private cacheService: CacheService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set<string>();
|
||||
|
|
@ -91,6 +93,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
|
||||
const notes = (await query.getMany()).filter(note => {
|
||||
if (me && isUserRelated(note, userIdsWhoBlockingMe, false)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false;
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
redisTimelines,
|
||||
useDbFallback: true,
|
||||
ignoreAuthorFromMute: true,
|
||||
ignoreAuthorFromInstanceBlock: true,
|
||||
excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies
|
||||
excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files
|
||||
excludePureRenotes: !ps.withRenotes,
|
||||
|
|
@ -216,6 +217,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query, true);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('reaction.note', 'note');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
|
||||
const reactions = (await query
|
||||
.limit(ps.limit)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ html {
|
|||
margin: auto;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ html.embed.noborder #splash {
|
|||
margin: auto;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,10 +74,6 @@ services:
|
|||
source: ../../../pnpm-workspace.yaml
|
||||
target: /misskey/pnpm-workspace.yaml
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../../scripts/dependency-patches
|
||||
target: /misskey/scripts/dependency-patches
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ./certificates/rootCA.crt
|
||||
target: /usr/local/share/ca-certificates/rootCA.crt
|
||||
|
|
|
|||
|
|
@ -70,10 +70,6 @@ services:
|
|||
source: ../../../pnpm-workspace.yaml
|
||||
target: /misskey/pnpm-workspace.yaml
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../../scripts/dependency-patches
|
||||
target: /misskey/scripts/dependency-patches
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ./certificates/rootCA.crt
|
||||
target: /usr/local/share/ca-certificates/rootCA.crt
|
||||
|
|
@ -118,10 +114,6 @@ services:
|
|||
source: ../../../pnpm-workspace.yaml
|
||||
target: /misskey/pnpm-workspace.yaml
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../../scripts/dependency-patches
|
||||
target: /misskey/scripts/dependency-patches
|
||||
read_only: true
|
||||
working_dir: /misskey
|
||||
command: >
|
||||
bash -c "
|
||||
|
|
|
|||
|
|
@ -159,8 +159,8 @@ describe('API', () => {
|
|||
user: { token: application3 },
|
||||
}, {
|
||||
status: 403,
|
||||
code: 'ROLE_PERMISSION_DENIED',
|
||||
id: 'c3d38592-54c0-429d-be96-5636b0431a61',
|
||||
code: 'PERMISSION_DENIED',
|
||||
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
|
||||
});
|
||||
|
||||
await failedApiCall({
|
||||
|
|
|
|||
108
packages/backend/test/unit/server/api/drive/files/create.ts
Normal file
108
packages/backend/test/unit/server/api/drive/files/create.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { S3Client } from '@aws-sdk/client-s3';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { mockClient } from 'aws-sdk-client-mock';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import request from 'supertest';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { MiRole, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { ServerModule } from '@/server/ServerModule.js';
|
||||
import { ServerService } from '@/server/ServerService.js';
|
||||
|
||||
describe('/drive/files/create', () => {
|
||||
let module: TestingModule;
|
||||
let server: FastifyInstance;
|
||||
const s3Mock = mockClient(S3Client);
|
||||
let roleService: RoleService;
|
||||
|
||||
let root: MiUser;
|
||||
let role_tinyAttachment: MiRole;
|
||||
|
||||
beforeAll(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [GlobalModule, CoreModule, ServerModule],
|
||||
}).compile();
|
||||
module.enableShutdownHooks();
|
||||
|
||||
const serverService = module.get<ServerService>(ServerService);
|
||||
server = await serverService.launch();
|
||||
|
||||
const usersRepository = module.get<UsersRepository>(DI.usersRepository);
|
||||
root = await usersRepository.insert({
|
||||
id: 'root',
|
||||
username: 'root',
|
||||
usernameLower: 'root',
|
||||
token: '1234567890123456',
|
||||
}).then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const userProfilesRepository = module.get<UserProfilesRepository>(DI.userProfilesRepository);
|
||||
await userProfilesRepository.insert({
|
||||
userId: root.id,
|
||||
});
|
||||
|
||||
roleService = module.get<RoleService>(RoleService);
|
||||
role_tinyAttachment = await roleService.create({
|
||||
name: 'test-role001',
|
||||
description: 'Test role001 description',
|
||||
target: 'manual',
|
||||
policies: {
|
||||
maxFileSizeMb: {
|
||||
useDefault: false,
|
||||
priority: 1,
|
||||
// 10byte
|
||||
value: 10 / 1024 / 1024,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
s3Mock.reset();
|
||||
await roleService.unassign(root.id, role_tinyAttachment.id).catch(() => {});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('200 ok', async () => {
|
||||
const result = await request(server.server)
|
||||
.post('/api/drive/files/create')
|
||||
.set('Content-Type', 'multipart/form-data')
|
||||
.set('Authorization', `Bearer ${root.token}`)
|
||||
.attach('file', Buffer.from('a'.repeat(1024 * 1024)));
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('200 ok(with role)', async () => {
|
||||
await roleService.assign(root.id, role_tinyAttachment.id);
|
||||
|
||||
const result = await request(server.server)
|
||||
.post('/api/drive/files/create')
|
||||
.set('Content-Type', 'multipart/form-data')
|
||||
.set('Authorization', `Bearer ${root.token}`)
|
||||
.attach('file', Buffer.from('a'.repeat(10)));
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('413 too large', async () => {
|
||||
await roleService.assign(root.id, role_tinyAttachment.id);
|
||||
|
||||
const result = await request(server.server)
|
||||
.post('/api/drive/files/create')
|
||||
.set('Content-Type', 'multipart/form-data')
|
||||
.set('Authorization', `Bearer ${root.token}`)
|
||||
.attach('file', Buffer.from('a'.repeat(11)));
|
||||
expect(result.statusCode).toBe(413);
|
||||
expect(result.body.error.code).toBe('FILE_SIZE_TOO_LARGE');
|
||||
});
|
||||
});
|
||||
|
|
@ -26,15 +26,15 @@
|
|||
"json5": "2.2.3",
|
||||
"misskey-js": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.39.0",
|
||||
"sass": "1.86.3",
|
||||
"shiki": "3.2.2",
|
||||
"rollup": "4.40.0",
|
||||
"sass": "1.87.0",
|
||||
"shiki": "3.3.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.15",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.8.3",
|
||||
"uuid": "11.1.0",
|
||||
"vite": "6.3.1",
|
||||
"vite": "6.3.3",
|
||||
"vue": "3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -42,13 +42,13 @@
|
|||
"@testing-library/vue": "8.1.0",
|
||||
"@types/estree": "1.0.7",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "22.14.0",
|
||||
"@types/node": "22.15.2",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.29.1",
|
||||
"@typescript-eslint/parser": "8.29.1",
|
||||
"@vitest/coverage-v8": "3.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.0",
|
||||
"@typescript-eslint/parser": "8.31.0",
|
||||
"@vitest/coverage-v8": "3.1.2",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"acorn": "8.14.1",
|
||||
"cross-env": "7.0.3",
|
||||
|
|
@ -58,13 +58,13 @@
|
|||
"happy-dom": "17.4.4",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.7.3",
|
||||
"nodemon": "3.1.9",
|
||||
"msw": "2.7.5",
|
||||
"nodemon": "3.1.10",
|
||||
"prettier": "3.5.3",
|
||||
"start-server-and-test": "2.0.11",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vue-component-type-helpers": "2.2.8",
|
||||
"vue-component-type-helpers": "2.2.10",
|
||||
"vue-eslint-parser": "10.1.3",
|
||||
"vue-tsc": "2.2.8"
|
||||
"vue-tsc": "2.2.10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,30 @@
|
|||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean {
|
||||
const collapsed = note.cw == null && (
|
||||
(note.text != null && (
|
||||
(note.text.includes('$[x2')) ||
|
||||
(note.text.includes('$[x3')) ||
|
||||
(note.text.includes('$[x4')) ||
|
||||
(note.text.includes('$[scale')) ||
|
||||
(note.text.split('\n').length > 9) ||
|
||||
(note.text.length > 500) ||
|
||||
(urls.length >= 4)
|
||||
)) || (note.files != null && note.files.length >= 5)
|
||||
);
|
||||
if (note.cw != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return collapsed;
|
||||
if (note.text != null) {
|
||||
if (
|
||||
note.text.includes('$[x2') ||
|
||||
note.text.includes('$[x3') ||
|
||||
note.text.includes('$[x4') ||
|
||||
note.text.includes('$[scale') ||
|
||||
note.text.split('\n').length > 9 ||
|
||||
note.text.length > 500
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (urls.length >= 4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (note.files != null && note.files.length >= 5) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ export const ROLE_POLICIES = [
|
|||
'canUseTranslator',
|
||||
'canHideAds',
|
||||
'driveCapacityMb',
|
||||
'maxFileSizeMb',
|
||||
'alwaysMarkNsfw',
|
||||
'canUpdateBioMedia',
|
||||
'pinLimit',
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@
|
|||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.14.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.29.1",
|
||||
"@typescript-eslint/parser": "8.29.1",
|
||||
"esbuild": "0.25.2",
|
||||
"@types/node": "22.15.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.0",
|
||||
"@typescript-eslint/parser": "8.31.0",
|
||||
"esbuild": "0.25.3",
|
||||
"eslint-plugin-vue": "10.0.0",
|
||||
"nodemon": "3.1.9",
|
||||
"nodemon": "3.1.10",
|
||||
"typescript": "5.8.3",
|
||||
"vue-eslint-parser": "10.1.3"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ await fs.readFile(
|
|||
'../../locales/ja-JP.yml',
|
||||
'assets/**',
|
||||
'public/**',
|
||||
'../../pnpm-lock.yaml',
|
||||
'package.json',
|
||||
]).length
|
||||
) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/pluginutils": "5.1.4",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2024.10.15",
|
||||
"@sentry/vue": "9.12.0",
|
||||
"@sentry/vue": "9.14.0",
|
||||
"@syuilo/aiscript": "0.19.0",
|
||||
"@transfem-org/sfm-js": "0.24.6",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
|
|
@ -36,12 +36,12 @@
|
|||
"broadcast-channel": "7.1.0",
|
||||
"buraha": "0.0.1",
|
||||
"canvas-confetti": "1.9.3",
|
||||
"chart.js": "4.4.8",
|
||||
"chart.js": "4.4.9",
|
||||
"chartjs-adapter-date-fns": "3.0.0",
|
||||
"chartjs-chart-matrix": "2.1.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"chromatic": "11.28.0",
|
||||
"chromatic": "11.28.2",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "2.0.0",
|
||||
"date-fns": "4.1.0",
|
||||
|
|
@ -61,13 +61,13 @@
|
|||
"moment": "^2.30.1",
|
||||
"photoswipe": "5.4.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.39.0",
|
||||
"sanitize-html": "2.15.0",
|
||||
"sass": "1.86.3",
|
||||
"shiki": "3.2.2",
|
||||
"rollup": "4.40.0",
|
||||
"sanitize-html": "2.16.0",
|
||||
"sass": "1.87.0",
|
||||
"shiki": "3.3.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.175.0",
|
||||
"three": "0.176.0",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.15",
|
||||
|
|
@ -75,13 +75,13 @@
|
|||
"typescript": "5.8.3",
|
||||
"uuid": "11.1.0",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "6.3.1",
|
||||
"vite": "6.3.3",
|
||||
"vue": "3.5.13",
|
||||
"vuedraggable": "next",
|
||||
"wanakana": "5.3.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cypress": "13.15.2"
|
||||
"cypress": "14.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.2.1",
|
||||
|
|
@ -109,16 +109,16 @@
|
|||
"@types/katex": "^0.16.7",
|
||||
"@types/matter-js": "0.19.8",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "22.14.0",
|
||||
"@types/node": "22.15.2",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/sanitize-html": "2.15.0",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.29.1",
|
||||
"@typescript-eslint/parser": "8.29.1",
|
||||
"@vitest/coverage-v8": "3.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.0",
|
||||
"@typescript-eslint/parser": "8.31.0",
|
||||
"@vitest/coverage-v8": "3.1.2",
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"acorn": "8.14.1",
|
||||
|
|
@ -130,9 +130,9 @@
|
|||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"minimatch": "10.0.1",
|
||||
"msw": "2.7.3",
|
||||
"msw": "2.7.5",
|
||||
"msw-storybook-addon": "2.0.4",
|
||||
"nodemon": "3.1.9",
|
||||
"nodemon": "3.1.10",
|
||||
"prettier": "3.5.3",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
|
|
@ -141,10 +141,10 @@
|
|||
"storybook": "8.6.12",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "3.1.1",
|
||||
"vitest": "3.1.2",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
"vue-component-type-helpers": "2.2.8",
|
||||
"vue-component-type-helpers": "2.2.10",
|
||||
"vue-eslint-parser": "10.1.3",
|
||||
"vue-tsc": "2.2.8"
|
||||
"vue-tsc": "2.2.10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</I18n>
|
||||
</template>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<div class="_gaps_m" :class="$style.root">
|
||||
<div class="">
|
||||
<MkTextarea v-model="comment">
|
||||
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkSpacer :contentMax="700">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 700px;">
|
||||
<div>
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="name">
|
||||
|
|
@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="emit('closed')">
|
||||
<template #header>:{{ emoji.name }}:</template>
|
||||
<template #default>
|
||||
<MkSpacer>
|
||||
<div class="_spacer">
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
<div :class="$style.emojiImgWrapper">
|
||||
<MkCustomEmoji :name="emoji.name" :normal="true" :useOriginalSize="true" style="height: 100%;"></MkCustomEmoji>
|
||||
|
|
@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@contextmenu.stop="onContextmenu"
|
||||
>
|
||||
<div ref="contents">
|
||||
<MkInfo v-if="!store.r.readDriveTip.value" closable @close="closeTip()"><div v-html="i18n.ts.driveAboutTip"></div></MkInfo>
|
||||
<div v-show="folders.length > 0" ref="foldersContainer" :class="$style.folders">
|
||||
<XFolder
|
||||
v-for="(f, i) in folders"
|
||||
|
|
@ -108,6 +109,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkInfo from './MkInfo.vue';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import XNavFolder from '@/components/MkDrive.navFolder.vue';
|
||||
import XFolder from '@/components/MkDrive.folder.vue';
|
||||
|
|
@ -121,6 +123,7 @@ import { uploadFile, uploads } from '@/utility/upload.js';
|
|||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { chooseFileFromPc } from '@/utility/select-file.js';
|
||||
import { store } from '@/store.js';
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
|
|
@ -723,6 +726,10 @@ function onContextmenu(ev: MouseEvent) {
|
|||
os.contextMenu(getMenu(), ev);
|
||||
}
|
||||
|
||||
function closeTip() {
|
||||
store.set('readDriveTip', true);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (prefer.s.enableInfiniteScroll && loadMoreFiles.value) {
|
||||
nextTick(() => {
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.describeFile }}</template>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<MkDriveFileThumbnail :file="file" fit="contain" style="height: 193px; margin-bottom: 16px;"/>
|
||||
<MkTextarea v-model="caption" autofocus :placeholder="i18n.ts.inputNewDescription" @keydown="onKeydown($event)">
|
||||
<template #label>{{ i18n.ts.caption }}</template>
|
||||
</MkTextarea>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
@enter="enter"
|
||||
@afterEnter="afterEnter"
|
||||
@leave="leave"
|
||||
@afterLeave="afterLeave"
|
||||
>
|
||||
<KeepAlive>
|
||||
<div v-show="opened">
|
||||
|
|
@ -45,9 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<MkSpacer v-if="withSpacer" :marginMin="spacerMin" :marginMax="spacerMax">
|
||||
<div v-if="withSpacer" class="_spacer" :style="{ '--MI_SPACER-min': props.spacerMin + 'px', '--MI_SPACER-max': props.spacerMax + 'px' }">
|
||||
<slot></slot>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div v-else>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
|
@ -90,32 +86,6 @@ const bgSame = ref(false);
|
|||
const opened = ref(props.defaultOpen);
|
||||
const openedAtLeastOnce = ref(props.defaultOpen);
|
||||
|
||||
function enter(el: Element) {
|
||||
if (!(el instanceof HTMLElement)) return;
|
||||
const elementHeight = el.getBoundingClientRect().height;
|
||||
el.style.height = '0';
|
||||
el.offsetHeight; // reflow
|
||||
el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`;
|
||||
}
|
||||
|
||||
function afterEnter(el: Element) {
|
||||
if (!(el instanceof HTMLElement)) return;
|
||||
el.style.height = '';
|
||||
}
|
||||
|
||||
function leave(el: Element) {
|
||||
if (!(el instanceof HTMLElement)) return;
|
||||
const elementHeight = el.getBoundingClientRect().height;
|
||||
el.style.height = `${elementHeight}px`;
|
||||
el.offsetHeight; // reflow
|
||||
el.style.height = '0';
|
||||
}
|
||||
|
||||
function afterLeave(el: Element) {
|
||||
if (!(el instanceof HTMLElement)) return;
|
||||
el.style.height = '';
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (!opened.value) {
|
||||
openedAtLeastOnce.value = true;
|
||||
|
|
@ -137,16 +107,18 @@ onMounted(() => {
|
|||
<style lang="scss" module>
|
||||
.transition_toggle_enterActive,
|
||||
.transition_toggle_leaveActive {
|
||||
overflow-y: clip;
|
||||
transition: opacity 0.3s, height 0.3s, transform 0.3s !important;
|
||||
overflow-y: hidden; // 子要素のmarginが突き出るため clip を使ってはいけない
|
||||
transition: opacity 0.3s, height 0.3s !important;
|
||||
}
|
||||
.transition_toggle_enterFrom,
|
||||
.transition_toggle_leaveTo {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: block;
|
||||
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
||||
}
|
||||
|
||||
.header {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
>
|
||||
<template #header>{{ i18n.ts.forgotPassword }}</template>
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<form v-if="instance.enableEmail" @submit.prevent="onSubmit">
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
|
||||
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-else>
|
||||
{{ i18n.ts._forgotPassword.contactAdmin }}
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ title }}
|
||||
</template>
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="32">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 32px;">
|
||||
<div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
|
||||
<template v-for="(v, k) in Object.fromEntries(Object.entries(form))">
|
||||
<template v-if="typeof v.hidden == 'function' ? v.hidden(values) : v.hidden"></template>
|
||||
|
|
@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ function closeInfo() {
|
|||
background: color-mix(in srgb, var(--MI_THEME-infoBg) 65%, transparent);
|
||||
color: var(--MI_THEME-infoFg);
|
||||
border-radius: var(--MI-radius);
|
||||
white-space: pre-wrap;
|
||||
z-index: 1;
|
||||
|
||||
&.warn {
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</bdi>
|
||||
</div>
|
||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
|
||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" style="margin-top: 6px;" :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
|
||||
<template #more>
|
||||
<MkA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</MkA>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
||||
</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
|
||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" style="margin-top: 6px;" :note="appearNote"/>
|
||||
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
|
||||
|
|
|
|||
|
|
@ -13,16 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<template #default="{ items: notes }">
|
||||
<component
|
||||
:is="prefer.s.animation ? TransitionGroup : 'div'"
|
||||
:class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap, [$style.reverse]: pagination.reversed }]"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
:moveClass=" $style.transition_x_move"
|
||||
tag="div"
|
||||
>
|
||||
<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap, [$style.reverse]: pagination.reversed }]">
|
||||
<template v-for="(note, i) in notes" :key="note.id">
|
||||
<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
|
||||
<DynamicNote :class="$style.note" :note="note as Misskey.entities.Note" :withHardMute="true"/>
|
||||
|
|
@ -32,20 +23,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<DynamicNote v-else :class="$style.note" :note="note as Misskey.entities.Note" :withHardMute="true" :data-scroll-anchor="note.id"/>
|
||||
</template>
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useTemplateRef, TransitionGroup } from 'vue';
|
||||
import { useTemplateRef } from 'vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import DynamicNote from '@/components/DynamicNote.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = defineProps<{
|
||||
pagination: Paging;
|
||||
|
|
@ -61,20 +51,6 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_x_move,
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
|
||||
}
|
||||
.transition_x_enterFrom,
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.transition_x_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.reverse {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
>
|
||||
<template #header>{{ i18n.ts.notificationSetting }}</template>
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<div class="_gaps_m">
|
||||
<MkInfo>{{ i18n.ts.notificationSettingDesc }}</MkInfo>
|
||||
<div class="_buttons">
|
||||
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.ts._notification._types[ntype] }}</MkSwitch>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</template>
|
||||
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.root" class="_forceShrinkSpacer">
|
||||
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :key="reloadCount" :router="windowRouter"/>
|
||||
<RouterView v-else :key="reloadCount" :router="windowRouter"/>
|
||||
</div>
|
||||
|
|
@ -121,7 +121,6 @@ provideMetadataReceiver((metadataGetter) => {
|
|||
provideReactiveMetadata(pageMetadata);
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
provide('shouldHeaderThin', true);
|
||||
provide(DI.forceSpacerMin, true);
|
||||
provide('shouldBackButton', false);
|
||||
|
||||
const contextmenu = computed(() => ([{
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
>
|
||||
<template #header>{{ i18n.ts.authentication }}</template>
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<div style="padding: 0 0 16px 0; text-align: center;">
|
||||
<img src="/client-assets/locked_with_key_3d.png" alt="🔐" style="display: block; margin: 0 auto; width: 48px;">
|
||||
<div style="margin-top: 16px;">{{ i18n.ts.authenticationRequiredToContinue }}</div>
|
||||
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" type="submit" primary rounded style="margin: 0 auto;"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton>
|
||||
</div>
|
||||
</form>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -182,7 +182,6 @@ if (!mock) {
|
|||
.root {
|
||||
display: inline-flex;
|
||||
height: 42px;
|
||||
margin: 2px;
|
||||
padding: 0 6px;
|
||||
font-size: 1.5em;
|
||||
border-radius: var(--MI-radius-sm);
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin: 4px -2px 0 -2px;
|
||||
gap: 4px;
|
||||
cursor: auto; /* not clickToOpen-able */
|
||||
|
||||
&:empty {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header>:{{ name }}:</template>
|
||||
|
||||
<div style="display: flex; flex-direction: column; min-height: 100%;">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px; flex-grow: 1;">
|
||||
<div class="_gaps_m">
|
||||
<div v-if="imgUrl != null" :class="$style.imgs">
|
||||
<div style="background: #000;" :class="$style.imgContainer">
|
||||
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #value>{{ license }}</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.footer">
|
||||
<MkButton primary rounded style="margin: 0 auto;" @click="done">
|
||||
<i class="ti ti-plus"></i> {{ i18n.ts.import }}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ title }}</template>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-else class="_gaps" :class="$style.root">
|
||||
<div :class="$style.header">
|
||||
|
|
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton @click="onCancelClicked">{{ i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
|
@ -51,7 +51,6 @@ import MkInfo from '@/components/MkInfo.vue';
|
|||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkSpacer from '@/components/global/MkSpacer.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkLoading from '@/components/global/MkLoading.vue';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@close="cancel()"
|
||||
>
|
||||
<template #header>{{ i18n.ts.schedulePostList }}</template>
|
||||
<MkSpacer :marginMin="14" :marginMax="16">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 14px; --MI_SPACER-max: 16px;">
|
||||
<MkPagination ref="paginationEl" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.banner">
|
||||
<i class="ti ti-user-edit"></i>
|
||||
</div>
|
||||
<MkSpacer :marginMin="20" :marginMax="32">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 32px;">
|
||||
<form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.invitationCode }}</template>
|
||||
|
|
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-else>{{ i18n.ts.start }}</template>
|
||||
</MkButton>
|
||||
</form>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.banner">
|
||||
<i class="ti ti-checklist"></i>
|
||||
</div>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<div class="_gaps_m">
|
||||
<div v-if="instance.disableRegistration || instance.federation !== 'all'" class="_gaps_s">
|
||||
<MkInfo v-if="instance.disableRegistration" warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
|
||||
|
|
@ -59,7 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton inline primary rounded gradate :disabled="!agreed" data-cy-signup-rules-continue @click="emit('done')">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<div style="display: flex; flex-direction: column; min-height: 100%;">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px; flex-grow: 1;">
|
||||
<MkLoading v-if="loading !== 0"/>
|
||||
<div v-else :class="$style.root" class="_gaps_m">
|
||||
<MkInput v-model="title">
|
||||
|
|
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.footer" class="_buttonsCenter">
|
||||
<MkButton primary rounded :disabled="disableSubmitButton" @click="onSubmitClicked">
|
||||
<i class="ti ti-check"></i>
|
||||
|
|
|
|||
|
|
@ -5,29 +5,55 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkPullToRefresh ref="prComponent" :refresher="() => reloadTimeline()">
|
||||
<MkNotes
|
||||
v-if="paginationQuery"
|
||||
ref="tlComponent"
|
||||
:pagination="paginationQuery"
|
||||
:noGap="!prefer.s.showGapBetweenNotesInTimeline"
|
||||
@queue="emit('queue', $event)"
|
||||
@status="prComponent?.setDisabled($event)"
|
||||
/>
|
||||
<MkPagination v-if="paginationQuery" ref="pagingComponent" :pagination="paginationQuery" @queue="emit('queue', $event)" @status="prComponent?.setDisabled($event)">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items: notes }">
|
||||
<component
|
||||
:is="prefer.s.animation ? TransitionGroup : 'div'"
|
||||
:class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap, [$style.reverse]: paginationQuery.reversed }]"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
:moveClass=" $style.transition_x_move"
|
||||
tag="div"
|
||||
>
|
||||
<template v-for="(note, i) in notes" :key="note.id">
|
||||
<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
|
||||
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
<div :class="$style.ad">
|
||||
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkNote v-else :class="$style.note" :note="note" :withHardMute="true" :data-scroll-anchor="note.id"/>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkPullToRefresh>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onUnmounted, provide, useTemplateRef } from 'vue';
|
||||
import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { BasicTimelineType } from '@/timelines.js';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
|
||||
|
|
@ -71,12 +97,12 @@ type TimelineQueryType = {
|
|||
};
|
||||
|
||||
const prComponent = useTemplateRef('prComponent');
|
||||
const tlComponent = useTemplateRef('tlComponent');
|
||||
const pagingComponent = useTemplateRef('pagingComponent');
|
||||
|
||||
let tlNotesCount = 0;
|
||||
|
||||
function prepend(note: Misskey.entities.Note) {
|
||||
if (tlComponent.value == null) return;
|
||||
if (pagingComponent.value == null) return;
|
||||
|
||||
tlNotesCount++;
|
||||
|
||||
|
|
@ -84,7 +110,7 @@ function prepend(note: Misskey.entities.Note) {
|
|||
note._shouldInsertAd_ = true;
|
||||
}
|
||||
|
||||
tlComponent.value.pagingComponent?.prepend(note);
|
||||
pagingComponent.value.prepend(note);
|
||||
|
||||
emit('note');
|
||||
|
||||
|
|
@ -96,6 +122,7 @@ function prepend(note: Misskey.entities.Note) {
|
|||
let connection: Misskey.ChannelConnection | null = null;
|
||||
let connection2: Misskey.ChannelConnection | null = null;
|
||||
let paginationQuery: Paging | null = null;
|
||||
const noGap = !prefer.s.showGapBetweenNotesInTimeline;
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
|
|
@ -290,11 +317,11 @@ onUnmounted(() => {
|
|||
|
||||
function reloadTimeline() {
|
||||
return new Promise<void>((res) => {
|
||||
if (tlComponent.value == null) return;
|
||||
if (pagingComponent.value == null) return;
|
||||
|
||||
tlNotesCount = 0;
|
||||
|
||||
tlComponent.value.pagingComponent?.reload().then(() => {
|
||||
pagingComponent.value.reload().then(() => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
|
|
@ -304,3 +331,56 @@ defineExpose({
|
|||
reloadTimeline,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_x_move,
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
|
||||
}
|
||||
.transition_x_enterFrom,
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.transition_x_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.reverse {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.root {
|
||||
container-type: inline-size;
|
||||
|
||||
&.noGap {
|
||||
background: var(--MI_THEME-panel);
|
||||
|
||||
.note {
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
.ad {
|
||||
padding: 8px;
|
||||
background-size: auto auto;
|
||||
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.noGap) {
|
||||
background: var(--MI_THEME-bg);
|
||||
|
||||
.note {
|
||||
background: var(--MI_THEME-panel);
|
||||
border-radius: var(--MI-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ad:empty {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
>
|
||||
<template #header>{{ title || i18n.ts.generateAccessToken }}</template>
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<div class="_gaps_m">
|
||||
<div v-if="information">
|
||||
<MkInfo warn>{{ information }}</MkInfo>
|
||||
|
|
@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-if="page === 0">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._landing.title }}</div>
|
||||
|
|
@ -37,15 +37,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
<MkButton style="margin: 0 auto;" transparent rounded @click="close(true)">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 1">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;" :class="$style.pageMain">
|
||||
<XNote phase="aboutNote"/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== 1" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
|
|
@ -58,12 +58,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-else-if="page === 2">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;" :class="$style.pageMain">
|
||||
<div class="_gaps">
|
||||
<XNote phase="howToReact" @reacted="isReactionTutorialPushed = true"/>
|
||||
<div v-if="!isReactionTutorialPushed">{{ i18n.ts._initialTutorial._reaction.reactToContinue }}</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
|
|
@ -76,9 +76,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-else-if="page === 3">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;" :class="$style.pageMain">
|
||||
<XTimeline/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
|
|
@ -91,9 +91,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-else-if="page === 4">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;" :class="$style.pageMain">
|
||||
<XPostNote/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
|
|
@ -106,12 +106,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-else-if="page === 5">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;" :class="$style.pageMain">
|
||||
<div class="_gaps">
|
||||
<XSensitive @succeeded="isSensitiveTutorialSucceeded = true"/>
|
||||
<div v-if="!isSensitiveTutorialSucceeded">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.doItToContinue }}</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
|
|
@ -124,7 +124,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-else-if="page === 6">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
|
||||
|
|
@ -139,7 +139,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Transition>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-else #header>New announcement</template>
|
||||
|
||||
<div>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="title">
|
||||
<template #label>{{ i18n.ts.title }}</template>
|
||||
|
|
@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkSwitch>
|
||||
<MkButton v-if="announcement" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.footer">
|
||||
<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.announcement ? i18n.ts.update : i18n.ts.create }}</MkButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-if="page === 0">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div>
|
||||
|
|
@ -41,15 +41,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
<MkButton style="margin: 0 auto;" transparent rounded @click="later(true)">{{ i18n.ts.later }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 1">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;" :class="$style.pageMain">
|
||||
<XProfile/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
|
|
@ -62,9 +62,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-else-if="page === 2">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;" :class="$style.pageMain">
|
||||
<XPrivacy/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
|
|
@ -76,9 +76,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
<template v-else-if="page === 3">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<XFollow/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
|
|
@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
<template v-else-if="page === 4">
|
||||
<div :class="$style.centerPage">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
|
||||
|
|
@ -100,13 +100,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 5">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
|
||||
|
|
@ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton rounded primary data-cy-user-setup-continue @click="setupComplete()">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Transition>
|
||||
|
|
@ -147,7 +147,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
|
||||
const page = ref(store.s.accountSetupWizard);
|
||||
|
||||
watch(page, () => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
MFM Cheatsheet
|
||||
</template>
|
||||
<MkStickyContainer>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 800px;">
|
||||
<div class="mfm-cheat-sheet">
|
||||
<div>{{ i18n.ts._mfm.intro }}</div>
|
||||
<br/>
|
||||
|
|
@ -402,7 +402,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -49,19 +49,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'vue';
|
||||
import { scrollToTop } from '@@/js/scroll.js';
|
||||
import XTabs from './MkPageHeader.tabs.vue';
|
||||
import type { Tab } from './MkPageHeader.tabs.vue';
|
||||
<script lang="ts">
|
||||
import type { PageHeaderItem } from '@/types/page-header.js';
|
||||
import type { PageMetadata } from '@/page.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { DI } from '@/di.js';
|
||||
import type { Tab } from './MkPageHeader.tabs.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
export type PageHeaderProps = {
|
||||
overridePageMetadata?: PageMetadata;
|
||||
tabs?: Tab[];
|
||||
tab?: string;
|
||||
|
|
@ -70,7 +63,19 @@ const props = withDefaults(defineProps<{
|
|||
hideTitle?: boolean;
|
||||
displayMyAvatar?: boolean;
|
||||
displayBackButton?: boolean;
|
||||
}>(), {
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'vue';
|
||||
import { scrollToTop } from '@@/js/scroll.js';
|
||||
import XTabs from './MkPageHeader.tabs.vue';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { DI } from '@/di.js';
|
||||
|
||||
const props = withDefaults(defineProps<PageHeaderProps>(), {
|
||||
tabs: () => ([] as Tab[]),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.rootMin]: forceSpacerMin }]">
|
||||
<div :class="$style.content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
import { DI } from '@/di.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
contentMax?: number | null;
|
||||
marginMin?: number;
|
||||
marginMax?: number;
|
||||
}>(), {
|
||||
contentMax: null,
|
||||
marginMin: 12,
|
||||
marginMax: 24,
|
||||
});
|
||||
|
||||
const forceSpacerMin = inject(DI.forceSpacerMin, false) || deviceKind === 'smartphone';
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
.rootMin {
|
||||
padding: v-bind('props.marginMin + "px"') !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
max-width: v-bind('props.contentMax + "px"');
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container (max-width: 450px) {
|
||||
.root {
|
||||
padding: v-bind('props.marginMin + "px"');
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 451px) {
|
||||
.root {
|
||||
padding: v-bind('props.marginMax + "px"');
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,9 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div ref="rootEl" :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']">
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="actions" :tabs="tabs" :displayBackButton="displayBackButton"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" v-bind="pageHeaderProps"/></template>
|
||||
<div :class="$style.body">
|
||||
<slot></slot>
|
||||
<MkSwiper v-if="swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs">
|
||||
<slot></slot>
|
||||
</MkSwiper>
|
||||
<slot v-else></slot>
|
||||
</div>
|
||||
<template #footer><slot name="footer"></slot></template>
|
||||
</MkStickyContainer>
|
||||
|
|
@ -16,22 +19,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTemplateRef } from 'vue';
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { scrollInContainer } from '@@/js/scroll.js';
|
||||
import type { PageHeaderItem } from '@/types/page-header.js';
|
||||
import type { Tab } from './MkPageHeader.tabs.vue';
|
||||
import type { PageHeaderProps } from './MkPageHeader.vue';
|
||||
import { useScrollPositionKeeper } from '@/use/use-scroll-position-keeper.js';
|
||||
import MkSwiper from '@/components/MkSwiper.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
tabs?: Tab[];
|
||||
actions?: PageHeaderItem[] | null;
|
||||
thin?: boolean;
|
||||
hideTitle?: boolean;
|
||||
displayMyAvatar?: boolean;
|
||||
const props = withDefaults(defineProps<PageHeaderProps & {
|
||||
reversed?: boolean;
|
||||
displayBackButton?: boolean;
|
||||
swipable?: boolean;
|
||||
}>(), {
|
||||
tabs: () => ([] as Tab[]),
|
||||
reversed: false,
|
||||
swipable: true,
|
||||
});
|
||||
|
||||
const pageHeaderProps = computed(() => {
|
||||
const { reversed, ...rest } = props;
|
||||
return rest;
|
||||
});
|
||||
|
||||
const tab = defineModel<string>('tab');
|
||||
|
|
@ -39,10 +44,18 @@ const rootEl = useTemplateRef('rootEl');
|
|||
|
||||
useScrollPositionKeeper(rootEl);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
router.useListener('same', () => {
|
||||
scrollToTop();
|
||||
});
|
||||
|
||||
function scrollToTop() {
|
||||
if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
scrollToTop: () => {
|
||||
if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'smooth' });
|
||||
},
|
||||
scrollToTop,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -51,7 +64,7 @@ defineExpose({
|
|||
|
||||
}
|
||||
|
||||
.body {
|
||||
.body, .swiper {
|
||||
min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div ref="rootEl" class="_pageContainer" :class="$style.root">
|
||||
<div class="_pageContainer" :class="$style.root">
|
||||
<KeepAlive :max="prefer.s.numberOfPageCache">
|
||||
<Suspense :timeout="0">
|
||||
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
||||
|
|
@ -42,37 +42,6 @@ provide(DI.viewId, viewId);
|
|||
const currentDepth = inject(DI.routerCurrentDepth, 0);
|
||||
provide(DI.routerCurrentDepth, currentDepth + 1);
|
||||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
onMounted(() => {
|
||||
if (prefer.s.animation) {
|
||||
rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入
|
||||
}
|
||||
});
|
||||
|
||||
// view-transition-newなどの<pt-name-selector>にはcss varが使えず、v-bindできないため直接スタイルを生成
|
||||
const viewTransitionStylesTag = window.document.createElement('style');
|
||||
viewTransitionStylesTag.textContent = `
|
||||
@keyframes ${viewId}-old {
|
||||
to { transform: scale(0.95); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes ${viewId}-new {
|
||||
from { transform: scale(0.95); opacity: 0; }
|
||||
}
|
||||
|
||||
::view-transition-old(${viewId}) {
|
||||
animation-duration: 0.2s;
|
||||
animation-name: ${viewId}-old;
|
||||
}
|
||||
|
||||
::view-transition-new(${viewId}) {
|
||||
animation-duration: 0.2s;
|
||||
animation-name: ${viewId}-new;
|
||||
}
|
||||
`;
|
||||
|
||||
window.document.head.appendChild(viewTransitionStylesTag);
|
||||
|
||||
const current = router.current!;
|
||||
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
|
||||
const currentPageProps = ref(current.props);
|
||||
|
|
@ -90,18 +59,7 @@ router.useListener('change', ({ resolved }) => {
|
|||
currentRoutePath = resolved.route.path;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (prefer.s.animation && window.document.startViewTransition) {
|
||||
window.document.startViewTransition(() => new Promise((res) => {
|
||||
_();
|
||||
nextTick(() => {
|
||||
res();
|
||||
//setTimeout(res, 100);
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
_();
|
||||
}
|
||||
_();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:setting="rowSetting"
|
||||
:bus="bus"
|
||||
:using="row.using"
|
||||
:class="[lastLine === row.index ? 'last_row' : '']"
|
||||
@operation:beginEdit="onCellEditBegin"
|
||||
@operation:endEdit="onCellEditEnd"
|
||||
@change:value="onChangeCellValue"
|
||||
|
|
@ -1301,8 +1300,6 @@ onMounted(() => {
|
|||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
$borderSetting: solid 0.5px var(--MI_THEME-divider);
|
||||
|
||||
// 配下コンポーネントを含めて一括してコントロールするため、scopedもmoduleも使用できない
|
||||
.mk_grid_border {
|
||||
--rootBorderSetting: none;
|
||||
|
|
@ -1310,66 +1307,39 @@ $borderSetting: solid 0.5px var(--MI_THEME-divider);
|
|||
|
||||
border-spacing: 0;
|
||||
|
||||
&.mk_grid_root_border {
|
||||
--rootBorderSetting: #{$borderSetting};
|
||||
}
|
||||
|
||||
&.mk_grid_root_rounded {
|
||||
--borderRadius: var(--MI-radius);
|
||||
}
|
||||
|
||||
.mk_grid_thead {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(8px));
|
||||
backdrop-filter: var(--MI-blur, blur(20px));
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
|
||||
.mk_grid_tr {
|
||||
.mk_grid_th {
|
||||
border-left: $borderSetting;
|
||||
border-top: var(--rootBorderSetting);
|
||||
|
||||
&:first-child {
|
||||
// 左上セル
|
||||
border-left: var(--rootBorderSetting);
|
||||
border-top-left-radius: var(--borderRadius);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
// 右上セル
|
||||
border-top-right-radius: var(--borderRadius);
|
||||
border-right: var(--rootBorderSetting);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mk_grid_tbody {
|
||||
.mk_grid_tr {
|
||||
.mk_grid_td, .mk_grid_th {
|
||||
border-left: $borderSetting;
|
||||
border-top: $borderSetting;
|
||||
|
||||
&:first-child {
|
||||
// 左端の列
|
||||
border-left: var(--rootBorderSetting);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
// 一番右端の列
|
||||
border-right: var(--rootBorderSetting);
|
||||
}
|
||||
&:nth-child(odd) {
|
||||
background: var(--MI_THEME-panel);
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
background: var(--MI_THEME-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.last_row {
|
||||
.mk_grid_td, .mk_grid_th {
|
||||
// 一番下の行
|
||||
border-bottom: var(--rootBorderSetting);
|
||||
|
||||
&:first-child {
|
||||
// 左下セル
|
||||
border-bottom-left-radius: var(--borderRadius);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
// 右下セル
|
||||
border-bottom-right-radius: var(--borderRadius);
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px var(--MI_THEME-divider) inset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import MkLoading from './global/MkLoading.vue';
|
|||
import MkError from './global/MkError.vue';
|
||||
import MkAd from './global/MkAd.vue';
|
||||
import MkPageHeader from './global/MkPageHeader.vue';
|
||||
import MkSpacer from './global/MkSpacer.vue';
|
||||
import MkStickyContainer from './global/MkStickyContainer.vue';
|
||||
import MkLazy from './global/MkLazy.vue';
|
||||
import PageWithHeader from './global/PageWithHeader.vue';
|
||||
|
|
@ -60,7 +59,6 @@ export const components = {
|
|||
MkError: MkError,
|
||||
MkAd: MkAd,
|
||||
MkPageHeader: MkPageHeader,
|
||||
MkSpacer: MkSpacer,
|
||||
MkStickyContainer: MkStickyContainer,
|
||||
MkLazy: MkLazy,
|
||||
PageWithHeader: PageWithHeader,
|
||||
|
|
@ -92,7 +90,6 @@ declare module '@vue/runtime-core' {
|
|||
MkError: typeof MkError;
|
||||
MkAd: typeof MkAd;
|
||||
MkPageHeader: typeof MkPageHeader;
|
||||
MkSpacer: typeof MkSpacer;
|
||||
MkStickyContainer: typeof MkStickyContainer;
|
||||
MkLazy: typeof MkLazy;
|
||||
PageWithHeader: typeof PageWithHeader;
|
||||
|
|
|
|||
|
|
@ -17,5 +17,4 @@ export const DI = {
|
|||
mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>,
|
||||
inModal: Symbol() as InjectionKey<boolean>,
|
||||
inAppSearchMarkerId: Symbol() as InjectionKey<Ref<string | null>>,
|
||||
forceSpacerMin: Symbol() as InjectionKey<boolean>,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<div style="overflow: clip;">
|
||||
<MkSpacer :contentMax="600" :marginMin="20">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 20px;">
|
||||
<div class="_gaps_m znqjceqz">
|
||||
<div v-panel class="about">
|
||||
<div ref="containerEl" class="container" :class="{ playing: easterEggEngine != null }">
|
||||
|
|
@ -85,7 +85,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-if="section.link" #description><MkLink :url="section.link.url">{{ section.link.label }}</MkLink></template>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -4,21 +4,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSwiper v-model:tab="tab" :tabs="headerTabs">
|
||||
<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
|
||||
<XOverview/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
|
||||
<XEmojis/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="instance.federation !== 'none' && tab === 'federation'" :contentMax="1000" :marginMin="20">
|
||||
<XFederation/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20">
|
||||
<MkInstanceStats/>
|
||||
</MkSpacer>
|
||||
</MkSwiper>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
|
||||
<div v-if="tab === 'overview'" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 20px;">
|
||||
<XOverview/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'emojis'" class="_spacer" style="--MI_SPACER-w: 1000px; --MI_SPACER-min: 20px;">
|
||||
<XEmojis/>
|
||||
</div>
|
||||
<div v-else-if="instance.federation !== 'none' && tab === 'federation'" class="_spacer" style="--MI_SPACER-w: 1000px; --MI_SPACER-min: 20px;">
|
||||
<XFederation/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'charts'" class="_spacer" style="--MI_SPACER-w: 1000px; --MI_SPACER-min: 20px;">
|
||||
<MkInstanceStats/>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
|
|
@ -28,7 +26,6 @@ import { instance } from '@/instance.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkSwiper from '@/components/MkSwiper.vue';
|
||||
|
||||
const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
|
||||
const XEmojis = defineAsyncComponent(() => import('@/pages/about.emojis.vue'));
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<PageWithHeader>
|
||||
<MkSpacer :contentMax="1200">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 1200px;">
|
||||
<MkAchievements :user="$i"/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer v-if="file" :contentMax="600" :marginMin="16" :marginMax="32">
|
||||
<div v-if="file" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
|
||||
<div v-if="tab === 'overview'" class="cxqhhsmd _gaps_m">
|
||||
<a class="thumbnail" :href="file.url" target="_blank">
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
|
|
@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkObjectView v-if="info" tall :value="info">
|
||||
</MkObjectView>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="600" :marginMin="16" :marginMax="32">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
|
||||
<FormSuspense :p="init">
|
||||
<div v-if="tab === 'overview'" class="_gaps_m">
|
||||
<div class="aeakzknw">
|
||||
|
|
@ -201,7 +201,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkObjectView>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ mode === 'create' ? i18n.ts._abuseReport._notificationRecipient.createRecipient : i18n.ts._abuseReport._notificationRecipient.modifyRecipient }}
|
||||
</template>
|
||||
<div v-if="loading === 0" style="display: flex; flex-direction: column; min-height: 100%;">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px; flex-grow: 1;">
|
||||
<div :class="$style.root" class="_gaps_m">
|
||||
<MkInput v-model="title">
|
||||
<template #label>{{ i18n.ts.title }}</template>
|
||||
|
|
@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
|
||||
<div :class="$style.footer" class="_buttonsCenter">
|
||||
<MkButton primary rounded :disabled="disableSubmitButton" @click="onSubmitClicked"><i class="ti ti-check"></i> {{ i18n.ts.ok }}</MkButton>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
||||
<div :class="$style.root" class="_gaps_m">
|
||||
<div :class="$style.addButton">
|
||||
<MkButton primary @click="onAddButtonClicked">
|
||||
|
|
@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
||||
<div :class="$style.root" class="_gaps">
|
||||
<div :class="$style.subMenus" class="_gaps">
|
||||
<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
|
||||
|
|
@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
||||
<MkSelect v-model="filterType" :class="$style.input" @update:modelValue="filterItems">
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
|
|
@ -77,7 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i class="ti ti-reload"></i>{{ i18n.ts.more }}
|
||||
</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
||||
<div class="_gaps">
|
||||
<MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo>
|
||||
<MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo>
|
||||
|
|
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkButton>
|
||||
</template>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div>
|
||||
<PageWithHeader :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
||||
<div class="_gaps_m">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination" :displayLimit="50">
|
||||
<template #default="{ items }">
|
||||
|
|
@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<PageWithHeader :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="iconUrl" type="url">
|
||||
|
|
@ -105,12 +105,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkTextarea>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 16px;">
|
||||
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</PageWithHeader>
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header>
|
||||
<i class="ti ti-notes" style="margin-right: 0.5em;"></i> {{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}
|
||||
</template>
|
||||
<MkSpacer>
|
||||
<div class="_spacer">
|
||||
<XRegisterLogs :logs="logs"/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i class="ti ti-search" style="margin-right: 0.5em;"></i> {{ i18n.ts.search }}
|
||||
</template>
|
||||
<div :class="$style.root">
|
||||
<MkSpacer>
|
||||
<div class="_spacer">
|
||||
<div class="_gaps">
|
||||
<div class="_gaps_s">
|
||||
<MkInput
|
||||
|
|
@ -107,7 +107,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
/>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div :class="$style.footerActions">
|
||||
<MkButton primary @click="onSearchRequest">
|
||||
{{ i18n.ts.search }}
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ const searchQuery = ref<EmojiSearchQuery>({
|
|||
localOnly: null,
|
||||
roles: [],
|
||||
sortOrders: [],
|
||||
limit: 25,
|
||||
limit: 100,
|
||||
});
|
||||
let searchWindowOpening = false;
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue