diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2025-03-30 01:58:17 -0400 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2025-03-30 01:58:17 -0400 |
| commit | 4ae26e6e185e52ac186ac10ccd4eda7718bf6e26 (patch) | |
| tree | 8d556072f3876f0bfbab9d24e7cd209ca6bd091a /packages | |
| parent | restore following feed deck UI (diff) | |
| parent | New Crowdin updates (#15721) (diff) | |
| download | sharkey-4ae26e6e185e52ac186ac10ccd4eda7718bf6e26.tar.gz sharkey-4ae26e6e185e52ac186ac10ccd4eda7718bf6e26.tar.bz2 sharkey-4ae26e6e185e52ac186ac10ccd4eda7718bf6e26.zip | |
Merge branch 'misskey-develop' into merge/2025-03-24
Diffstat (limited to 'packages')
101 files changed, 924 insertions, 1686 deletions
diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index c4feeaf971..412ab33b3f 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -69,7 +69,7 @@ export class QueryService { // ここでいうBlockedは被Blockedの意 @bindThis - public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void { + public generateBlockedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void { const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') .select('blocking.blockerId') .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); @@ -127,7 +127,7 @@ export class QueryService { } @bindThis - public generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void { + public generateMutedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void { const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') .select('muting.muteeId') .where('muting.muterId = :muterId', { muterId: me.id }); diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 4782a6c7b0..e17677bcb5 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -300,8 +300,8 @@ export class SearchService { } this.queryService.generateVisibilityQuery(query, me); - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); + if (me) this.queryService.generateMutedUserQueryForNotes(query, me); + if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); return await query.limit(pagination.limit).getMany(); } diff --git a/packages/backend/src/core/UserSearchService.ts b/packages/backend/src/core/UserSearchService.ts index 0d03cf6ee0..4be7bd9bdb 100644 --- a/packages/backend/src/core/UserSearchService.ts +++ b/packages/backend/src/core/UserSearchService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets, SelectQueryBuilder } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js'; +import { type FollowingsRepository, MiUser, type MutingsRepository, type UserProfilesRepository, type UsersRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; import type { Config } from '@/config.js'; @@ -22,10 +22,19 @@ export class UserSearchService { constructor( @Inject(DI.config) private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + private userEntityService: UserEntityService, ) { } @@ -58,7 +67,7 @@ export class UserSearchService { * @see {@link UserSearchService#buildSearchUserNoLoginQueries} */ @bindThis - public async search( + public async searchByUsernameAndHost( params: { username?: string | null, host?: string | null, @@ -202,4 +211,91 @@ export class UserSearchService { return userQuery; } + + @bindThis + public async search(query: string, meId: MiUser['id'] | null, options: Partial<{ + limit: number; + offset: number; + origin: 'local' | 'remote' | 'combined'; + }> = {}) { + const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 + + const isUsername = query.startsWith('@') && !query.includes(' ') && query.indexOf('@', 1) === -1; + + let users: MiUser[] = []; + + const mutingQuery = meId == null ? null : this.mutingsRepository.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: meId }); + + const nameQuery = this.usersRepository.createQueryBuilder('user') + .where(new Brackets(qb => { + qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(query) + '%' }); + + if (isUsername) { + qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(query.replace('@', '').toLowerCase()) + '%' }); + } else if (this.userEntityService.validateLocalUsername(query)) { // Also search username if it qualifies as username + qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(query.toLowerCase()) + '%' }); + } + })) + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE'); + + if (mutingQuery) { + nameQuery.andWhere(`user.id NOT IN (${mutingQuery.getQuery()})`); + nameQuery.setParameters(mutingQuery.getParameters()); + } + + if (options.origin === 'local') { + nameQuery.andWhere('user.host IS NULL'); + } else if (options.origin === 'remote') { + nameQuery.andWhere('user.host IS NOT NULL'); + } + + users = await nameQuery + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .limit(options.limit) + .offset(options.offset) + .getMany(); + + if (users.length < (options.limit ?? 30)) { + const profQuery = this.userProfilesRepository.createQueryBuilder('prof') + .select('prof.userId') + .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(query) + '%' }); + + if (mutingQuery) { + profQuery.andWhere(`prof.userId NOT IN (${mutingQuery.getQuery()})`); + profQuery.setParameters(mutingQuery.getParameters()); + } + + if (options.origin === 'local') { + profQuery.andWhere('prof.userHost IS NULL'); + } else if (options.origin === 'remote') { + profQuery.andWhere('prof.userHost IS NOT NULL'); + } + + const userQuery = this.usersRepository.createQueryBuilder('user') + .where(`user.id IN (${ profQuery.getQuery() })`) + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE') + .setParameters(profQuery.getParameters()); + + users = users.concat(await userQuery + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .limit(options.limit) + .offset(options.offset) + .getMany(), + ); + } + + return users; + } } diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index 8d2e181aa5..3e89595155 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -482,6 +482,7 @@ export class WebhookTestService { followersVisibility: 'public', followingVisibility: 'public', chatScope: 'mutual', + canChat: true, twoFactorEnabled: false, usePasswordLessLogin: false, securityKeys: false, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index cfcf10a0f4..2dc2c50e4a 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -664,6 +664,7 @@ export class UserEntityService implements OnModuleInit { followersVisibility: profile!.followersVisibility, followingVisibility: profile!.followingVisibility, chatScope: user.chatScope, + canChat: this.roleService.getUserPolicies(user.id).then(r => r.canChat), roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ id: role.id, name: role.name, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index e6f85afcce..964a179244 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -423,6 +423,10 @@ export const packedUserDetailedNotMeOnlySchema = { nullable: false, optional: false, enum: ['everyone', 'following', 'followers', 'mutual', 'none'], }, + canChat: { + type: 'boolean', + nullable: false, optional: false, + }, roles: { type: 'array', nullable: false, optional: false, diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 8ad2315209..00448297fb 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -115,8 +115,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me); const notes = await query.getMany(); if (sinceId != null && untilId == null) { diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 0bd01d712c..5fc0ae00b2 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -138,8 +138,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('note.channel', 'channel'); if (me) { - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me); } if (ps.withRenotes === false) { diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index 6175c4d0e5..69ff45a1c6 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -93,8 +93,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (me) { this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me); } const notes = await query diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index e69ba9be7e..c97a0c0bc7 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -80,7 +80,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.queryService.generateVisibilityQuery(query, me); if (me) { - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me); } const notes = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 0f2592bd78..efecf0b3c1 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -94,8 +94,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); if (me) { - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); } diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 3c66154e19..b0e3327411 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -254,8 +254,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.includeMyRenotes === false) { diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 1f986079c2..864592ed9b 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -167,8 +167,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); + if (me) this.queryService.generateMutedUserQueryForNotes(query, me); + if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.withFiles) { diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 30baa0edf8..bfcd120f49 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -78,9 +78,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateMutedNoteThreadQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me); if (ps.visibility) { query.andWhere('note.visibility = :visibility', { visibility: ps.visibility }); diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 15f114266a..01f09433a9 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -91,8 +91,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } this.queryService.generateVisibilityQuery(query, me); - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); + if (me) this.queryService.generateMutedUserQueryForNotes(query, me); + if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); const renotes = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index 3f0a8157c4..f04c9b0ec4 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -62,8 +62,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); + if (me) this.queryService.generateMutedUserQueryForNotes(query, me); + if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); const timeline = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 6bba7bf37e..50711bc2bd 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -97,8 +97,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE'); this.queryService.generateVisibilityQuery(query, me); - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); + if (me) this.queryService.generateMutedUserQueryForNotes(query, me); + if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); const [ followings, diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 5a46f66f9e..9f19117426 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -209,8 +209,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- })); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.includeMyRenotes === false) { diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 55cda135e2..4c37edfdec 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -190,8 +190,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- })); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.includeMyRenotes === false) { diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index b3c73e0391..dd40e49d3d 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -108,8 +108,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me); const notes = await query.getMany(); notes.sort((a, b) => a.id > b.id ? -1 : 1); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 6416e43ff1..a4eee544f0 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -217,8 +217,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.queryService.generateVisibilityQuery(query, me); if (me) { - this.queryService.generateMutedUserQuery(query, me, { id: ps.userId }); - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId }); + this.queryService.generateBlockedUserQueryForNotes(query, me); } if (ps.withFiles) { diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index 46af1f38ac..642d788459 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -69,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.queryService.generateMutedUserQueryForUsers(query, me); this.queryService.generateBlockQueryForUsers(query, me); - this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateBlockedUserQueryForNotes(query, me); const followingQuery = this.followingsRepository.createQueryBuilder('following') .select('following.followeeId') diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index fda56ea6fe..f1a0fc5ddb 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -52,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private userSearchService: UserSearchService, ) { super(meta, paramDef, (ps, me) => { - return this.userSearchService.search({ + return this.userSearchService.searchByUsernameAndHost({ username: ps.username, host: ps.host, }, { diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index 2d17c91e1d..138cef2ec5 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -3,14 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import { UserSearchService } from '@/core/UserSearchService.js'; export const meta = { tags: ['users'], @@ -51,79 +48,15 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - private userEntityService: UserEntityService, + private userSearchService: UserSearchService, ) { super(meta, paramDef, async (ps, me) => { - const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 - - ps.query = ps.query.trim(); - const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1; - - let users: MiUser[] = []; - - const nameQuery = this.usersRepository.createQueryBuilder('user') - .where(new Brackets(qb => { - qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); - - if (isUsername) { - qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' }); - } else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username - qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' }); - } - })) - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE'); - - if (ps.origin === 'local') { - nameQuery.andWhere('user.host IS NULL'); - } else if (ps.origin === 'remote') { - nameQuery.andWhere('user.host IS NOT NULL'); - } - - users = await nameQuery - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .limit(ps.limit) - .offset(ps.offset) - .getMany(); - - if (users.length < ps.limit) { - const profQuery = this.userProfilesRepository.createQueryBuilder('prof') - .select('prof.userId') - .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); - - if (ps.origin === 'local') { - profQuery.andWhere('prof.userHost IS NULL'); - } else if (ps.origin === 'remote') { - profQuery.andWhere('prof.userHost IS NOT NULL'); - } - - const query = this.usersRepository.createQueryBuilder('user') - .where(`user.id IN (${ profQuery.getQuery() })`) - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE') - .setParameters(profQuery.getParameters()); - - users = users.concat(await query - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .limit(ps.limit) - .offset(ps.offset) - .getMany(), - ); - } + const users = await this.userSearchService.search(ps.query.trim(), me?.id ?? null, { + offset: ps.offset, + limit: ps.limit, + origin: ps.origin, + }); return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' }); }); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 58cfab23dc..ed1dadd181 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -87,6 +87,7 @@ describe('ユーザー', () => { followingVisibility: user.followingVisibility, followersVisibility: user.followersVisibility, chatScope: user.chatScope, + canChat: user.canChat, roles: user.roles, memo: user.memo, }); @@ -353,6 +354,7 @@ describe('ユーザー', () => { assert.strictEqual(response.followingVisibility, 'public'); assert.strictEqual(response.followersVisibility, 'public'); assert.strictEqual(response.chatScope, 'mutual'); + assert.strictEqual(response.canChat, true); assert.deepStrictEqual(response.roles, []); assert.strictEqual(response.memo, null); @@ -763,7 +765,7 @@ describe('ユーザー', () => { }); test.each([ { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, - { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice }, + { label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice }, { label: '承認制ユーザーが含まれる', user: () => userLocking }, diff --git a/packages/backend/test/unit/UserSearchService.ts b/packages/backend/test/unit/UserSearchService.ts index 66a7f39ff1..697425beb8 100644 --- a/packages/backend/test/unit/UserSearchService.ts +++ b/packages/backend/test/unit/UserSearchService.ts @@ -134,13 +134,13 @@ describe('UserSearchService', () => { await app.close(); }); - describe('search', () => { + describe('searchByUsernameAndHost', () => { test('フォロー中のアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]); await setInactive([alycia, alysha, alyson]); - const result = await service.search( + const result = await service.searchByUsernameAndHost( { username: 'al' }, { limit: 100 }, root, @@ -154,7 +154,7 @@ describe('UserSearchService', () => { await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - const result = await service.search( + const result = await service.searchByUsernameAndHost( { username: 'al' }, { limit: 100 }, root, @@ -168,7 +168,7 @@ describe('UserSearchService', () => { await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); await setInactive([alice, alyce, alycia]); - const result = await service.search( + const result = await service.searchByUsernameAndHost( { username: 'al' }, { limit: 100 }, root, @@ -181,7 +181,7 @@ describe('UserSearchService', () => { test('フォローしていない非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - const result = await service.search( + const result = await service.searchByUsernameAndHost( { username: 'al' }, { limit: 100 }, root, @@ -195,7 +195,7 @@ describe('UserSearchService', () => { await setActive([root, alyssa, bob, bobbi, alyce, alycia]); await setInactive([alyson, alice, alysha, bobbie, bobby]); - const result = await service.search( + const result = await service.searchByUsernameAndHost( { }, { limit: 100 }, root, @@ -216,7 +216,7 @@ describe('UserSearchService', () => { await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); await setInactive([alice, alyce, alycia]); - const result = await service.search( + const result = await service.searchByUsernameAndHost( { username: 'al' }, { limit: 100 }, ); @@ -228,7 +228,7 @@ describe('UserSearchService', () => { test('[非ログイン] 非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - const result = await service.search( + const result = await service.searchByUsernameAndHost( { username: 'al' }, { limit: 100 }, ); @@ -240,7 +240,7 @@ describe('UserSearchService', () => { await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - const result = await service.search( + const result = await service.searchByUsernameAndHost( { username: 'al', host: 'exam' }, { limit: 100 }, root, @@ -253,7 +253,7 @@ describe('UserSearchService', () => { await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); await setSuspended([alice, alyce, alycia]); - const result = await service.search( + const result = await service.searchByUsernameAndHost( { username: 'al' }, { limit: 100 }, root, diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index e6bed380e7..12e2dcc745 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -34,7 +34,7 @@ "typescript": "5.8.2", "uuid": "11.1.0", "json5": "2.2.3", - "vite": "6.2.2", + "vite": "6.2.3", "vue": "3.5.13" }, "devDependencies": { diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue index 4cf156ba23..94a91305f4 100644 --- a/packages/frontend-embed/src/components/EmPagination.vue +++ b/packages/frontend-embed/src/components/EmPagination.vue @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; -import { onScrollTop, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible, isHeadVisible } from '@@/js/scroll.js'; +import { onScrollTop, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible, isHeadVisible } from '@@/js/scroll.js'; import type { ComputedRef } from 'vue'; import { misskeyApi } from '@/misskey-api.js'; import { i18n } from '@/i18n.js'; @@ -252,7 +252,7 @@ const fetchMore = async (): Promise<void> => { return nextTick(() => { if (scrollableElement.value) { - scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); + scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); } else { window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); } diff --git a/packages/frontend-shared/js/scroll.ts b/packages/frontend-shared/js/scroll.ts index 6c61c582e1..9057b896c6 100644 --- a/packages/frontend-shared/js/scroll.ts +++ b/packages/frontend-shared/js/scroll.ts @@ -93,7 +93,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1 return removeListener; } -export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) { +export function scrollInContainer(el: HTMLElement, options: ScrollToOptions | undefined) { const container = getScrollContainer(el); if (container == null) { window.scroll(options); @@ -108,7 +108,7 @@ export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) { * @param options Scroll options */ export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) { - scroll(el, { top: 0, ...options }); + scrollInContainer(el, { top: 0, ...options }); } /** diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js index 1532a7e67b..26ee288523 100644 --- a/packages/frontend/eslint.config.js +++ b/packages/frontend/eslint.config.js @@ -65,7 +65,12 @@ export default [ // location ... window.locationと衝突 or 紛らわしい // document ... window.documentと衝突 or 紛らわしい // history ... window.historyと衝突 or 紛らわしい - 'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history'], + // scroll ... window.scrollと衝突 or 紛らわしい + // setTimeout ... window.setTimeoutと衝突 or 紛らわしい + // setInterval ... window.setIntervalと衝突 or 紛らわしい + // clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい + // clearInterval ... window.clearIntervalと衝突 or 紛らわしい + 'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'], 'no-restricted-globals': [ 'error', { @@ -93,6 +98,26 @@ export default [ 'message': 'Use `window.history`.', }, { + 'name': 'scroll', + 'message': 'Use `window.scroll`.', + }, + { + 'name': 'setTimeout', + 'message': 'Use `window.setTimeout`.', + }, + { + 'name': 'setInterval', + 'message': 'Use `window.setInterval`.', + }, + { + 'name': 'clearTimeout', + 'message': 'Use `window.clearTimeout`.', + }, + { + 'name': 'clearInterval', + 'message': 'Use `window.clearInterval`.', + }, + { 'name': 'name', 'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている', }, diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 4f13bc98d3..47d4e8cd3d 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -75,7 +75,7 @@ "typescript": "5.8.2", "uuid": "11.1.0", "v-code-diff": "1.13.1", - "vite": "6.2.2", + "vite": "6.2.3", "vue": "3.5.13", "vuedraggable": "next", "wanakana": "5.3.1" diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 56e229283b..5f59a90881 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -29,7 +29,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.js'; import { prefer } from '@/preferences.js'; import { $i } from '@/i.js'; -export async function common(createVue: () => App<Element>) { +export async function common(createVue: () => Promise<App<Element>>) { console.info(`Sharkey v${version}`); if (_DEV_) { @@ -255,7 +255,7 @@ export async function common(createVue: () => App<Element>) { await fetchCustomEmojis(); } catch (err) { /* empty */ } - const app = createVue(); + const app = await createVue(); if (_DEV_) { app.config.performance = true; diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 537d61d1a1..17ccb979d5 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -34,7 +34,7 @@ import { signout } from '@/signout.js'; import { migrateOldSettings } from '@/pref-migrate.js'; export async function mainBoot() { - const { isClientUpdated, lastVersion } = await common(() => { + const { isClientUpdated, lastVersion } = await common(async () => { let uiStyle = ui; const searchParams = new URLSearchParams(window.location.search); @@ -48,19 +48,16 @@ export async function mainBoot() { let rootComponent: Component; switch (uiStyle) { case 'zen': - rootComponent = defineAsyncComponent(() => import('@/ui/zen.vue')); + rootComponent = await import('@/ui/zen.vue').then(x => x.default); break; case 'deck': - rootComponent = defineAsyncComponent(() => import('@/ui/deck.vue')); + rootComponent = await import('@/ui/deck.vue').then(x => x.default); break; case 'visitor': - rootComponent = defineAsyncComponent(() => import('@/ui/visitor.vue')); - break; - case 'classic': - rootComponent = defineAsyncComponent(() => import('@/ui/classic.vue')); + rootComponent = await import('@/ui/visitor.vue').then(x => x.default); break; default: - rootComponent = defineAsyncComponent(() => import('@/ui/universal.vue')); + rootComponent = await import('@/ui/universal.vue').then(x => x.default); break; } diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts index e24c324dfb..036142bc4d 100644 --- a/packages/frontend/src/boot/sub-boot.ts +++ b/packages/frontend/src/boot/sub-boot.ts @@ -6,11 +6,10 @@ import { createApp, defineAsyncComponent } from 'vue'; import { common } from './common.js'; import { emojiPicker } from '@/utility/emoji-picker.js'; +import UiMinimum from '@/ui/minimum.vue'; export async function subBoot() { - const { isClientUpdated } = await common(() => createApp( - defineAsyncComponent(() => import('@/ui/minimum.vue')), - )); + const { isClientUpdated } = await common(async () => createApp(UiMinimum)); emojiPicker.init(); } diff --git a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts index a42e80c27a..4304c2e2b7 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts +++ b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts @@ -2,20 +2,18 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; + import { HttpResponse, http } from 'msw'; import { action } from '@storybook/addon-actions'; import { expect, userEvent, within } from '@storybook/test'; import { channel } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkChannelFollowButton from './MkChannelFollowButton.vue'; +import type { StoryObj } from '@storybook/vue3'; import { i18n } from '@/i18n.js'; function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => window.setTimeout(resolve, ms)); } export const Default = { diff --git a/packages/frontend/src/components/MkClickerGame.stories.impl.ts b/packages/frontend/src/components/MkClickerGame.stories.impl.ts index eb7e61f294..6e1eb13d61 100644 --- a/packages/frontend/src/components/MkClickerGame.stories.impl.ts +++ b/packages/frontend/src/components/MkClickerGame.stories.impl.ts @@ -2,18 +2,16 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; + import { HttpResponse, http } from 'msw'; import { action } from '@storybook/addon-actions'; import { expect, userEvent, within } from '@storybook/test'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkClickerGame from './MkClickerGame.vue'; +import type { StoryObj } from '@storybook/vue3'; function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => window.setTimeout(resolve, ms)); } export const Default = { diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 730dd0d55e..0aeb65d69c 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -4,41 +4,45 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="$style.root"> - <nav :class="$style.nav"> - <div :class="$style.navPath" @contextmenu.prevent.stop="() => {}"> - <XNavFolder - :class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]" - :parentFolder="folder" - @move="move" - @upload="upload" - @removeFile="removeFile" - @removeFolder="removeFolder" - /> - <template v-for="f in hierarchyFolders"> - <span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> +<MkStickyContainer> + <template #header> + <nav :class="$style.nav"> + <div :class="$style.navPath" @contextmenu.prevent.stop="() => {}"> <XNavFolder - :folder="f" + :class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]" :parentFolder="folder" - :class="[$style.navPathItem]" @move="move" @upload="upload" @removeFile="removeFile" @removeFolder="removeFolder" /> - </template> - <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> - <span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span> - </div> - <div :class="$style.navMenu"> - <!-- "Search drive via alt text or file names" --> - <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" :placeholder="i18n.ts.driveSearchbarPlaceholder" @enter="fetch"> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> - </MkInput> - + <template v-for="f in hierarchyFolders"> + <span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> + <XNavFolder + :folder="f" + :parentFolder="folder" + :class="[$style.navPathItem]" + @move="move" + @upload="upload" + @removeFile="removeFile" + @removeFolder="removeFolder" + /> + </template> + <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> + <span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span> + </div> <button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button> - </div> - </nav> + <div :class="$style.navMenu"> + <!-- "Search drive via alt text or file names" --> + <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" :placeholder="i18n.ts.driveSearchbarPlaceholder" @enter="fetch"> + <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + </MkInput> + + <button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button> + </div> + </nav> + </template> + <div ref="main" :class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]" @@ -98,8 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading v-if="fetching"/> </div> <div v-if="draghover" :class="$style.dropzone"></div> - <input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/> -</div> +</MkStickyContainer> </template> <script lang="ts" setup> @@ -118,6 +121,7 @@ import { i18n } from '@/i18n.js'; 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'; const searchQuery = ref(''); @@ -140,7 +144,6 @@ const emit = defineEmits<{ }>(); const loadMoreFiles = useTemplateRef('loadMoreFiles'); -const fileInput = useTemplateRef('fileInput'); const folder = ref<Misskey.entities.DriveFolder | null>(null); const files = ref<Misskey.entities.DriveFile[]>([]); @@ -152,7 +155,6 @@ const selectedFiles = ref<Misskey.entities.DriveFile[]>([]); const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]); const uploadings = uploads; const connection = useStream().useChannel('drive'); -const keepOriginal = ref<boolean>(prefer.s.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい // ドロップされようとしているか const draghover = ref(false); @@ -314,10 +316,6 @@ function onDrop(ev: DragEvent) { //#endregion } -function selectLocalFile() { - fileInput.value?.click(); -} - function urlUpload() { os.inputText({ title: i18n.ts.uploadFromUrl, @@ -393,15 +391,8 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { }); } -function onChangeFileInput() { - if (!fileInput.value?.files) return; - for (const file of Array.from(fileInput.value.files)) { - upload(file, folder.value); - } -} - -function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) { - uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => { +function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null, keepOriginal?: boolean) { + uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal).then(res => { addFile(res, true); }); } @@ -644,16 +635,20 @@ function getMenu() { const menu: MenuItem[] = []; menu.push({ - type: 'switch', - text: i18n.ts.keepOriginalUploading, - ref: keepOriginal, - }, { type: 'divider' }, { text: i18n.ts.addFile, type: 'label', }, { + text: i18n.ts.upload + ' (' + i18n.ts.compress + ')', + icon: 'ti ti-upload', + action: () => { + chooseFileFromPc(true, { keepOriginal: false }); + }, + }, { text: i18n.ts.upload, icon: 'ti ti-upload', - action: () => { selectLocalFile(); }, + action: () => { + chooseFileFromPc(true, { keepOriginal: true }); + }, }, { text: i18n.ts.fromUrl, icon: 'ti ti-link', @@ -765,22 +760,17 @@ onBeforeUnmount(() => { </script> <style lang="scss" module> -.root { - display: flex; - flex-direction: column; - height: 100%; -} - .nav { display: flex; - z-index: 2; width: 100%; padding: 0 8px; box-sizing: border-box; overflow: auto; font-size: 0.9em; - box-shadow: 0 1px 0 var(--MI_THEME-divider); - user-select: none; + background: color(from var(--MI_THEME-bg) srgb r g b / 0.75); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + border-bottom: solid 0.5px var(--MI_THEME-divider); } .navPath { diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 5c4400eaf8..118eb8ecc0 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -79,7 +79,7 @@ function opening() { picker.value?.focus(); // 何故かちょっと待たないとフォーカスされない - setTimeout(() => { + window.setTimeout(() => { picker.value?.focus(); }, 10); } diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue index bc63bef0b6..1d0ffaea11 100644 --- a/packages/frontend/src/components/MkHorizontalSwipe.vue +++ b/packages/frontend/src/components/MkHorizontalSwipe.vue @@ -100,7 +100,7 @@ function touchMove(event: TouchEvent) { pullDistance.value = 0; isSwiping.value = false; - setTimeout(() => { + window.setTimeout(() => { isSwipingForClass.value = false; }, 400); diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index a60e100969..8db3924fbd 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -342,7 +342,7 @@ const bufferedDataRatio = computed(() => { // MediaControl Events function onMouseOver() { if (controlStateTimer) { - clearTimeout(controlStateTimer); + window.clearTimeout(controlStateTimer); } isHoverring.value = true; } diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index b5c93df4ed..3bcf835ec9 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -50,6 +50,7 @@ import { deviceKind } from '@/utility/device-kind.js'; import { focusTrap } from '@/utility/focus-trap.js'; import { focusParent } from '@/utility/focus.js'; import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; function getFixedContainer(el: Element | null): Element | null { if (el == null || el.tagName === 'BODY') return null; @@ -94,7 +95,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -provide('modal', true); +provide(DI.inModal, true); const maxHeight = ref<number>(); const fixed = ref(false); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 2e2693d319..5fcb91b338 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -19,8 +19,6 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkNoteSub v-if="appearNote.reply" v-show="!renoteCollapsed && !inReplyToCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> <div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div> - <!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>--> - <!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>--> <div v-if="isRenote" :class="$style.renote"> <div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div> <MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/> @@ -53,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <article v-else :class="$style.article" @contextmenu.stop="onContextmenu"> <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> - <MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/> + <MkAvatar :class="[$style.avatar, prefer.s.useStickyIcons ? $style.useSticky : null]" :user="appearNote.user" :link="!mock" :preview="!mock"/> <div :class="[$style.main, { [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click.stop="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined"> <MkNoteHeader :note="appearNote" :mini="true" @click.stop/> <MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/> @@ -922,7 +920,6 @@ function emitUpdReaction(emoji: string, delta: number) { <style lang="scss" module> .root { position: relative; - transition: box-shadow 0.1s ease; font-size: 1.05em; overflow: clip; contain: content; @@ -993,6 +990,8 @@ function emitUpdReaction(emoji: string, delta: number) { } .skipRender { + // TODO: これが有効だとTransitionGroupでnoteを追加するときに一瞬がくっとなってしまうのをどうにかしたい + // Transitionが完了するのを待ってからskipRenderを付与すれば解決しそうだけどパフォーマンス的な影響が不明 content-visibility: auto; contain-intrinsic-size: 0 150px; } @@ -1127,9 +1126,12 @@ function emitUpdReaction(emoji: string, delta: number) { margin: 0 14px 0 0; width: var(--MI-avatar); height: var(--MI-avatar); - position: sticky !important; - top: calc(22px + var(--MI-stickyTop, 0px)); - left: 0; + + &.useSticky { + position: sticky !important; + top: calc(22px + var(--MI-stickyTop, 0px)); + left: 0; + } } .main { @@ -1324,7 +1326,10 @@ function emitUpdReaction(emoji: string, delta: number) { margin: 0 10px 0 0; width: 46px; height: 46px; - top: calc(14px + var(--MI-stickyTop, 0px)); + + &.useSticky { + top: calc(14px + var(--MI-stickyTop, 0px)); + } } } diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index 928a143a72..bc72351552 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -13,38 +13,34 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #default="{ items: notes }"> - <div :class="[$style.root, { [$style.noGap]: noGap }]"> - <MkDateSeparatedList - ref="notes" - v-slot="{ item: note }" - :items="notes" - :direction="pagination.reversed ? 'up' : 'down'" - :reversed="pagination.reversed" - :noGap="noGap" - :ad="true" - :class="$style.notes" - > - <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/> - </MkDateSeparatedList> - </div> + <component + :is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]" + :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"> + <DynamicNote :class="$style.note" :note="note" :withHardMute="true"/> + <div v-if="note._shouldInsertAd_" :class="$style.ad"> + <MkAd :preferForms="['horizontal', 'horizontal-big']"/> + </div> + </template> + </component> </template> </MkPagination> </template> <script lang="ts" setup> -import { defineAsyncComponent, useTemplateRef } from 'vue'; +import { useTemplateRef, TransitionGroup } from 'vue'; import type { Paging } from '@/components/MkPagination.vue'; -import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; +import DynamicNote from '@/components/DynamicNote.vue'; import MkPagination from '@/components/MkPagination.vue'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; -import { defaultStore } from '@/store.js'; - -const MkNote = defineAsyncComponent(() => - (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') : - (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') : - null -); +import { prefer } from '@/preferences.js'; const props = defineProps<{ pagination: Paging; @@ -60,24 +56,49 @@ 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; +} + .root { + container-type: inline-size; + &.noGap { - border-radius: var(--MI-radius); + background: var(--MI_THEME-panel); + + .note { + border-bottom: solid 0.5px var(--MI_THEME-divider); + } - > .notes { - background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent); + .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) { - > .notes { - background: var(--MI_THEME-bg); + background: var(--MI_THEME-bg); - .note { - background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent); - border-radius: var(--MI-radius); - } + .note { + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); } } } + +.ad:empty { + display: none; +} </style> diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 4a1377655f..453a6c83b3 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -14,34 +14,38 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #default="{ items: notifications }"> - <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> - <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/> - <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> - </MkDateSeparatedList> + <component + :is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.notifications]" + :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="(notification, i) in notifications" :key="notification.id"> + <DynamicNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true"/> + <XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true"/> + </template> + </component> </template> </MkPagination> </MkPullToRefresh> </template> <script lang="ts" setup> -import { defineAsyncComponent, onUnmounted, onDeactivated, onMounted, computed, useTemplateRef, onActivated } from 'vue'; +import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } from 'vue'; import * as Misskey from 'misskey-js'; import type { notificationTypes } from '@@/js/const.js'; import MkPagination from '@/components/MkPagination.vue'; import XNotification from '@/components/MkNotification.vue'; -import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; +import DynamicNote from '@/components/DynamicNote.vue'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import { prefer } from '@/preferences.js'; -const MkNote = defineAsyncComponent(() => - (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') : - (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') : - null -); - const props = defineProps<{ excludeTypes?: typeof notificationTypes[number][]; }>(); @@ -89,28 +93,36 @@ onMounted(() => { connection.on('notificationFlushed', reload); }); -onActivated(() => { - pagingComponent.value?.reload(); - connection = useStream().useChannel('main'); - connection.on('notification', onNotification); - connection.on('notificationFlushed', reload); -}); - onUnmounted(() => { if (connection) connection.dispose(); }); -onDeactivated(() => { - if (connection) connection.dispose(); -}); - defineExpose({ reload, }); </script> <style lang="scss" module> -.list { +.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; +} + +.notifications { + container-type: inline-size; background: var(--MI_THEME-panel); } + +.item { + border-bottom: solid 0.5px var(--MI_THEME-divider); +} </style> diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 71f7fc513b..ee128e5991 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkError v-else-if="error" @retry="init()"/> - <div v-else-if="empty" key="_empty_" class="empty"> + <div v-else-if="empty" key="_empty_"> <slot name="empty"> <div class="_fullinfo"> <img :src="infoImageUrl" draggable="false"/> @@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead"> {{ i18n.ts.loadMore }} </MkButton> - <MkLoading v-else class="loading"/> + <MkLoading v-else/> </div> <slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot> <div v-show="!pagination.reversed && more" key="_more_"> <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore"> {{ i18n.ts.loadMore }} </MkButton> - <MkLoading v-else class="loading"/> + <MkLoading v-else/> </div> </div> </Transition> @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, useTemplateRef, watch, type Ref } from 'vue'; import * as Misskey from 'misskey-js'; import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; -import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible } from '@@/js/scroll.js'; +import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible } from '@@/js/scroll.js'; import type { ComputedRef } from 'vue'; import type { MisskeyEntity } from '@/types/date-separated-list.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -268,7 +268,7 @@ const fetchMore = async (): Promise<void> => { return nextTick(() => { if (scrollableElement.value) { - scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); + scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); } else { window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); } @@ -368,7 +368,7 @@ watch(visibility, () => { BACKGROUND_PAUSE_WAIT_SEC * 1000); } else { // 'visible' if (timerForSetPause) { - clearTimeout(timerForSetPause); + window.clearTimeout(timerForSetPause); timerForSetPause = null; } else { isPausingUpdate = false; @@ -464,11 +464,11 @@ onBeforeMount(() => { init().then(() => { if (props.pagination.reversed) { nextTick(() => { - setTimeout(toBottom, 800); + window.setTimeout(toBottom, 800); // scrollToBottomでmoreFetchingボタンが画面外まで出るまで // more = trueを遅らせる - setTimeout(() => { + window.setTimeout(() => { moreFetching.value = false; }, 2000); }); @@ -478,11 +478,11 @@ onBeforeMount(() => { onBeforeUnmount(() => { if (timerForSetPause) { - clearTimeout(timerForSetPause); + window.clearTimeout(timerForSetPause); timerForSetPause = null; } if (preventAppearFetchMoreTimer.value) { - clearTimeout(preventAppearFetchMoreTimer.value); + window.clearTimeout(preventAppearFetchMoreTimer.value); preventAppearFetchMoreTimer.value = null; } scrollObserver.value?.disconnect(); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 921829c41e..c70d0d5581 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -150,7 +150,7 @@ import { DI } from '@/di.js'; const $i = ensureSignin(); -const modal = inject('modal'); +const modal = inject(DI.inModal, false); const props = withDefaults(defineProps<PostFormProps & { fixed?: boolean; @@ -1431,7 +1431,7 @@ defineExpose({ padding: 0 24px; margin: 0; width: 100%; - font-size: 16px; + font-size: 110%; border: none; border-radius: 0; background: transparent; diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index 1fbf00d212..22ae563d13 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> </div> - <div :class="{ [$style.slotClip]: isPullStart }"> - <slot/> - </div> + + <slot/> </div> </template> @@ -82,11 +81,11 @@ function moveBySystem(to: number): Promise<void> { return; } const startTime = Date.now(); - let intervalId = setInterval(() => { + let intervalId = window.setInterval(() => { const time = Date.now() - startTime; if (time > RELEASE_TRANSITION_DURATION) { pullDistance.value = to; - clearInterval(intervalId); + window.clearInterval(intervalId); r(); return; } @@ -261,8 +260,4 @@ defineExpose({ margin: 5px 0; } } - -.slotClip { - overflow-y: clip; -} </style> diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index cf0bb7b1f2..6b19f4a55c 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -4,22 +4,24 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<TransitionGroup - :enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''" - :leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''" - :enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''" - :leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''" - :moveClass="prefer.s.animation ? $style.transition_x_move : ''" +<component + :is="prefer.s.animation ? TransitionGroup : 'div'" + :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" :class="$style.root" > <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/> <slot v-if="hasMoreReactions" name="more"/> -</TransitionGroup> +</component> </template> <script lang="ts" setup> import * as Misskey from 'misskey-js'; import { inject, watch, ref } from 'vue'; +import { TransitionGroup } from 'vue'; import XReaction from '@/components/MkReactionsViewer.reaction.vue'; import { prefer } from '@/preferences.js'; import { DI } from '@/di.js'; diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue index 955c24b6ef..16913386c1 100644 --- a/packages/frontend/src/components/MkTooltip.vue +++ b/packages/frontend/src/components/MkTooltip.vue @@ -9,7 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveActiveClass="prefer.s.animation ? $style.transition_tooltip_leaveActive : ''" :enterFromClass="prefer.s.animation ? $style.transition_tooltip_enterFrom : ''" :leaveToClass="prefer.s.animation ? $style.transition_tooltip_leaveTo : ''" - appear @afterLeave="emit('closed')" + appear :css="prefer.s.animation" + @afterLeave="emit('closed')" > <div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> <slot> diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index b29bbb6392..525c47da45 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -36,7 +36,6 @@ SPDX-License-Identifier: AGPL-3.0-only <button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button> </div> </div> -<div v-else></div> </template> <script lang="ts" setup> @@ -53,7 +52,7 @@ import { prefer } from '@/preferences.js'; type Ad = (typeof instance)['ads'][number]; const props = defineProps<{ - prefer: string[]; + preferForms: string[]; specify?: Ad; }>(); @@ -72,7 +71,7 @@ const choseAd = (): Ad | null => { ratio: 0, } : ad); - let ads = allAds.filter(ad => props.prefer.includes(ad.place)); + let ads = allAds.filter(ad => props.preferForms.includes(ad.place)); if (ads.length === 0) { ads = allAds.filter(ad => ad.place === 'square'); diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index f5323690d0..6abe1f30b6 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -25,7 +25,7 @@ const props = defineProps<{ menuReaction?: boolean; }>(); -const react = inject(DI.mfmEmojiReactCallback); +const react = inject(DI.mfmEmojiReactCallback, null); const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : prefer.s.emojiStyle === 'tossface' ? char2tossfaceFilePath : char2fluentEmojiFilePath; diff --git a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts index c9af5f4ea4..15938d0495 100644 --- a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts +++ b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts @@ -2,11 +2,10 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ + import { waitFor } from '@storybook/test'; -import type { StoryObj } from '@storybook/vue3'; import MkPageHeader from './MkPageHeader.vue'; +import type { StoryObj } from '@storybook/vue3'; export const Empty = { render(args) { return { @@ -29,7 +28,7 @@ export const Empty = { }; }, async play() { - const wait = new Promise((resolve) => setTimeout(resolve, 800)); + const wait = new Promise((resolve) => window.setTimeout(resolve, 800)); await waitFor(async () => await wait); }, args: { diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index 832c7887cb..b54e683179 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -133,7 +133,7 @@ async function enter(el: Element) { entering = false; }); - setTimeout(renderTab, 170); + window.setTimeout(renderTab, 170); } function afterEnter(el: Element) { diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 54ce0f85eb..1dda723fd2 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div v-if="show" ref="el" :class="[$style.root]" :style="{ background: bg }"> +<div v-if="show" ref="el" :class="[$style.root]"> <div :class="[$style.upper, { [$style.slim]: narrow, [$style.thin]: thin_ }]"> <div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu"> <MkAvatar :class="$style.avatar" :user="$i"/> @@ -51,7 +51,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'vue'; -import tinycolor from 'tinycolor2'; import { scrollToTop } from '@@/js/scroll.js'; import XTabs from './MkPageHeader.tabs.vue'; import type { Tab } from './MkPageHeader.tabs.vue'; @@ -82,7 +81,6 @@ const emit = defineEmits<{ const displayBackButton = props.displayBackButton && history.state.key !== 'index' && history.length > 1 && inject('shouldBackButton', true); const viewId = inject(DI.viewId); -const viewTransitionName = computed(() => `${viewId}---pageHeader`); const injectedPageMetadata = inject(DI.pageMetadata); const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value); @@ -90,7 +88,6 @@ const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props const thin_ = props.thin || inject('shouldHeaderThin', false); const el = useTemplateRef('el'); -const bg = ref<string | undefined>(undefined); const narrow = ref(false); const hasTabs = computed(() => props.tabs.length > 0); const hasActions = computed(() => props.actions && props.actions.length > 0); @@ -122,19 +119,9 @@ function goBack(): void { window.history.back(); } -const calcBg = () => { - const rawBg = 'var(--MI_THEME-bg)'; - const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(window.document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); - tinyBg.setAlpha(0.85); - bg.value = tinyBg.toRgbString(); -}; - let ro: ResizeObserver | null; onMounted(() => { - calcBg(); - globalEvents.on('themeChanging', calcBg); - if (el.value && el.value.parentElement) { narrow.value = el.value.parentElement.offsetWidth < 500; ro = new ResizeObserver((entries, observer) => { @@ -147,18 +134,17 @@ onMounted(() => { }); onUnmounted(() => { - globalEvents.off('themeChanging', calcBg); if (ro) ro.disconnect(); }); </script> <style lang="scss" module> .root { + background: color(from var(--MI_THEME-bg) srgb r g b / 0.75); -webkit-backdrop-filter: var(--MI-blur, blur(15px)); backdrop-filter: var(--MI-blur, blur(15px)); border-bottom: solid 0.5px var(--MI_THEME-divider); width: 100%; - view-transition-name: v-bind(viewTransitionName); } .upper, diff --git a/packages/frontend/src/components/global/PageWithHeader.vue b/packages/frontend/src/components/global/PageWithHeader.vue index fb813689ba..7ea0b5c97f 100644 --- a/packages/frontend/src/components/global/PageWithHeader.vue +++ b/packages/frontend/src/components/global/PageWithHeader.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']"> +<div ref="rootEl" :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']"> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="actions" :tabs="tabs"/></template> <div :class="$style.body"> @@ -16,6 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { useTemplateRef } from 'vue'; +import { scrollInContainer } from '@@/js/scroll.js'; import type { PageHeaderItem } from '@/types/page-header.js'; import type { Tab } from './MkPageHeader.tabs.vue'; @@ -31,6 +33,13 @@ const props = withDefaults(defineProps<{ }); const tab = defineModel<string>('tab'); +const rootEl = useTemplateRef('rootEl'); + +defineExpose({ + scrollToTop: () => { + if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'smooth' }); + }, +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 1c0c35f34e..78ac6900a3 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -44,7 +44,9 @@ provide(DI.routerCurrentDepth, currentDepth + 1); const rootEl = useTemplateRef('rootEl'); onMounted(() => { - rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入 + if (prefer.s.animation) { + rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入 + } }); // view-transition-newなどの<pt-name-selector>にはcss varが使えず、v-bindできないため直接スタイルを生成 diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue index 94f4f3dab1..c37f3df0d3 100644 --- a/packages/frontend/src/components/grid/MkGrid.vue +++ b/packages/frontend/src/components/grid/MkGrid.vue @@ -50,6 +50,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> import { computed, onMounted, ref, toRefs, watch } from 'vue'; +import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js'; +import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; +import type { GridContext, GridEvent } from '@/components/grid/grid-event.js'; +import type { GridColumn } from '@/components/grid/column.js'; +import type { GridRow, GridRowSetting } from '@/components/grid/row.js'; +import type { MenuItem } from '@/types/menu.js'; import { GridEventEmitter } from '@/components/grid/grid.js'; import MkDataRow from '@/components/grid/MkDataRow.vue'; import MkHeaderRow from '@/components/grid/MkHeaderRow.vue'; @@ -68,13 +74,6 @@ import { createColumn } from '@/components/grid/column.js'; import { createRow, defaultGridRowSetting, resetRow } from '@/components/grid/row.js'; import { handleKeyEvent } from '@/utility/key-event.js'; -import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js'; -import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; -import type { GridContext, GridEvent } from '@/components/grid/grid-event.js'; -import type { GridColumn } from '@/components/grid/column.js'; -import type { GridRow, GridRowSetting } from '@/components/grid/row.js'; -import type { MenuItem } from '@/types/menu.js'; - type RowHolder = { row: GridRow, cells: GridCell[], @@ -130,7 +129,7 @@ const bus = new GridEventEmitter(); * * @see {@link onResize} */ -const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries))); +const resizeObserver = new ResizeObserver((entries) => window.setTimeout(() => onResize(entries))); const rootEl = ref<InstanceType<typeof HTMLTableElement>>(); /** diff --git a/packages/frontend/src/di.ts b/packages/frontend/src/di.ts index b58c8c9659..541cdb76a8 100644 --- a/packages/frontend/src/di.ts +++ b/packages/frontend/src/di.ts @@ -15,4 +15,5 @@ export const DI = { currentStickyTop: Symbol() as InjectionKey<Ref<number>>, currentStickyBottom: Symbol() as InjectionKey<Ref<number>>, mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>, + inModal: Symbol() as InjectionKey<boolean>, }; diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index 7c15c9666a..3ebff5a3f6 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -165,13 +165,6 @@ export const navbarItemDef = reactive({ miLocalStorage.setItem('ui', 'deck'); unisonReload(); }, - }, { - text: i18n.ts.classic, - active: ui === 'classic', - action: () => { - miLocalStorage.setItem('ui', 'classic'); - unisonReload(); - }, }], ev.currentTarget ?? ev.target); }, }, diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index dce2a8e910..fe7f7d9cb4 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -13,6 +13,8 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/utility/form.js'; import type { MenuItem } from '@/types/menu.js'; import type { PostFormProps } from '@/types/post-form.js'; +import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue'; +import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; import { prefer } from '@/preferences.js'; import { i18n } from '@/i18n.js'; @@ -23,8 +25,6 @@ import MkToast from '@/components/MkToast.vue'; import MkDialog from '@/components/MkDialog.vue'; import MkPopupMenu from '@/components/MkPopupMenu.vue'; import MkContextMenu from '@/components/MkContextMenu.vue'; -import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue'; -import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { pleaseLogin } from '@/utility/please-login.js'; import { showMovedDialog } from '@/utility/show-moved-dialog.js'; diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue index 666e3c95ac..eff7efd0fa 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue @@ -18,11 +18,6 @@ SPDX-License-Identifier: AGPL-3.0-only </option> </MkSelect> - <MkSwitch v-model="keepOriginalUploading"> - <template #label>{{ i18n.ts.keepOriginalUploading }}</template> - <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template> - </MkSwitch> - <MkSwitch v-model="directoryToCategory"> <template #label>{{ i18n.ts._customEmojisManager._local._register.directoryToCategoryLabel }}</template> <template #caption>{{ i18n.ts._customEmojisManager._local._register.directoryToCategoryCaption }}</template> @@ -245,7 +240,6 @@ function setupGrid(): GridSetting { const uploadFolders = ref<FolderItem[]>([]); const gridItems = ref<GridItem[]>([]); const selectedFolderId = ref(prefer.s.uploadFolder); -const keepOriginalUploading = ref(prefer.s.keepOriginalUploading); const directoryToCategory = ref<boolean>(false); const registerButtonDisabled = ref<boolean>(false); const requestLogs = ref<RequestLogItem[]>([]); @@ -338,7 +332,7 @@ async function onDrop(ev: DragEvent) { it.file, selectedFolderId.value, it.file.name.replace(/\.[^.]+$/, ''), - keepOriginalUploading.value, + true, ), }), ), @@ -373,7 +367,7 @@ async function onFileSelectClicked() { true, { uploadFolder: selectedFolderId.value, - keepOriginal: keepOriginalUploading.value, + keepOriginal: true, // 拡張子は消す nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''), }, diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index 400c19ce93..c5c5b2144f 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; -import { scroll } from '@@/js/scroll.js'; +import { scrollInContainer } from '@@/js/scroll.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -49,7 +49,7 @@ function queueUpdated(q) { } function top() { - scroll(rootEl.value, { top: 0 }); + scrollInContainer(rootEl.value, { top: 0 }); } async function timetravel() { diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue index 843d2fd79b..ab57020613 100644 --- a/packages/frontend/src/pages/chat/XMessage.vue +++ b/packages/frontend/src/pages/chat/XMessage.vue @@ -242,6 +242,10 @@ function showMenu(ev: MouseEvent, contextmenu = false) { font-size: 80%; } +.fukidashi { + text-align: left; +} + .content { overflow: clip; overflow-wrap: break-word; diff --git a/packages/frontend/src/pages/chat/home.home.vue b/packages/frontend/src/pages/chat/home.home.vue index 0affef6333..49857db9ab 100644 --- a/packages/frontend/src/pages/chat/home.home.vue +++ b/packages/frontend/src/pages/chat/home.home.vue @@ -5,9 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps"> - <MkButton primary gradate rounded :class="$style.start" @click="start"><i class="ti ti-plus"></i> {{ i18n.ts.startChat }}</MkButton> + <MkButton v-if="$i.policies.canChat" primary gradate rounded :class="$style.start" @click="start"><i class="ti ti-plus"></i> {{ i18n.ts.startChat }}</MkButton> - <MkAd :prefer="['horizontal', 'horizontal-big']"/> + <MkInfo v-else>{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo> + + <MkAd :preferForms="['horizontal', 'horizontal-big']"/> <MkInput v-model="searchQuery" @@ -78,6 +80,7 @@ import * as os from '@/os.js'; import { updateCurrentAccountPartial } from '@/accounts.js'; import MkInput from '@/components/MkInput.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; +import MkInfo from '@/components/MkInfo.vue'; const $i = ensureSignin(); diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue index 5938fd2688..ec92a1dce1 100644 --- a/packages/frontend/src/pages/chat/room.vue +++ b/packages/frontend/src/pages/chat/room.vue @@ -41,6 +41,12 @@ SPDX-License-Identifier: AGPL-3.0-only <XMessage v-for="message in messages.toReversed()" :key="message.id" :message="message"/> </TransitionGroup> </div> + + <div v-if="user && (!user.canChat || user.host !== null)"> + <MkInfo warn>{{ i18n.ts._chat.chatNotAvailableInOtherAccount }}</MkInfo> + </div> + + <MkInfo v-if="!$i.policies.canChat" warn>{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo> </MkSpacer> <MkSpacer v-else-if="tab === 'search'" :contentMax="700"> @@ -93,6 +99,7 @@ import { prefer } from '@/preferences.js'; import MkButton from '@/components/MkButton.vue'; import { useRouter } from '@/router.js'; import { useMutationObserver } from '@/use/use-mutation-observer.js'; +import MkInfo from '@/components/MkInfo.vue'; const $i = ensureSignin(); const router = useRouter(); diff --git a/packages/frontend/src/pages/drive.vue b/packages/frontend/src/pages/drive.vue index c5813a4523..bee54f3fd2 100644 --- a/packages/frontend/src/pages/drive.vue +++ b/packages/frontend/src/pages/drive.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> - <XDrive ref="drive" @cd="x => folder = x"/> + <XDrive @cd="x => folder = x"/> </div> </template> diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index a644c83620..e1a51ccaad 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--MI_THEME-accent);">{{ i18n.ts._play.editThisPage }}</MkA> - <MkAd :prefer="['horizontal', 'horizontal-big']"/> + <MkAd :preferForms="['horizontal', 'horizontal-big']"/> </div> <MkError v-else-if="error" @retry="fetchFlash()"/> <MkLoading v-else/> diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index 77c4cf1f08..27f4687eb4 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFollowButton v-if="!$i || $i.id != post.user.id" v-model:user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/> </div> </div> - <MkAd :prefer="['horizontal', 'horizontal-big']"/> + <MkAd :preferForms="['horizontal', 'horizontal-big']"/> <MkContainer :max-height="300" :foldable="true" class="other"> <template #icon><i class="ti ti-clock"></i></template> <template #header>{{ i18n.ts.recentPosts }}</template> diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index a7ff519a34..0a2bc02de5 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -6,17 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"> <MkSpacer :contentMax="800"> - <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> - <div v-if="tab === 'all'"> - <XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/> - </div> - <div v-else-if="tab === 'mentions'"> - <MkNotes :pagination="mentionsPagination"/> - </div> - <div v-else-if="tab === 'directNotes'"> - <MkNotes :pagination="directNotesPagination"/> - </div> - </MkHorizontalSwipe> + <div v-if="tab === 'all'"> + <XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/> + </div> + <div v-else-if="tab === 'mentions'"> + <MkNotes :pagination="mentionsPagination"/> + </div> + <div v-else-if="tab === 'directNotes'"> + <MkNotes :pagination="directNotesPagination"/> + </div> </MkSpacer> </PageWithHeader> </template> diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 637821e74f..9e76a450e6 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -80,7 +80,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="page.createdAt != page.updatedAt"><i class="ti ti-clock-edit"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div> </div> </div> - <MkAd :prefer="['horizontal', 'horizontal-big']"/> + <MkAd :preferForms="['horizontal', 'horizontal-big']"/> <MkContainer :max-height="300" :foldable="true" class="other"> <template #icon><i class="ti ti-clock"></i></template> <template #header>{{ i18n.ts.recentPosts }}</template> diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 403a760521..b7434bff9f 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -145,13 +145,13 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import * as Reversi from 'misskey-reversi'; +import { useInterval } from '@@/js/use-interval.js'; +import { url } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import { deepClone } from '@/utility/clone.js'; -import { useInterval } from '@@/js/use-interval.js'; import { ensureSignin } from '@/i.js'; -import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { userPage } from '@/filters/user.js'; @@ -301,7 +301,7 @@ if (!props.game.isEnded) { if (iAmPlayer.value) { if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) { - props.connection!.send('claimTimeIsUp', {}); + props.connection!.send('claimTimeIsUp', {}); } } }, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true }); @@ -424,7 +424,7 @@ function autoplay() { const tick = () => { const log = logs[i]; const time = log.time - previousLog.time; - setTimeout(() => { + window.setTimeout(() => { i++; logPos.value++; previousLog = log; diff --git a/packages/frontend/src/pages/settings/accessibility.vue b/packages/frontend/src/pages/settings/accessibility.vue deleted file mode 100644 index e8268719f5..0000000000 --- a/packages/frontend/src/pages/settings/accessibility.vue +++ /dev/null @@ -1,173 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<SearchMarker path="/settings/accessibility" :label="i18n.ts.accessibility" :keywords="['accessibility']" icon="ti ti-accessible"> - <div class="_gaps_m"> - <MkFeatureBanner icon="/client-assets/mens_room_3d.png" color="#0011ff"> - <SearchKeyword>{{ i18n.ts._settings.accessibilityBanner }}</SearchKeyword> - </MkFeatureBanner> - - <div class="_gaps_s"> - <SearchMarker :keywords="['animation', 'motion', 'reduce']"> - <MkPreferenceContainer k="animation"> - <MkSwitch v-model="reduceAnimation"> - <template #label><SearchLabel>{{ i18n.ts.reduceUiAnimation }}</SearchLabel></template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - - <SearchMarker :keywords="['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif']"> - <MkPreferenceContainer k="disableShowingAnimatedImages"> - <MkSwitch v-model="disableShowingAnimatedImages"> - <template #label><SearchLabel>{{ i18n.ts.disableShowingAnimatedImages }}</SearchLabel></template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - - <SearchMarker :keywords="['mfm', 'enable', 'show', 'animated']"> - <MkPreferenceContainer k="animatedMfm"> - <MkSwitch v-model="animatedMfm"> - <template #label><SearchLabel>{{ i18n.ts.enableAnimatedMfm }}</SearchLabel></template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - - <SearchMarker :keywords="['swipe', 'horizontal', 'tab']"> - <MkPreferenceContainer k="enableHorizontalSwipe"> - <MkSwitch v-model="enableHorizontalSwipe"> - <template #label><SearchLabel>{{ i18n.ts.enableHorizontalSwipe }}</SearchLabel></template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - - <SearchMarker :keywords="['keep', 'screen', 'display', 'on']"> - <MkPreferenceContainer k="keepScreenOn"> - <MkSwitch v-model="keepScreenOn"> - <template #label><SearchLabel>{{ i18n.ts.keepScreenOn }}</SearchLabel></template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - - <SearchMarker :keywords="['native', 'system', 'video', 'audio', 'player', 'media']"> - <MkPreferenceContainer k="useNativeUiForVideoAudioPlayer"> - <MkSwitch v-model="useNativeUiForVideoAudioPlayer"> - <template #label><SearchLabel>{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</SearchLabel></template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - - <SearchMarker :keywords="['text', 'selectable']"> - <MkPreferenceContainer k="makeEveryTextElementsSelectable"> - <MkSwitch v-model="makeEveryTextElementsSelectable"> - <template #label><SearchLabel>{{ i18n.ts._settings.makeEveryTextElementsSelectable }}</SearchLabel></template> - <template #caption>{{ i18n.ts._settings.makeEveryTextElementsSelectable_description }}</template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - </div> - - <SearchMarker :keywords="['menu', 'style', 'popup', 'drawer']"> - <MkPreferenceContainer k="menuStyle"> - <MkSelect v-model="menuStyle"> - <template #label><SearchLabel>{{ i18n.ts.menuStyle }}</SearchLabel></template> - <option value="auto">{{ i18n.ts.auto }}</option> - <option value="popup">{{ i18n.ts.popup }}</option> - <option value="drawer">{{ i18n.ts.drawer }}</option> - </MkSelect> - </MkPreferenceContainer> - </SearchMarker> - - <SearchMarker :keywords="['contextmenu', 'system', 'native']"> - <MkPreferenceContainer k="contextMenu"> - <MkSelect v-model="contextMenu"> - <template #label><SearchLabel>{{ i18n.ts._contextMenu.title }}</SearchLabel></template> - <option value="app">{{ i18n.ts._contextMenu.app }}</option> - <option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option> - <option value="native">{{ i18n.ts._contextMenu.native }}</option> - </MkSelect> - </MkPreferenceContainer> - </SearchMarker> - - <SearchMarker :keywords="['font', 'size']"> - <MkRadios v-model="fontSize"> - <template #label><SearchLabel>{{ i18n.ts.fontSize }}</SearchLabel></template> - <option :value="null"><span style="font-size: 14px;">Aa</span></option> - <option value="1"><span style="font-size: 15px;">Aa</span></option> - <option value="2"><span style="font-size: 16px;">Aa</span></option> - <option value="3"><span style="font-size: 17px;">Aa</span></option> - </MkRadios> - </SearchMarker> - - <SearchMarker :keywords="['font', 'system', 'native']"> - <MkSwitch v-model="useSystemFont"> - <template #label><SearchLabel>{{ i18n.ts.useSystemFont }}</SearchLabel></template> - </MkSwitch> - </SearchMarker> - </div> -</SearchMarker> -</template> - -<script lang="ts" setup> -import { computed, ref, watch } from 'vue'; -import MkSwitch from '@/components/MkSwitch.vue'; -import MkSelect from '@/components/MkSelect.vue'; -import { prefer } from '@/preferences.js'; -import { reloadAsk } from '@/utility/reload-ask.js'; -import { i18n } from '@/i18n.js'; -import { definePage } from '@/page.js'; -import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue'; -import MkFeatureBanner from '@/components/MkFeatureBanner.vue'; -import { miLocalStorage } from '@/local-storage.js'; -import MkRadios from '@/components/MkRadios.vue'; - -const reduceAnimation = prefer.model('animation', v => !v, v => !v); -const animatedMfm = prefer.model('animatedMfm'); -const disableShowingAnimatedImages = prefer.model('disableShowingAnimatedImages'); -const keepScreenOn = prefer.model('keepScreenOn'); -const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe'); -const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer'); -const contextMenu = prefer.model('contextMenu'); -const menuStyle = prefer.model('menuStyle'); -const makeEveryTextElementsSelectable = prefer.model('makeEveryTextElementsSelectable'); - -const fontSize = ref(miLocalStorage.getItem('fontSize')); -const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null); - -watch(fontSize, () => { - if (fontSize.value == null) { - miLocalStorage.removeItem('fontSize'); - } else { - miLocalStorage.setItem('fontSize', fontSize.value); - } -}); - -watch(useSystemFont, () => { - if (useSystemFont.value) { - miLocalStorage.setItem('useSystemFont', 't'); - } else { - miLocalStorage.removeItem('useSystemFont'); - } -}); - -watch([ - keepScreenOn, - contextMenu, - fontSize, - useSystemFont, - makeEveryTextElementsSelectable, -], async () => { - await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); -}); - -const headerActions = computed(() => []); - -const headerTabs = computed(() => []); - -definePage(() => ({ - title: i18n.ts.accessibility, - icon: 'ti ti-accessible', -})); -</script> diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index 57161aa666..b6c6a544ea 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -53,15 +53,6 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.drivecleaner }} </FormLink> - <SearchMarker :keywords="['keep', 'original', 'raw', 'upload']"> - <MkPreferenceContainer k="keepOriginalUploading"> - <MkSwitch v-model="keepOriginalUploading"> - <template #label><SearchLabel>{{ i18n.ts.keepOriginalUploading }}</SearchLabel></template> - <template #caption><SearchKeyword>{{ i18n.ts.keepOriginalUploadingDescription }}</SearchKeyword></template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - <SearchMarker :keywords="['keep', 'original', 'filename']"> <MkPreferenceContainer k="keepOriginalFilename"> <MkSwitch v-model="keepOriginalFilename"> @@ -122,7 +113,6 @@ const meterStyle = computed(() => { }; }); -const keepOriginalUploading = prefer.model('keepOriginalUploading'); const keepOriginalFilename = prefer.model('keepOriginalFilename'); misskeyApi('drive').then(info => { diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index a1e1460da1..4ed4cdc773 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -123,11 +123,6 @@ const menuDef = computed<SuperMenuDef[]>(() => [{ to: '/settings/sounds', active: currentPage.value?.route.name === 'sounds', }, { - icon: 'ti ti-accessible', - text: i18n.ts.accessibility, - to: '/settings/accessibility', - active: currentPage.value?.route.name === 'accessibility', - }, { icon: 'ti ti-plug', text: i18n.ts.plugins, to: '/settings/plugin', diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index f944490a66..91968c5300 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -40,8 +40,6 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.display }}</template> <option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option> <option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option> - <option value="top">{{ i18n.ts._menuDisplay.top }}</option> - <!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ i18n.ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 --> </MkRadios> <SearchMarker :keywords="['navbar', 'sidebar', 'toggle', 'button', 'sub']"> diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue index ac1f860d59..9de03ceec5 100644 --- a/packages/frontend/src/pages/settings/preferences.vue +++ b/packages/frontend/src/pages/settings/preferences.vue @@ -42,22 +42,6 @@ SPDX-License-Identifier: AGPL-3.0-only </SearchMarker> <div class="_gaps_s"> - <SearchMarker :keywords="['blur']"> - <MkPreferenceContainer k="useBlurEffect"> - <MkSwitch v-model="useBlurEffect"> - <template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - - <SearchMarker :keywords="['blur', 'modal']"> - <MkPreferenceContainer k="useBlurEffectForModal"> - <MkSwitch v-model="useBlurEffectForModal"> - <template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - <SearchMarker :keywords="['avatar', 'icon', 'decoration', 'show']"> <MkPreferenceContainer k="showAvatarDecorations"> <MkSwitch v-model="showAvatarDecorations"> @@ -120,15 +104,6 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkPreferenceContainer> </SearchMarker> - - <SearchMarker :keywords="['pinned', 'list']"> - <MkFolder> - <template #label><SearchLabel>{{ i18n.ts.pinnedList }}</SearchLabel></template> - <!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ --> - <MkButton v-if="prefer.r.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton> - <MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> - </MkFolder> - </SearchMarker> </div> </MkFolder> </SearchMarker> @@ -199,6 +174,15 @@ SPDX-License-Identifier: AGPL-3.0-only </MkPreferenceContainer> </SearchMarker> + <SearchMarker :keywords="['pinned', 'list']"> + <MkFolder> + <template #label><SearchLabel>{{ i18n.ts.pinnedList }}</SearchLabel></template> + <!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ --> + <MkButton v-if="prefer.r.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton> + <MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> + </MkFolder> + </SearchMarker> + <SearchMarker :keywords="['show', 'ticker', 'replies']"> <MkSwitch v-model="showTickerOnReplies"> <template #label>{{ i18n.ts.showTickerOnReplies }}</template> @@ -478,40 +462,6 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> </SearchMarker> - <SearchMarker :keywords="['datasaver']"> - <MkFolder> - <template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template> - <template #icon><i class="ti ti-antenna-bars-3"></i></template> - - <div class="_gaps_m"> - <MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo> - - <div class="_buttons"> - <MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton> - <MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton> - </div> - <div class="_gaps_m"> - <MkSwitch v-model="dataSaver.media"> - {{ i18n.ts._dataSaver._media.title }} - <template #caption>{{ i18n.ts._dataSaver._media.description }}</template> - </MkSwitch> - <MkSwitch v-model="dataSaver.avatar"> - {{ i18n.ts._dataSaver._avatar.title }} - <template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template> - </MkSwitch> - <MkSwitch v-model="dataSaver.urlPreview"> - {{ i18n.ts._dataSaver._urlPreview.title }} - <template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template> - </MkSwitch> - <MkSwitch v-model="dataSaver.code"> - {{ i18n.ts._dataSaver._code.title }} - <template #caption>{{ i18n.ts._dataSaver._code.description }}</template> - </MkSwitch> - </div> - </div> - </MkFolder> - </SearchMarker> - <SearchMarker :keywords="['chat', 'messaging']"> <MkFolder> <template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template> @@ -551,6 +501,186 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> </SearchMarker> + <SearchMarker :keywords="['accessibility']"> + <MkFolder> + <template #label><SearchLabel>{{ i18n.ts.accessibility }}</SearchLabel></template> + <template #icon><i class="ti ti-accessible"></i></template> + + <div class="_gaps_m"> + <MkFeatureBanner icon="/client-assets/mens_room_3d.png" color="#0011ff"> + <SearchKeyword>{{ i18n.ts._settings.accessibilityBanner }}</SearchKeyword> + </MkFeatureBanner> + + <div class="_gaps_s"> + <SearchMarker :keywords="['animation', 'motion', 'reduce']"> + <MkPreferenceContainer k="animation"> + <MkSwitch v-model="reduceAnimation"> + <template #label><SearchLabel>{{ i18n.ts.reduceUiAnimation }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif']"> + <MkPreferenceContainer k="disableShowingAnimatedImages"> + <MkSwitch v-model="disableShowingAnimatedImages"> + <template #label><SearchLabel>{{ i18n.ts.disableShowingAnimatedImages }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['mfm', 'enable', 'show', 'animated']"> + <MkPreferenceContainer k="animatedMfm"> + <MkSwitch v-model="animatedMfm"> + <template #label><SearchLabel>{{ i18n.ts.enableAnimatedMfm }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['swipe', 'horizontal', 'tab']"> + <MkPreferenceContainer k="enableHorizontalSwipe"> + <MkSwitch v-model="enableHorizontalSwipe"> + <template #label><SearchLabel>{{ i18n.ts.enableHorizontalSwipe }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['keep', 'screen', 'display', 'on']"> + <MkPreferenceContainer k="keepScreenOn"> + <MkSwitch v-model="keepScreenOn"> + <template #label><SearchLabel>{{ i18n.ts.keepScreenOn }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['native', 'system', 'video', 'audio', 'player', 'media']"> + <MkPreferenceContainer k="useNativeUiForVideoAudioPlayer"> + <MkSwitch v-model="useNativeUiForVideoAudioPlayer"> + <template #label><SearchLabel>{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['text', 'selectable']"> + <MkPreferenceContainer k="makeEveryTextElementsSelectable"> + <MkSwitch v-model="makeEveryTextElementsSelectable"> + <template #label><SearchLabel>{{ i18n.ts._settings.makeEveryTextElementsSelectable }}</SearchLabel></template> + <template #caption>{{ i18n.ts._settings.makeEveryTextElementsSelectable_description }}</template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + </div> + + <SearchMarker :keywords="['menu', 'style', 'popup', 'drawer']"> + <MkPreferenceContainer k="menuStyle"> + <MkSelect v-model="menuStyle"> + <template #label><SearchLabel>{{ i18n.ts.menuStyle }}</SearchLabel></template> + <option value="auto">{{ i18n.ts.auto }}</option> + <option value="popup">{{ i18n.ts.popup }}</option> + <option value="drawer">{{ i18n.ts.drawer }}</option> + </MkSelect> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['contextmenu', 'system', 'native']"> + <MkPreferenceContainer k="contextMenu"> + <MkSelect v-model="contextMenu"> + <template #label><SearchLabel>{{ i18n.ts._contextMenu.title }}</SearchLabel></template> + <option value="app">{{ i18n.ts._contextMenu.app }}</option> + <option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option> + <option value="native">{{ i18n.ts._contextMenu.native }}</option> + </MkSelect> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['font', 'size']"> + <MkRadios v-model="fontSize"> + <template #label><SearchLabel>{{ i18n.ts.fontSize }}</SearchLabel></template> + <option :value="null"><span style="font-size: 14px;">Aa</span></option> + <option value="1"><span style="font-size: 15px;">Aa</span></option> + <option value="2"><span style="font-size: 16px;">Aa</span></option> + <option value="3"><span style="font-size: 17px;">Aa</span></option> + </MkRadios> + </SearchMarker> + + <SearchMarker :keywords="['font', 'system', 'native']"> + <MkSwitch v-model="useSystemFont"> + <template #label><SearchLabel>{{ i18n.ts.useSystemFont }}</SearchLabel></template> + </MkSwitch> + </SearchMarker> + </div> + </MkFolder> + </SearchMarker> + + <SearchMarker :keywords="['performance']"> + <MkFolder> + <template #label><SearchLabel>{{ i18n.ts.performance }}</SearchLabel></template> + <template #icon><i class="ti ti-battery-vertical-eco"></i></template> + + <div class="_gaps_s"> + <SearchMarker :keywords="['blur']"> + <MkPreferenceContainer k="useBlurEffect"> + <MkSwitch v-model="useBlurEffect"> + <template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template> + <template #caption><SearchLabel>{{ i18n.ts.turnOffToImprovePerformance }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['blur', 'modal']"> + <MkPreferenceContainer k="useBlurEffectForModal"> + <MkSwitch v-model="useBlurEffectForModal"> + <template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template> + <template #caption><SearchLabel>{{ i18n.ts.turnOffToImprovePerformance }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['sticky']"> + <MkPreferenceContainer k="useStickyIcons"> + <MkSwitch v-model="useStickyIcons"> + <template #label><SearchLabel>{{ i18n.ts._settings.useStickyIcons }}</SearchLabel></template> + <template #caption><SearchLabel>{{ i18n.ts.turnOffToImprovePerformance }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + </div> + </MkFolder> + </SearchMarker> + + <SearchMarker :keywords="['datasaver']"> + <MkFolder> + <template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template> + <template #icon><i class="ti ti-antenna-bars-3"></i></template> + + <div class="_gaps_m"> + <MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo> + + <div class="_buttons"> + <MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton> + <MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton> + </div> + <div class="_gaps_m"> + <MkSwitch v-model="dataSaver.media"> + {{ i18n.ts._dataSaver._media.title }} + <template #caption>{{ i18n.ts._dataSaver._media.description }}</template> + </MkSwitch> + <MkSwitch v-model="dataSaver.avatar"> + {{ i18n.ts._dataSaver._avatar.title }} + <template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template> + </MkSwitch> + <MkSwitch v-model="dataSaver.urlPreview"> + {{ i18n.ts._dataSaver._urlPreview.title }} + <template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template> + </MkSwitch> + <MkSwitch v-model="dataSaver.code"> + {{ i18n.ts._dataSaver._code.title }} + <template #caption>{{ i18n.ts._dataSaver._code.description }}</template> + </MkSwitch> + </div> + </div> + </MkFolder> + </SearchMarker> + <SearchMarker :keywords="['other']"> <MkFolder> <template #label><SearchLabel>{{ i18n.ts.other }}</SearchLabel></template> @@ -784,6 +914,19 @@ const useBlurEffect = prefer.model('useBlurEffect'); const defaultFollowWithReplies = prefer.model('defaultFollowWithReplies'); const chatShowSenderName = prefer.model('chat.showSenderName'); const chatSendOnEnter = prefer.model('chat.sendOnEnter'); +const useStickyIcons = prefer.model('useStickyIcons'); +const reduceAnimation = prefer.model('animation', v => !v, v => !v); +const animatedMfm = prefer.model('animatedMfm'); +const disableShowingAnimatedImages = prefer.model('disableShowingAnimatedImages'); +const keepScreenOn = prefer.model('keepScreenOn'); +const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe'); +const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer'); +const contextMenu = prefer.model('contextMenu'); +const menuStyle = prefer.model('menuStyle'); +const makeEveryTextElementsSelectable = prefer.model('makeEveryTextElementsSelectable'); + +const fontSize = ref(miLocalStorage.getItem('fontSize')); +const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null); // Sharkey options const collapseNotesRepliedTo = prefer.model('collapseNotesRepliedTo'); @@ -811,6 +954,22 @@ watch(lang, () => { miLocalStorage.removeItem('localeVersion'); }); +watch(fontSize, () => { + if (fontSize.value == null) { + miLocalStorage.removeItem('fontSize'); + } else { + miLocalStorage.setItem('fontSize', fontSize.value); + } +}); + +watch(useSystemFont, () => { + if (useSystemFont.value) { + miLocalStorage.setItem('useSystemFont', 't'); + } else { + miLocalStorage.removeItem('useSystemFont'); + } +}); + watch([ hemisphere, lang, @@ -832,6 +991,12 @@ watch([ highlightSensitiveMedia, enableSeasonalScreenEffect, chatShowSenderName, + useStickyIcons, + keepScreenOn, + contextMenu, + fontSize, + useSystemFont, + makeEveryTextElementsSelectable, ], async () => { await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); }); @@ -940,7 +1105,7 @@ function testNotification(): void { smashCount = 0; } if (smashTimer) { - clearTimeout(smashTimer); + window.clearTimeout(smashTimer); } smashTimer = window.setTimeout(() => { smashCount = 0; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 39281db947..9c6755d571 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -4,48 +4,44 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<PageWithHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"> - <MkSpacer :contentMax="800"> - <MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin"> - <div ref="rootEl"> - <MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()"> - {{ i18n.ts._timelineDescription[src] }} - </MkInfo> - <MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/> - <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> - <div :class="$style.tl"> - <MkTimeline - ref="tlComponent" - :key="src + withRenotes + withBots + withReplies + onlyFiles + withSensitive" - :src="src.split(':')[0]" - :list="src.split(':')[1]" - :withRenotes="withRenotes" - :withReplies="withReplies" - :withSensitive="withSensitive" - :onlyFiles="onlyFiles" - :withBots="withBots" - :sound="true" - @queue="queueUpdated" - /> - </div> - </div> - </MkHorizontalSwipe> - </MkSpacer> -</PageWithHeader> +<div ref="rootEl" class="_pageScrollable"> + <MkStickyContainer> + <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template> + <MkSpacer :contentMax="800"> + <MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()"> + {{ i18n.ts._timelineDescription[src] }} + </MkInfo> + <MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="_panel" fixed style="margin-bottom: var(--MI-margin);"/> + <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> + <MkTimeline + ref="tlComponent" + :key="src + withRenotes + withBots + withReplies + onlyFiles + withSensitive" + :class="$style.tl" + :src="src.split(':')[0]" + :list="src.split(':')[1]" + :withRenotes="withRenotes" + :withReplies="withReplies" + :withSensitive="withSensitive" + :onlyFiles="onlyFiles" + :withBots="withBots" + :sound="true" + @queue="queueUpdated" + /> + </MkSpacer> + </MkStickyContainer> +</div> </template> <script lang="ts" setup> import { computed, watch, provide, useTemplateRef, ref, onMounted, onActivated } from 'vue'; -import { scroll } from '@@/js/scroll.js'; +import { scrollInContainer } from '@@/js/scroll.js'; import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; import type { MenuItem } from '@/types/menu.js'; import type { BasicTimelineType } from '@/timelines.js'; import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkPostForm from '@/components/MkPostForm.vue'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/utility/misskey-api.js'; import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/i.js'; @@ -139,7 +135,7 @@ function queueUpdated(q: number): void { } function top(): void { - if (rootEl.value) scroll(rootEl.value, { top: 0, behavior: 'smooth' }); + if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'instant' }); } async function chooseList(ev: MouseEvent): Promise<void> { diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index 2b9d644eca..45fe53328a 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; -import { scroll } from '@@/js/scroll.js'; +import { scrollInContainer } from '@@/js/scroll.js'; import MkTimeline from '@/components/MkTimeline.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; import { definePage } from '@/page.js'; @@ -77,7 +77,7 @@ function queueUpdated(q) { } function top() { - scroll(rootEl.value, { top: 0 }); + scrollInContainer(rootEl.value, { top: 0 }); } function settings() { diff --git a/packages/frontend/src/pref-migrate.ts b/packages/frontend/src/pref-migrate.ts index beeaac8383..5d48508def 100644 --- a/packages/frontend/src/pref-migrate.ts +++ b/packages/frontend/src/pref-migrate.ts @@ -66,7 +66,6 @@ export function migrateOldSettings() { prefer.commit('collapseRenotes', store.s.collapseRenotes); prefer.commit('rememberNoteVisibility', store.s.rememberNoteVisibility); prefer.commit('uploadFolder', store.s.uploadFolder); - prefer.commit('keepOriginalUploading', store.s.keepOriginalUploading); prefer.commit('menu', store.s.menu); prefer.commit('statusbars', store.s.statusbars); prefer.commit('pinnedUserLists', store.s.pinnedUserLists); diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts index b588cc3b5f..37fa9471ee 100644 --- a/packages/frontend/src/preferences/def.ts +++ b/packages/frontend/src/preferences/def.ts @@ -118,9 +118,6 @@ export const PREF_DEF = { keepCw: { default: true, }, - keepOriginalUploading: { - default: false, - }, rememberNoteVisibility: { default: false, }, @@ -201,6 +198,9 @@ export const PREF_DEF = { useBlurEffect: { default: DEFAULT_DEVICE_KIND === 'desktop', }, + useStickyIcons: { + default: true, + }, showFixedPostForm: { default: false, }, diff --git a/packages/frontend/src/router.definition.ts b/packages/frontend/src/router.definition.ts index b8c6739490..943aa67cd3 100644 --- a/packages/frontend/src/router.definition.ts +++ b/packages/frontend/src/router.definition.ts @@ -129,10 +129,6 @@ export const ROUTE_DEF = [{ name: 'sounds', component: page(() => import('@/pages/settings/sounds.vue')), }, { - path: '/accessibility', - name: 'accessibility', - component: page(() => import('@/pages/settings/accessibility.vue')), - }, { path: '/plugin/install', name: 'plugin', component: page(() => import('@/pages/settings/plugin.install.vue')), diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 6568c738c5..dce5e0371e 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -58,9 +58,6 @@ html.radius-misskey { } html { - background-color: var(--MI_THEME-bg); - color: var(--MI_THEME-fg); - accent-color: var(--MI_THEME-accent); overflow: auto; overflow-wrap: break-word; font-family: 'sharkey-theme-font-face', 'Lexend', 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; @@ -69,6 +66,11 @@ html { text-size-adjust: 100%; tab-size: 2; -webkit-text-size-adjust: 100%; + touch-action: manipulation; + scroll-behavior: smooth; + background-color: var(--MI_THEME-bg); + color: var(--MI_THEME-fg); + accent-color: var(--MI_THEME-accent); &, * { scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; @@ -124,11 +126,19 @@ html._themeChanging_ { } } -html, body { - touch-action: manipulation; +html, +body, +#misskey_app { margin: 0; padding: 0; - scroll-behavior: smooth; + width: 100%; + height: 100%; + overscroll-behavior: none; +} + +body { + /* NOTE: htmlにも overflow: clip を設定したいところだが、設定すると何故か少なくともChromeで html が main thread scrolling になりパフォーマンスが(多分)落ちる */ + overflow: clip; } a { @@ -208,6 +218,14 @@ rt { overflow: clip; overflow-y: scroll; overscroll-behavior: contain; + + /* + 理屈は知らないけど、ここでbackgroundを設定しておかないと + スクロールコンテナーが少なくともChromeにおいて + main thread scrolling になってしまい、パフォーマンスが(多分)落ちる。 + backgroundが透明だと裏側を描画しないといけなくなるとかそういう理由かもしれない + */ + background: var(--MI_THEME-bg); } ._pageScrollableReversed { diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index a7203b1eb9..ae22b4a015 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -14,7 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only <XUpload v-if="uploads.length > 0"/> -<TransitionGroup +<component + :is="prefer.s.animation ? TransitionGroup : 'div'" tag="div" :class="[$style.notifications, { [$style.notificationsPosition_leftTop]: prefer.s.notificationPosition === 'leftTop', @@ -24,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.notificationsStackAxis_vertical]: prefer.s.notificationStackAxis === 'vertical', [$style.notificationsStackAxis_horizontal]: prefer.s.notificationStackAxis === 'horizontal', }]" - :moveClass="prefer.s.animation ? $style.transition_notification_move : ''" - :enterActiveClass="prefer.s.animation ? $style.transition_notification_enterActive : ''" - :leaveActiveClass="prefer.s.animation ? $style.transition_notification_leaveActive : ''" - :enterFromClass="prefer.s.animation ? $style.transition_notification_enterFrom : ''" - :leaveToClass="prefer.s.animation ? $style.transition_notification_leaveTo : ''" + :moveClass="$style.transition_notification_move" + :enterActiveClass="$style.transition_notification_enterActive" + :leaveActiveClass="$style.transition_notification_leaveActive" + :enterFromClass="$style.transition_notification_enterFrom" + :leaveToClass="$style.transition_notification_leaveTo" > <div v-for="notification in notifications" :key="notification.id" :class="$style.notification" :style="{ @@ -37,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <XNotification :notification="notification"/> </div> -</TransitionGroup> +</component> <XStreamIndicator/> @@ -51,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, ref } from 'vue'; +import { defineAsyncComponent, ref, TransitionGroup } from 'vue'; import * as Misskey from 'misskey-js'; import { swInject } from './sw-inject.js'; import XNotification from './notification.vue'; diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue deleted file mode 100644 index 7d4235bd4e..0000000000 --- a/packages/frontend/src/ui/classic.header.vue +++ /dev/null @@ -1,212 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<div class="azykntjl"> - <div class="body"> - <div class="left"> - <button v-click-anime class="item _button instance" @click="openInstanceMenu"> - <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" draggable="false"/> - </button> - <MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact> - <i class="ti ti-home ti-fw"></i> - </MkA> - <template v-for="item in menu"> - <div v-if="item === '-'" class="divider"></div> - <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> - <i class="ti-fw" :class="navbarItemDef[item].icon"></i> - <span v-if="navbarItemDef[item].indicated" class="indicator _blink"><i class="_indicatorCircle"></i></span> - </component> - </template> - <div class="divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip="i18n.ts.controlPanel" class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null"> - <i class="ti ti-dashboard ti-fw"></i> - </MkA> - <button v-click-anime class="item _button" @click="more"> - <i class="ti ti-dots ti-fw"></i> - <span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span> - </button> - </div> - <div class="right"> - <MkA v-click-anime v-tooltip="i18n.ts.settings" class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null"> - <i class="ti ti-settings ti-fw"></i> - </MkA> - <button v-click-anime class="item _button account" @click="openAccountMenu"> - <MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/> - </button> - <div class="post" @click="os.post()"> - <MkButton class="button" gradate full rounded> - <i class="ti ti-pencil ti-fw"></i> - </MkButton> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts" setup> -import { computed, defineAsyncComponent, onMounted, ref } from 'vue'; -import { openInstanceMenu } from './_common_/common.js'; -import * as os from '@/os.js'; -import { navbarItemDef } from '@/navbar.js'; -import MkButton from '@/components/MkButton.vue'; -import { instance } from '@/instance.js'; -import { i18n } from '@/i18n.js'; -import { prefer } from '@/preferences.js'; -import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; -import { $i } from '@/i.js'; - -const WINDOW_THRESHOLD = 1400; - -const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD); -const menu = ref(prefer.s.menu); -// const menuDisplay = computed(store.makeGetterSetter('menuDisplay')); -const otherNavItemIndicated = computed<boolean>(() => { - for (const def in navbarItemDef) { - if (menu.value.includes(def)) continue; - if (navbarItemDef[def].indicated) return true; - } - return false; -}); - -function more(ev: MouseEvent) { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { - src: ev.currentTarget ?? ev.target, - anchor: { x: 'center', y: 'bottom' }, - }, { - closed: () => dispose(), - }); -} - -function openAccountMenu(ev: MouseEvent) { - openAccountMenu_({ - withExtraOperation: true, - }, ev); -} - -onMounted(() => { - window.addEventListener('resize', () => { - settingsWindowed.value = (window.innerWidth >= WINDOW_THRESHOLD); - }, { passive: true }); -}); - -</script> - -<style lang="scss" scoped> -.azykntjl { - $height: 60px; - $avatar-size: 32px; - $avatar-margin: 8px; - - position: sticky; - top: 0; - z-index: 1000; - width: 100%; - height: $height; - background-color: var(--MI_THEME-bg); - - > .body { - max-width: 1380px; - margin: 0 auto; - display: flex; - - > .right, - > .left { - - > .item { - position: relative; - font-size: 0.9em; - display: inline-block; - padding: 0 12px; - line-height: $height; - - > i, - > .avatar { - margin-right: 0; - } - - > i { - left: 10px; - } - - > .avatar { - width: $avatar-size; - height: $avatar-size; - vertical-align: middle; - } - - > .indicator { - position: absolute; - top: 0; - left: 0; - color: var(--MI_THEME-navIndicator); - font-size: 8px; - } - - &:hover { - text-decoration: none; - color: var(--MI_THEME-navHoverFg); - } - - &.active { - color: var(--MI_THEME-navActive); - } - } - - > .divider { - display: inline-block; - height: 16px; - margin: 0 10px; - border-right: solid 0.5px var(--MI_THEME-divider); - } - - > .instance { - display: inline-block; - position: relative; - width: 56px; - height: 100%; - vertical-align: bottom; - - > img { - display: inline-block; - width: 24px; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - } - } - - > .post { - display: inline-block; - - > .button { - width: 40px; - height: 40px; - padding: 0; - min-width: 0; - } - } - - > .account { - display: inline-flex; - align-items: center; - vertical-align: top; - margin-right: 8px; - - > .acct { - margin-left: 8px; - } - } - } - - > .right { - margin-left: auto; - } - } -} -</style> diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue deleted file mode 100644 index 6e81f72549..0000000000 --- a/packages/frontend/src/ui/classic.sidebar.vue +++ /dev/null @@ -1,249 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<div class="npcljfve" :class="{ iconOnly }"> - <button v-click-anime class="item _button account" @click="openAccountMenu"> - <MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/> - </button> - <div class="post" data-cy-open-post-form @click="os.post"> - <MkButton class="button" gradate full rounded> - <i class="ti ti-pencil ti-fw"></i><span v-if="!iconOnly" class="text">{{ i18n.ts.note }}</span> - </MkButton> - </div> - <div class="divider"></div> - <MkA v-click-anime class="item index" activeClass="active" to="/" exact> - <i class="ti ti-home ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span> - </MkA> - <template v-for="item in menu"> - <div v-if="item === '-'" class="divider"></div> - <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> - <i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span> - <span v-if="navbarItemDef[item].indicated" class="indicator _blink"> - <span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span> - <i v-else class="_indicatorCircle"></i> - </span> - </component> - </template> - <div class="divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null"> - <i class="ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span> - </MkA> - <button v-click-anime class="item _button" @click="more"> - <i class="ti ti-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span> - <span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span> - </button> - <MkA v-click-anime class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null"> - <i class="ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span> - </MkA> - <div class="divider"></div> - <div class="about"> - <button v-click-anime class="item _button" @click="openInstanceMenu"> - <img :src="instance.sidebarLogoUrl && !iconOnly ? instance.sidebarLogoUrl : instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" :class="{ wideIcon: instance.sidebarLogoUrl && !iconOnly }" class="_ghost" draggable="false" /> - </button> - </div> - <!--<MisskeyLogo class="misskey"/>--> -</div> -</template> - -<script lang="ts" setup> -import { defineAsyncComponent, computed, watch, ref, useTemplateRef } from 'vue'; -import { openInstanceMenu } from './_common_/common.js'; -// import { host } from '@@/js/config.js'; -import * as os from '@/os.js'; -import { navbarItemDef } from '@/navbar.js'; -import MkButton from '@/components/MkButton.vue'; -// import { StickySidebar } from '@/utility/sticky-sidebar.js'; -// import { mainRouter } from '@/router.js'; -//import MisskeyLogo from '@assets/client/sharkey.svg'; -import { store } from '@/store.js'; -import { instance } from '@/instance.js'; -import { i18n } from '@/i18n.js'; -import { prefer } from '@/preferences.js'; -import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; -import { $i } from '@/i.js'; - -const WINDOW_THRESHOLD = 1400; - -const menu = ref(prefer.s.menu); -const otherNavItemIndicated = computed<boolean>(() => { - for (const def in navbarItemDef) { - if (menu.value.includes(def)) continue; - if (navbarItemDef[def].indicated) return true; - } - return false; -}); -const el = useTemplateRef('el'); -// let accounts = $ref([]); -// let connection = $ref(null); -const iconOnly = ref(false); -const settingsWindowed = ref(false); - -function calcViewState() { - iconOnly.value = (window.innerWidth <= WINDOW_THRESHOLD) || (defaultStore.state.menuDisplay === 'sideIcon'); - settingsWindowed.value = (window.innerWidth > WINDOW_THRESHOLD); -} - -calcViewState(); - -function more(ev: MouseEvent) { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { - src: ev.currentTarget ?? ev.target, - }, { - closed: () => dispose(), - }); -} - -function openAccountMenu(ev: MouseEvent) { - openAccountMenu_({ - withExtraOperation: true, - }, ev); -} - -watch(store.r.menuDisplay, () => { - calcViewState(); -}); - -</script> - -<style lang="scss" scoped> -.npcljfve { - $ui-font-size: 1em; // TODO: どこかに集約したい - $nav-icon-only-width: 78px; // TODO: どこかに集約したい - $avatar-size: 32px; - $avatar-margin: 8px; - position: sticky; - top: 16px; - padding: 0 16px; - box-sizing: border-box; - width: 260px; - - &.iconOnly { - flex: 0 0 $nav-icon-only-width; - width: $nav-icon-only-width !important; - - > .divider { - margin: 8px auto; - width: calc(100% - 32px); - } - - > .post { - > .button { - width: 46px; - height: 46px; - padding: 0; - } - } - - > .item { - padding-left: 0; - width: 100%; - text-align: center; - font-size: $ui-font-size * 1.1; - line-height: 3.7rem; - - > i, - > .avatar { - margin-right: 0; - } - - > i { - left: 10px; - } - - > .text { - display: none; - } - } - } - - > .divider { - margin: 10px 0; - border-top: solid 0.5px var(--MI_THEME-divider); - } - - > .post { - position: sticky; - top: 0; - z-index: 1; - padding: 16px 0; - - > .button { - min-width: 0; - } - } - - > .about { - fill: currentColor; - padding: 8px 0 16px 0; - text-align: center; - - > .item { - display: block; - margin: 0 auto; - - img { - display: block; - width: 32px; - &.wideIcon { - width: 80%; - margin: 0 auto; - } - } - } - } - - > .item { - position: relative; - display: block; - font-size: $ui-font-size; - line-height: 2.6rem; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - width: 100%; - text-align: left; - box-sizing: border-box; - - > i { - width: 32px; - } - - > i, - > .avatar { - margin-right: $avatar-margin; - } - - > .avatar { - width: $avatar-size; - height: $avatar-size; - vertical-align: middle; - } - - > .indicator { - position: absolute; - top: 0; - left: 0; - color: var(--MI_THEME-navIndicator); - font-size: 8px; - - &:has(.itemIndicateValueIcon) { - animation: none; - left: auto; - right: 20px; - } - } - - &:hover { - text-decoration: none; - color: var(--MI_THEME-navHoverFg); - } - - &.active { - color: var(--MI_THEME-navActive); - } - } -} -</style> diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue deleted file mode 100644 index c252b03c82..0000000000 --- a/packages/frontend/src/ui/classic.vue +++ /dev/null @@ -1,325 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<div class="gbhvwtnk" :class="{ wallpaper }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`"> - <XHeaderMenu v-if="showMenuOnTop" v-get-size="(w, h) => globalHeaderHeight = h"/> - - <div class="columns" :class="{ fullView, withGlobalHeader: showMenuOnTop }"> - <div v-if="!showMenuOnTop" class="sidebar"> - <XSidebar/> - </div> - <div v-else-if="!pageMetadata?.needWideArea" ref="widgetsLeft" class="widgets left"> - <XWidgets place="left" :marginTop="'var(--MI-margin)'" @mounted="attachSticky(widgetsLeft)"/> - </div> - - <main class="main" @contextmenu.stop="onContextmenu"> - <div class="content" style="container-type: inline-size;"> - <RouterView/> - </div> - </main> - - <div v-if="isDesktop && !pageMetadata?.needWideArea" ref="widgetsRight" class="widgets right"> - <XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--MI-margin)'" @mounted="attachSticky(widgetsRight)"/> - </div> - </div> - - <Transition :name="prefer.s.animation ? 'tray-back' : ''"> - <div - v-if="widgetsShowing" - class="tray-back _modalBg" - @click="widgetsShowing = false" - @touchstart.passive="widgetsShowing = false" - ></div> - </Transition> - - <Transition :name="prefer.s.animation ? 'tray' : ''"> - <XWidgets v-if="widgetsShowing" class="tray"/> - </Transition> - - <iframe v-if="prefer.s.aiChanMode" ref="live2d" class="ivnzpscs" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe> - - <XCommon/> -</div> -</template> - -<script lang="ts" setup> -import { defineAsyncComponent, onMounted, provide, ref, computed, useTemplateRef } from 'vue'; -import { instanceName } from '@@/js/config.js'; -import { isLink } from '@@/js/is-link.js'; -import XSidebar from './classic.sidebar.vue'; -import XCommon from './_common_/common.vue'; -import type { PageMetadata } from '@/page.js'; -import { StickySidebar } from '@/utility/sticky-sidebar.js'; -import * as os from '@/os.js'; -import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js'; -import { store } from '@/store.js'; -import { i18n } from '@/i18n.js'; -import { miLocalStorage } from '@/local-storage.js'; -import { mainRouter } from '@/router.js'; -import { prefer } from '@/preferences.js'; -import { DI } from '@/di.js'; - -const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue')); -const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); - -const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index'); - -const DESKTOP_THRESHOLD = 1100; - -const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); - -const pageMetadata = ref<null | PageMetadata>(null); -const widgetsShowing = ref(false); -const fullView = ref(false); -const globalHeaderHeight = ref(0); -const wallpaper = miLocalStorage.getItem('wallpaper') != null; -const showMenuOnTop = computed(() => store.s.menuDisplay === 'top'); -const live2d = useTemplateRef('live2d'); -const widgetsLeft = ref<HTMLElement>(); -const widgetsRight = ref<HTMLElement>(); - -provide(DI.router, mainRouter); -provideMetadataReceiver((metadataGetter) => { - const info = metadataGetter(); - pageMetadata.value = info; - if (pageMetadata.value) { - if (isRoot.value && pageMetadata.value.title === instanceName) { - window.document.title = pageMetadata.value.title; - } else { - window.document.title = `${pageMetadata.value.title} | ${instanceName}`; - } - } -}); -provideReactiveMetadata(pageMetadata); -provide('shouldHeaderThin', showMenuOnTop.value); -provide('forceSpacerMin', true); - -function attachSticky(el: HTMLElement) { - const sticky = new StickySidebar(el, 0, store.s.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す - window.addEventListener('scroll', () => { - sticky.calc(window.scrollY); - }, { passive: true }); -} - -function top() { - window.scroll({ top: 0, behavior: 'smooth' }); -} - -function onContextmenu(ev: MouseEvent) { - if (isLink(ev.target)) return; - if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; - if (window.getSelection().toString() !== '') return; - const path = mainRouter.getCurrentFullPath(); - os.contextMenu([{ - type: 'label', - text: path, - }, { - icon: fullView.value ? 'ti ti-minimize' : 'ti ti-maximize', - text: fullView.value ? i18n.ts.quitFullView : i18n.ts.fullView, - action: () => { - fullView.value = !fullView.value; - }, - }, { - icon: 'ti ti-window-maximize', - text: i18n.ts.openInWindow, - action: () => { - os.pageWindow(path); - }, - }], ev); -} - -function onAiClick(ev) { - //if (this.live2d) this.live2d.click(ev); -} - -if (window.innerWidth < 1024) { - const currentUI = miLocalStorage.getItem('ui'); - miLocalStorage.setItem('ui_temp', currentUI ?? 'default'); - miLocalStorage.setItem('ui', 'default'); - window.location.reload(); -} - -window.document.documentElement.style.overflowY = 'scroll'; - -onMounted(() => { - window.addEventListener('resize', () => { - isDesktop.value = (window.innerWidth >= DESKTOP_THRESHOLD); - }, { passive: true }); - - if (prefer.s.aiChanMode) { - const iframeRect = live2d.value.getBoundingClientRect(); - window.addEventListener('mousemove', ev => { - live2d.value.contentWindow.postMessage({ - type: 'moveCursor', - body: { - x: ev.clientX - iframeRect.left, - y: ev.clientY - iframeRect.top, - }, - }, '*'); - }, { passive: true }); - window.addEventListener('touchmove', ev => { - live2d.value.contentWindow.postMessage({ - type: 'moveCursor', - body: { - x: ev.touches[0].clientX - iframeRect.left, - y: ev.touches[0].clientY - iframeRect.top, - }, - }, '*'); - }, { passive: true }); - } -}); -</script> - -<style lang="scss" scoped> -.tray-enter-active, -.tray-leave-active { - opacity: 1; - transform: translateX(0); - transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.tray-enter-from, -.tray-leave-active { - opacity: 0; - transform: translateX(240px); -} - -.tray-back-enter-active, -.tray-back-leave-active { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.tray-back-enter-from, -.tray-back-leave-active { - opacity: 0; -} - -.gbhvwtnk { - $ui-font-size: 1em; - $widgets-hide-threshold: 1200px; - - min-height: 100dvh; - box-sizing: border-box; - - &.wallpaper { - background: var(--MI_THEME-wallpaperOverlay); - //backdrop-filter: var(--MI-blur, blur(4px)); - } - - > .columns { - display: flex; - justify-content: center; - max-width: 100%; - //margin: 32px 0; - - &.fullView { - margin: 0; - - > .sidebar { - display: none; - } - - > .widgets { - display: none; - } - - > .main { - margin: 0; - border-radius: 0; - box-shadow: none; - width: 100%; - } - } - - > .main { - min-width: 0; - width: 750px; - margin: 0 16px 0 0; - border-left: solid 1px var(--MI_THEME-divider); - border-right: solid 1px var(--MI_THEME-divider); - border-radius: 0; - overflow: clip; - --MI-margin: 12px; - } - - > .widgets { - position: sticky; - top: 0; - height: 100%; - width: 300px; - padding-top: 16px; - box-sizing: border-box; - overflow: auto; - - @media (max-width: $widgets-hide-threshold) { - display: none; - } - - &.left { - margin-right: 16px; - } - } - - > .sidebar { - margin-top: 16px; - } - - &.withGlobalHeader { - > .main { - margin-top: 0; - border: solid 1px var(--MI_THEME-divider); - border-radius: var(--MI-radius); - --MI-stickyTop: var(--globalHeaderHeight); - } - - > .widgets { - --MI-stickyTop: var(--globalHeaderHeight); - margin-top: 0; - } - } - - @media (max-width: 850px) { - margin: 0; - - > .sidebar { - border-right: solid 0.5px var(--MI_THEME-divider); - } - - > .main { - margin: 0; - border-radius: 0; - box-shadow: none; - width: 100%; - } - } - } - - > .tray-back { - z-index: 1001; - } - - > .tray { - position: fixed; - top: 0; - right: 0; - z-index: 1001; - height: 100dvh; - padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)); - box-sizing: border-box; - overflow: auto; - background: var(--MI_THEME-bg); - } - - > .ivnzpscs { - position: fixed; - bottom: 0; - right: 0; - width: 300px; - height: 600px; - border: none; - pointer-events: none; - } -} -</style> diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 18094b4444..a59399a10c 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -228,28 +228,6 @@ async function deleteProfile() { </script> -<style> -html, -body { - width: 100%; - height: 100%; - overflow: clip; - position: fixed; - top: 0; - left: 0; - overscroll-behavior: none; -} - -#sharkey_app { - width: 100%; - height: 100%; - overflow: clip; - position: absolute; - top: 0; - left: 0; -} -</style> - <style lang="scss" module> .transition_menuDrawerBg_enterActive, .transition_menuDrawerBg_leaveActive { diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index febf8ca6f8..5bad337461 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -377,7 +377,7 @@ function onDrop(ev) { font-size: 0.9em; color: var(--MI_THEME-panelHeaderFg); background: var(--MI_THEME-panelHeaderBg); - box-shadow: 0 1px 0 0 var(--MI_THEME-panelHeaderDivider); + box-shadow: 0 0.5px 0 0 var(--MI_THEME-panelHeaderDivider); cursor: pointer; user-select: none; } diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index cc3836c646..86bd7cf055 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -13,10 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only <XAnnouncements v-if="$i"/> <XStatusBars :class="$style.statusbars"/> </div> - <div :class="$style.content"> - <StackingRouterView v-if="prefer.s['experimental.stackingRouterView']"/> - <RouterView v-else/> - </div> + <StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :class="$style.content"/> + <RouterView v-else :class="$style.content"/> <div v-if="isMobile" ref="navFooter" :class="$style.nav"> <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button> <button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> @@ -97,7 +95,6 @@ import { defineAsyncComponent, provide, onMounted, computed, ref, watch, useTemp import { instanceName } from '@@/js/config.js'; import { isLink } from '@@/js/is-link.js'; import XCommon from './_common_/common.vue'; -import type { Ref } from 'vue'; import type { PageMetadata } from '@/page.js'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import * as os from '@/os.js'; @@ -211,28 +208,6 @@ watch(navFooter, () => { }); </script> -<style> -html, -body { - width: 100%; - height: 100%; - overflow: clip; - position: fixed; - top: 0; - left: 0; - overscroll-behavior: none; -} - -#sharkey_app { - width: 100%; - height: 100%; - overflow: clip; - position: absolute; - top: 0; - left: 0; -} -</style> - <style lang="scss" module> $ui-font-size: 1em; // TODO: どこかに集約したい $widgets-hide-threshold: 1090px; diff --git a/packages/frontend/src/utility/achievements.ts b/packages/frontend/src/utility/achievements.ts index f6ab587ae1..06b445ab0d 100644 --- a/packages/frontend/src/utility/achievements.ts +++ b/packages/frontend/src/utility/achievements.ts @@ -497,7 +497,7 @@ export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) { if (claimedAchievements.includes(type)) return; claimingQueue.add(type); claimedAchievements.push(type); - await new Promise(resolve => setTimeout(resolve, (claimingQueue.size - 1) * 500)); + await new Promise(resolve => window.setTimeout(resolve, (claimingQueue.size - 1) * 500)); window.setTimeout(() => { claimingQueue.delete(type); }, 500); diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts index c939c93425..df621beb7d 100644 --- a/packages/frontend/src/utility/autogen/settings-search-index.ts +++ b/packages/frontend/src/utility/autogen/settings-search-index.ts @@ -290,138 +290,128 @@ export const searchIndexes: SearchIndexItem[] = [ }, { id: 'lfI3yMX9g', - label: i18n.ts.useBlurEffect, - keywords: ['blur'], - }, - { - id: '31Y4IcGEf', - label: i18n.ts.useBlurEffectForModal, - keywords: ['blur', 'modal'], - }, - { - id: '78q2asrLS', label: i18n.ts.showAvatarDecorations, keywords: ['avatar', 'icon', 'decoration', 'show'], }, { - id: 'zydOfGYip', + id: '31Y4IcGEf', label: i18n.ts.alwaysConfirmFollow, keywords: ['follow', 'confirm', 'always'], }, { - id: 'wqpOC22Zm', + id: '78q2asrLS', label: i18n.ts.highlightSensitiveMedia, keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'], }, { - id: 'c98gbF9c6', + id: 'zydOfGYip', label: i18n.ts.confirmWhenRevealingSensitiveMedia, keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'], }, { - id: '4LxdiOMNh', + id: 'wqpOC22Zm', label: i18n.ts.enableAdvancedMfm, keywords: ['mfm', 'enable', 'show', 'advanced'], }, { - id: '9gTCaLkIf', + id: 'c98gbF9c6', label: i18n.ts.enableInfiniteScroll, keywords: ['auto', 'load', 'auto', 'more', 'scroll'], }, { - id: 'jmJT0twuJ', + id: '6ANRSOaNg', label: i18n.ts.emojiStyle, keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'], }, - { - id: 'igFN7RIUa', - label: i18n.ts.pinnedList, - keywords: ['pinned', 'list'], - }, ], label: i18n.ts.general, keywords: ['general'], }, { - id: 'ufc2X9voy', + id: '5G6O6qdis', children: [ { - id: 'd2H4E5ys6', + id: 'khT3n6byY', label: i18n.ts.showFixedPostForm, keywords: ['post', 'form', 'timeline'], }, { - id: '1LHOhDKGW', + id: 'q5ElfNSou', label: i18n.ts.showFixedPostFormInChannel, keywords: ['post', 'form', 'timeline', 'channel'], }, { - id: 'DSzwvTp7i', + id: '3GcWIaZf8', label: i18n.ts.collapseRenotes, keywords: ['renote', i18n.ts.collapseRenotesDescription], }, { - id: 'jb3HUeyrx', + id: 'd2H4E5ys6', label: i18n.ts.showGapBetweenNotesInTimeline, keywords: ['note', 'timeline', 'gap'], }, { - id: '2LNjwv1cr', + id: '1LHOhDKGW', label: i18n.ts.disableStreamingTimeline, keywords: ['disable', 'streaming', 'timeline'], }, { - id: '7W6g8Dcqz', + id: 'DSzwvTp7i', + label: i18n.ts.pinnedList, + keywords: ['pinned', 'list'], + }, + { + id: 'ykifk3NHS', label: i18n.ts.showNoteActionsOnlyHover, keywords: ['hover', 'show', 'footer', 'action'], }, { - id: 'uAOoH3LFF', + id: 'tLGyaQagB', label: i18n.ts.showClipButtonInNoteFooter, keywords: ['footer', 'action', 'clip', 'show'], }, { - id: 'eCiyZLC8n', + id: '7W6g8Dcqz', label: i18n.ts.showReactionsCount, keywords: ['reaction', 'count', 'show'], }, { - id: '68u9uRmFP', + id: 'uAOoH3LFF', label: i18n.ts.confirmOnReact, keywords: ['reaction', 'confirm'], }, { - id: 'rHWm4sXIe', + id: 'eCiyZLC8n', label: i18n.ts.loadRawImages, keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'], }, { - id: '9L2XGJw7e', + id: '68u9uRmFP', label: i18n.ts.useReactionPickerForContextMenu, keywords: ['reaction', 'picker', 'contextmenu', 'open'], }, { - id: 'uIMCIK7kG', + id: 'yxehrHZ6x', label: i18n.ts.reactionsDisplaySize, keywords: ['reaction', 'size', 'scale', 'display'], }, { - id: 'uMckjO9bz', + id: 'gi8ILaE2Z', label: i18n.ts.limitWidthOfReaction, keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'], }, { - id: 'yeghU4qiH', + id: 'cEQJZ7DQG', label: i18n.ts.mediaListWithOneImageAppearance, keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'], }, { - id: 'yYSOPoAKE', + id: 'haX4QVulD', label: i18n.ts.instanceTicker, keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'], }, { - id: 'iOHiIu32L', + id: 'pneYnQekL', label: i18n.ts.displayOfSensitiveMedia, keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'], }, @@ -430,25 +420,25 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['timeline', 'note'], }, { - id: 'eROFRMtXv', + id: 'eJ2jme16W', children: [ { - id: 'BaQfrVO82', + id: 'ErMQr6LQk', label: i18n.ts.keepCw, keywords: ['remember', 'keep', 'note', 'cw'], }, { - id: 'vFerPo2he', + id: 'zrJicawH9', label: i18n.ts.rememberNoteVisibility, keywords: ['remember', 'keep', 'note', 'visibility'], }, { - id: 'dcAC0yJcH', + id: 'BaQfrVO82', label: i18n.ts.enableQuickAddMfmFunction, keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'], }, { - id: 'bECeWZVMb', + id: 'C2WYcVM1d', label: i18n.ts.defaultNoteVisibility, keywords: ['default', 'note', 'visibility'], }, @@ -457,20 +447,20 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['post', 'form'], }, { - id: 'tsSP93Cc6', + id: 'sQXSA6gik', children: [ { - id: 'dtw8FepYL', + id: 'rICn8stqk', label: i18n.ts.useGroupedNotifications, keywords: ['group'], }, { - id: 'eb0yCYJTn', + id: 'xFmAg2tDe', label: i18n.ts.position, keywords: ['position'], }, { - id: '1Spt4Gpr5', + id: 'Ek4Cw3VPq', label: i18n.ts.stackAxis, keywords: ['stack', 'axis', 'direction'], }, @@ -479,20 +469,15 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['notification'], }, { - id: 'SYmWxGOF', - label: i18n.ts.dataSaver, - keywords: ['datasaver'], - }, - { - id: 'vPQPvmntL', + id: 'gDVCqZfxm', children: [ { - id: 'zZxyXHk3A', + id: 'ei8Ix3s4S', label: i18n.ts._settings._chat.showSenderName, keywords: ['show', 'sender', 'name'], }, { - id: 'omEy5Q3Ev', + id: '2E7vdIUQd', label: i18n.ts._settings._chat.sendOnEnter, keywords: ['send', 'enter', 'newline'], }, @@ -501,50 +486,139 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['chat', 'messaging'], }, { - id: '5fy7VEy6i', + id: '96LnS1sxB', + children: [ + { + id: 'vPQPvmntL', + label: i18n.ts.reduceUiAnimation, + keywords: ['animation', 'motion', 'reduce'], + }, + { + id: 'wfJ91vwzq', + label: i18n.ts.disableShowingAnimatedImages, + keywords: ['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif'], + }, + { + id: '42b1L4xdq', + label: i18n.ts.enableAnimatedMfm, + keywords: ['mfm', 'enable', 'show', 'animated'], + }, + { + id: 'dLkRNHn3k', + label: i18n.ts.enableHorizontalSwipe, + keywords: ['swipe', 'horizontal', 'tab'], + }, + { + id: 'BvooTWFW5', + label: i18n.ts.keepScreenOn, + keywords: ['keep', 'screen', 'display', 'on'], + }, + { + id: 'yzbghkAq0', + label: i18n.ts.useNativeUIForVideoAudioPlayer, + keywords: ['native', 'system', 'video', 'audio', 'player', 'media'], + }, + { + id: 'aSbKFHbOy', + label: i18n.ts._settings.makeEveryTextElementsSelectable, + keywords: ['text', 'selectable'], + }, + { + id: 'bTcAsPvNz', + label: i18n.ts.menuStyle, + keywords: ['menu', 'style', 'popup', 'drawer'], + }, + { + id: 'lSVBaLnyW', + label: i18n.ts._contextMenu.title, + keywords: ['contextmenu', 'system', 'native'], + }, + { + id: 'pec0uMPq5', + label: i18n.ts.fontSize, + keywords: ['font', 'size'], + }, + { + id: 'Eh7vTluDO', + label: i18n.ts.useSystemFont, + keywords: ['font', 'system', 'native'], + }, + ], + label: i18n.ts.accessibility, + keywords: ['accessibility', i18n.ts._settings.accessibilityBanner], + }, + { + id: 'vTRSKf1JA', + children: [ + { + id: '2VjlA02wB', + label: i18n.ts.turnOffToImprovePerformance, + keywords: ['blur'], + }, + { + id: 'f6J0lmg1g', + label: i18n.ts.turnOffToImprovePerformance, + keywords: ['blur', 'modal'], + }, + { + id: 'hQqXhfNg8', + label: i18n.ts.turnOffToImprovePerformance, + keywords: ['sticky'], + }, + ], + label: i18n.ts.performance, + keywords: ['performance'], + }, + { + id: 'utM8dEobb', + label: i18n.ts.dataSaver, + keywords: ['datasaver'], + }, + { + id: 'gOUvwkE9t', children: [ { - id: 'EosiWZvak', + id: 'iUMUvFURf', label: i18n.ts.squareAvatars, keywords: ['avatar', 'icon', 'square'], }, { - id: 'qY5xTzl35', + id: 'ceyPO9Ywi', label: i18n.ts.seasonalScreenEffect, keywords: ['effect', 'show'], }, { - id: '2VSnj81vC', + id: 'ztwIlsXhP', label: i18n.ts.openImageInNewTab, keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'], }, { - id: 'hdQa7W2H1', + id: 'vLSsQbZEo', label: i18n.ts.withRepliesByDefaultForNewlyFollowed, keywords: ['follow', 'replies'], }, { - id: 'nnj4DkjhP', + id: 'hQt85bBIX', label: i18n.ts.whenServerDisconnected, keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'], }, { - id: 'Eh7vTluDO', + id: 'C9SyK2m0', label: i18n.ts.numberOfPageCache, keywords: ['cache', 'page'], }, { - id: 'vTRSKf1JA', + id: '2U0iVUtfW', label: i18n.ts.forceShowAds, keywords: ['ad', 'show'], }, { - id: 'dwhQfcLGt', + id: '1rA7ADEXY', label: i18n.ts.hemisphere, keywords: [], }, { - id: 'Ar1lj7f7U', + id: 'vRayx89Rt', label: i18n.ts.additionalEmojiDictionary, keywords: ['emoji', 'dictionary', 'additional', 'extra'], }, @@ -759,21 +833,16 @@ export const searchIndexes: SearchIndexItem[] = [ }, { id: 'goQdtf3dD', - label: i18n.ts.keepOriginalUploading, - keywords: ['keep', 'original', 'raw', 'upload', i18n.ts.keepOriginalUploadingDescription], - }, - { - id: '83xRo0XJl', label: i18n.ts.keepOriginalFilename, keywords: ['keep', 'original', 'filename', i18n.ts.keepOriginalFilenameDescription], }, { - id: 'wf77yRQQq', + id: '83xRo0XJl', label: i18n.ts.alwaysMarkSensitive, keywords: ['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file'], }, { - id: '3pxwNB8e4', + id: 'BrBqZL35E', label: i18n.ts.enableAutoSensitive, keywords: ['auto', 'nsfw', 'sensitive', 'media', 'file', i18n.ts.enableAutoSensitiveDescription], }, @@ -899,70 +968,6 @@ export const searchIndexes: SearchIndexItem[] = [ path: '/settings/account-data', icon: 'ti ti-package', }, - { - id: 'f08Mi1Uwn', - children: [ - { - id: 'C5dRH2Ypy', - label: i18n.ts.reduceUiAnimation, - keywords: ['animation', 'motion', 'reduce'], - }, - { - id: '5mZxz2cru', - label: i18n.ts.disableShowingAnimatedImages, - keywords: ['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif'], - }, - { - id: 'c0Iy5hL5o', - label: i18n.ts.enableAnimatedMfm, - keywords: ['mfm', 'enable', 'show', 'animated'], - }, - { - id: '4HYFjs2Nv', - label: i18n.ts.enableHorizontalSwipe, - keywords: ['swipe', 'horizontal', 'tab'], - }, - { - id: 'kYVJ3SVNq', - label: i18n.ts.keepScreenOn, - keywords: ['keep', 'screen', 'display', 'on'], - }, - { - id: 'w4Bv0meAt', - label: i18n.ts.useNativeUIForVideoAudioPlayer, - keywords: ['native', 'system', 'video', 'audio', 'player', 'media'], - }, - { - id: 'b1GYEEJeh', - label: i18n.ts._settings.makeEveryTextElementsSelectable, - keywords: ['text', 'selectable'], - }, - { - id: 'vVLxwINTJ', - label: i18n.ts.menuStyle, - keywords: ['menu', 'style', 'popup', 'drawer'], - }, - { - id: '14cMhMLHL', - label: i18n.ts._contextMenu.title, - keywords: ['contextmenu', 'system', 'native'], - }, - { - id: 'oSo4LXMX9', - label: i18n.ts.fontSize, - keywords: ['font', 'size'], - }, - { - id: '7LQSAThST', - label: i18n.ts.useSystemFont, - keywords: ['font', 'system', 'native'], - }, - ], - label: i18n.ts.accessibility, - keywords: ['accessibility', i18n.ts._settings.accessibilityBanner], - path: '/settings/accessibility', - icon: 'ti ti-accessible', - }, ] as const; export type SearchIndex = typeof searchIndexes; diff --git a/packages/frontend/src/utility/confetti.ts b/packages/frontend/src/utility/confetti.ts index 8e53a6ceeb..c19149875f 100644 --- a/packages/frontend/src/utility/confetti.ts +++ b/packages/frontend/src/utility/confetti.ts @@ -15,11 +15,11 @@ export function confetti(options: { duration?: number; } = {}) { return Math.random() * (max - min) + min; } - const interval = setInterval(() => { + const interval = window.setInterval(() => { const timeLeft = animationEnd - Date.now(); if (timeLeft <= 0) { - return clearInterval(interval); + return window.clearInterval(interval); } const particleCount = 50 * (timeLeft / duration); diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts index 9693197ab4..8eb2ef3ec9 100644 --- a/packages/frontend/src/utility/get-user-menu.ts +++ b/packages/frontend/src/utility/get-user-menu.ts @@ -361,12 +361,18 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; os.post({ specified: user, initialText: `${canonical} ` }); }, - }, { - type: 'link', - icon: 'ti ti-messages', - text: i18n.ts._chat.chatWithThisUser, - to: `/chat/user/${user.id}`, - }, { type: 'divider' }, { + }); + + if ($i.policies.canChat && user.canChat && user.host == null) { + menuItems.push({ + type: 'link', + icon: 'ti ti-messages', + text: i18n.ts._chat.chatWithThisUser, + to: `/chat/user/${user.id}`, + }); + } + + menuItems.push({ type: 'divider' }, { icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, action: toggleMute, diff --git a/packages/frontend/src/utility/hotkey.ts b/packages/frontend/src/utility/hotkey.ts index 852abb6140..c48340c49f 100644 --- a/packages/frontend/src/utility/hotkey.ts +++ b/packages/frontend/src/utility/hotkey.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import { getHTMLElementOrNull } from "@/utility/get-dom-node-or-null.js"; +import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js'; //#region types export type Keymap = Record<string, CallbackFunction | CallbackObject>; @@ -136,7 +136,7 @@ let lastHotKeyStoreTimer: number | null = null; const storePattern = (ev: KeyboardEvent, callback: CallbackFunction) => { if (lastHotKeyStoreTimer != null) { - clearTimeout(lastHotKeyStoreTimer); + window.clearTimeout(lastHotKeyStoreTimer); } latestHotkey = { diff --git a/packages/frontend/src/utility/idle-render.ts b/packages/frontend/src/utility/idle-render.ts index 6adfedcb9f..32daa1df02 100644 --- a/packages/frontend/src/utility/idle-render.ts +++ b/packages/frontend/src/utility/idle-render.ts @@ -5,7 +5,7 @@ const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.requestIdleCallback ?? ((callback) => { const start = performance.now(); - const timeoutId = setTimeout(() => { + const timeoutId = window.setTimeout(() => { callback({ didTimeout: false, // polyfill でタイムアウト発火することはない timeRemaining() { @@ -17,7 +17,7 @@ const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.re return timeoutId; }); const cancelIdleCallback: typeof globalThis.cancelIdleCallback = globalThis.cancelIdleCallback ?? ((timeoutId) => { - clearTimeout(timeoutId); + window.clearTimeout(timeoutId); }); class IdlingRenderScheduler { diff --git a/packages/frontend/src/utility/select-file.ts b/packages/frontend/src/utility/select-file.ts index b9b3687483..731ef58302 100644 --- a/packages/frontend/src/utility/select-file.ts +++ b/packages/frontend/src/utility/select-file.ts @@ -21,7 +21,7 @@ export function chooseFileFromPc( }, ): Promise<Misskey.entities.DriveFile[]> { const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder; - const keepOriginal = options?.keepOriginal ?? prefer.s.keepOriginalUploading; + const keepOriginal = options?.keepOriginal ?? false; const nameConverter = options?.nameConverter ?? (() => undefined); return new Promise((res, rej) => { @@ -96,19 +96,17 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> { function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> { return new Promise((res, rej) => { - const keepOriginal = ref(prefer.s.keepOriginalUploading); - os.popupMenu([label ? { text: label, type: 'label', } : undefined, { - type: 'switch', - text: i18n.ts.keepOriginalUploading, - ref: keepOriginal, + text: i18n.ts.upload + ' (' + i18n.ts.compress + ')', + icon: 'ti ti-upload', + action: () => chooseFileFromPc(multiple, { keepOriginal: false }).then(files => res(files)), }, { text: i18n.ts.upload, icon: 'ti ti-upload', - action: () => chooseFileFromPc(multiple, { keepOriginal: keepOriginal.value }).then(files => res(files)), + action: () => chooseFileFromPc(multiple, { keepOriginal: true }).then(files => res(files)), }, { text: i18n.ts.fromDrive, icon: 'ti ti-cloud', diff --git a/packages/frontend/src/utility/sound.ts b/packages/frontend/src/utility/sound.ts index f217bdfcd5..d3f82a37f2 100644 --- a/packages/frontend/src/utility/sound.ts +++ b/packages/frontend/src/utility/sound.ts @@ -158,7 +158,7 @@ export async function playMisskeySfxFile(soundStore: SoundStore): Promise<boolea canPlay = false; return await playMisskeySfxFileInternal(soundStore).finally(() => { // ごく短時間に音が重複しないように - setTimeout(() => { + window.setTimeout(() => { canPlay = true; }, 25); }); @@ -230,10 +230,10 @@ export async function getSoundDuration(file: string): Promise<number> { const audioEl = window.document.createElement('audio'); audioEl.src = file; return new Promise((resolve) => { - const si = setInterval(() => { + const si = window.setInterval(() => { if (audioEl.readyState > 0) { resolve(audioEl.duration * 1000); - clearInterval(si); + window.clearInterval(si); audioEl.remove(); } }, 100); diff --git a/packages/frontend/src/utility/test-utils.ts b/packages/frontend/src/utility/test-utils.ts index 52bb2d94e0..54742c1a9e 100644 --- a/packages/frontend/src/utility/test-utils.ts +++ b/packages/frontend/src/utility/test-utils.ts @@ -5,5 +5,5 @@ export async function tick(): Promise<void> { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never); + await new Promise((globalThis.requestIdleCallback ?? window.setTimeout) as never); } diff --git a/packages/frontend/src/utility/upload.ts b/packages/frontend/src/utility/upload.ts index e13d793ffb..b43fea8e15 100644 --- a/packages/frontend/src/utility/upload.ts +++ b/packages/frontend/src/utility/upload.ts @@ -34,7 +34,7 @@ export function uploadFile( file: File, folder?: string | Misskey.entities.DriveFolder | null, name?: string, - keepOriginal: boolean = prefer.s.keepOriginalUploading, + keepOriginal = false, ): Promise<Misskey.entities.DriveFile> { if ($i == null) throw new Error('Not logged in'); diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index bfcacaad46..c150e1b995 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2025.3.2-beta.13", + "version": "2025.3.2-beta.18", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index f96cb718b4..972e12d8bf 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4057,6 +4057,7 @@ export type components = { followersVisibility: 'public' | 'followers' | 'private'; /** @enum {string} */ chatScope: 'everyone' | 'following' | 'followers' | 'mutual' | 'none'; + canChat: boolean; roles: components['schemas']['RoleLite'][]; followedMessage?: string | null; memo: string | null; |