Merge remote-tracking branch 'origin/develop' into no-cache-sensitive

This commit is contained in:
Kagami Sascha Rosylight 2023-07-12 21:27:49 +02:00
commit 24a0063af0
60 changed files with 455 additions and 287 deletions

View File

@ -27,6 +27,10 @@
- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように - フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
- 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように - 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
- プロフィールURLをコピーできるボタンを追加 #11190 - プロフィールURLをコピーできるボタンを追加 #11190
- ユーザーのContextMenuに「アンテナに追加」ボタンを追加
- フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように
- 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように
- オフライン時の画面にリロードボタンを追加
- Fix: サーバーメトリクスが90度傾いている - Fix: サーバーメトリクスが90度傾いている
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正 - Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正 - Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正

1
locales/index.d.ts vendored
View File

@ -52,6 +52,7 @@ export interface Locale {
"deleteAndEdit": string; "deleteAndEdit": string;
"deleteAndEditConfirm": string; "deleteAndEditConfirm": string;
"addToList": string; "addToList": string;
"addToAntenna": string;
"sendMessage": string; "sendMessage": string;
"copyRSS": string; "copyRSS": string;
"copyUsername": string; "copyUsername": string;

View File

@ -49,6 +49,7 @@ delete: "削除"
deleteAndEdit: "削除して編集" deleteAndEdit: "削除して編集"
deleteAndEditConfirm: "このートを削除してもう一度編集しますかこのートへのリアクション、Renote、返信も全て削除されます。" deleteAndEditConfirm: "このートを削除してもう一度編集しますかこのートへのリアクション、Renote、返信も全て削除されます。"
addToList: "リストに追加" addToList: "リストに追加"
addToAntenna: "アンテナに追加"
sendMessage: "メッセージを送信" sendMessage: "メッセージを送信"
copyRSS: "RSSをコピー" copyRSS: "RSSをコピー"
copyUsername: "ユーザー名をコピー" copyUsername: "ユーザー名をコピー"

View File

@ -103,7 +103,7 @@ export class FetchInstanceMetadataService {
if (name) updates.name = name; if (name) updates.name = name;
if (description) updates.description = description; if (description) updates.description = description;
if (icon || favicon) updates.iconUrl = icon ?? favicon; if (icon || favicon) updates.iconUrl = (icon && !icon.includes('data:image/png;base64')) ? icon : favicon;
if (favicon) updates.faviconUrl = favicon; if (favicon) updates.faviconUrl = favicon;
if (themeColor) updates.themeColor = themeColor; if (themeColor) updates.themeColor = themeColor;

View File

@ -570,12 +570,14 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.reply) { if (data.reply) {
// 通知 // 通知
if (data.reply.userHost === null) { if (data.reply.userHost === null) {
const threadMuted = await this.noteThreadMutingsRepository.findOneBy({ const isThreadMuted = await this.noteThreadMutingsRepository.exist({
userId: data.reply.userId, where: {
threadId: data.reply.threadId ?? data.reply.id, userId: data.reply.userId,
threadId: data.reply.threadId ?? data.reply.id,
}
}); });
if (!threadMuted) { if (!isThreadMuted) {
nm.push(data.reply.userId, 'reply'); nm.push(data.reply.userId, 'reply');
this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
@ -712,12 +714,14 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis @bindThis
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) { private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) {
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) { for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
const threadMuted = await this.noteThreadMutingsRepository.findOneBy({ const isThreadMuted = await this.noteThreadMutingsRepository.exist({
userId: u.id, where: {
threadId: note.threadId ?? note.id, userId: u.id,
threadId: note.threadId ?? note.id,
},
}); });
if (threadMuted) { if (isThreadMuted) {
continue; continue;
} }

View File

@ -43,11 +43,13 @@ export class NoteReadService implements OnApplicationShutdown {
//#endregion //#endregion
// スレッドミュート // スレッドミュート
const threadMute = await this.noteThreadMutingsRepository.findOneBy({ const isThreadMuted = await this.noteThreadMutingsRepository.exist({
userId: userId, where: {
threadId: note.threadId ?? note.id, userId: userId,
threadId: note.threadId ?? note.id,
},
}); });
if (threadMute) return; if (isThreadMuted) return;
const unread = { const unread = {
id: this.idService.genId(), id: this.idService.genId(),
@ -62,9 +64,9 @@ export class NoteReadService implements OnApplicationShutdown {
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id }); const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } });
if (exist == null) return; if (!exist) return;
if (params.isMentioned) { if (params.isMentioned) {
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id); this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);

View File

@ -71,12 +71,12 @@ export class SignupService {
const secret = generateUserToken(); const secret = generateUserToken();
// Check username duplication // Check username duplication
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new Error('DUPLICATED_USERNAME'); throw new Error('DUPLICATED_USERNAME');
} }
// Check deleted username duplication // Check deleted username duplication
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
throw new Error('USED_USERNAME'); throw new Error('USED_USERNAME');
} }

View File

@ -122,22 +122,26 @@ export class UserFollowingService implements OnModuleInit {
let autoAccept = false; let autoAccept = false;
// 鍵アカウントであっても、既にフォローされていた場合はスルー // 鍵アカウントであっても、既にフォローされていた場合はスルー
const following = await this.followingsRepository.findOneBy({ const isFollowing = await this.followingsRepository.exist({
followerId: follower.id, where: {
followeeId: followee.id, followerId: follower.id,
followeeId: followee.id,
},
}); });
if (following) { if (isFollowing) {
autoAccept = true; autoAccept = true;
} }
// フォローしているユーザーは自動承認オプション // フォローしているユーザーは自動承認オプション
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
const followed = await this.followingsRepository.findOneBy({ const isFollowed = await this.followingsRepository.exist({
followerId: followee.id, where: {
followeeId: follower.id, followerId: followee.id,
followeeId: follower.id,
},
}); });
if (followed) autoAccept = true; if (isFollowed) autoAccept = true;
} }
// Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account. // Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account.
@ -206,12 +210,14 @@ export class UserFollowingService implements OnModuleInit {
this.cacheService.userFollowingsCache.refresh(follower.id); this.cacheService.userFollowingsCache.refresh(follower.id);
const req = await this.followRequestsRepository.findOneBy({ const requestExist = await this.followRequestsRepository.exist({
followeeId: followee.id, where: {
followerId: follower.id, followeeId: followee.id,
followerId: follower.id,
},
}); });
if (req) { if (requestExist) {
await this.followRequestsRepository.delete({ await this.followRequestsRepository.delete({
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
@ -505,12 +511,14 @@ export class UserFollowingService implements OnModuleInit {
} }
} }
const request = await this.followRequestsRepository.findOneBy({ const requestExist = await this.followRequestsRepository.exist({
followeeId: followee.id, where: {
followerId: follower.id, followeeId: followee.id,
followerId: follower.id,
},
}); });
if (request == null) { if (!requestExist) {
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
} }

View File

@ -618,12 +618,14 @@ export class ApInboxService {
return 'skip: follower not found'; return 'skip: follower not found';
} }
const following = await this.followingsRepository.findOneBy({ const isFollowing = await this.followingsRepository.exist({
followerId: follower.id, where: {
followeeId: actor.id, followerId: follower.id,
followeeId: actor.id,
},
}); });
if (following) { if (isFollowing) {
await this.userFollowingService.unfollow(follower, actor); await this.userFollowingService.unfollow(follower, actor);
return 'ok: unfollowed'; return 'ok: unfollowed';
} }
@ -673,22 +675,26 @@ export class ApInboxService {
return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません'; return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
} }
const req = await this.followRequestsRepository.findOneBy({ const requestExist = await this.followRequestsRepository.exist({
followerId: actor.id, where: {
followeeId: followee.id, followerId: actor.id,
followeeId: followee.id,
},
}); });
const following = await this.followingsRepository.findOneBy({ const isFollowing = await this.followingsRepository.exist({
followerId: actor.id, where: {
followeeId: followee.id, followerId: actor.id,
followeeId: followee.id,
},
}); });
if (req) { if (requestExist) {
await this.userFollowingService.cancelFollowRequest(followee, actor); await this.userFollowingService.cancelFollowRequest(followee, actor);
return 'ok: follow request canceled'; return 'ok: follow request canceled';
} }
if (following) { if (isFollowing) {
await this.userFollowingService.unfollow(actor, followee); await this.userFollowingService.unfollow(actor, followee);
return 'ok: unfollowed'; return 'ok: unfollowed';
} }

View File

@ -323,9 +323,9 @@ export class ApRendererService {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId }); inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) { if (inReplyToNote != null) {
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId }); const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
if (inReplyToUser != null) { if (inReplyToUserExist) {
if (inReplyToNote.uri) { if (inReplyToNote.uri) {
inReplyTo = inReplyToNote.uri; inReplyTo = inReplyToNote.uri;
} else { } else {

View File

@ -47,17 +47,26 @@ export class ChannelEntityService {
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null; const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
const hasUnreadNote = meId ? (await this.noteUnreadsRepository.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined; const hasUnreadNote = meId ? await this.noteUnreadsRepository.exist({
where: {
noteChannelId: channel.id,
userId: meId
},
}) : undefined;
const following = meId ? await this.channelFollowingsRepository.findOneBy({ const isFollowing = meId ? await this.channelFollowingsRepository.exist({
followerId: meId, where: {
followeeId: channel.id, followerId: meId,
}) : null; followeeId: channel.id,
},
}) : false;
const favorite = meId ? await this.channelFavoritesRepository.findOneBy({ const isFavorited = meId ? await this.channelFavoritesRepository.exist({
userId: meId, where: {
channelId: channel.id, userId: meId,
}) : null; channelId: channel.id,
},
}) : false;
const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({ const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({
where: { where: {
@ -80,8 +89,8 @@ export class ChannelEntityService {
notesCount: channel.notesCount, notesCount: channel.notesCount,
...(me ? { ...(me ? {
isFollowing: following != null, isFollowing,
isFavorited: favorite != null, isFavorited,
hasUnreadNote, hasUnreadNote,
} : {}), } : {}),

View File

@ -39,7 +39,7 @@ export class ClipEntityService {
description: clip.description, description: clip.description,
isPublic: clip.isPublic, isPublic: clip.isPublic,
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }), favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
isFavorited: meId ? await this.clipFavoritesRepository.findOneBy({ clipId: clip.id, userId: meId }).then(x => x != null) : undefined, isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined,
}); });
} }

View File

@ -40,7 +40,7 @@ export class FlashEntityService {
summary: flash.summary, summary: flash.summary,
script: flash.script, script: flash.script,
likedCount: flash.likedCount, likedCount: flash.likedCount,
isLiked: meId ? await this.flashLikesRepository.findOneBy({ flashId: flash.id, userId: meId }).then(x => x != null) : undefined, isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined,
}); });
} }

View File

@ -46,7 +46,7 @@ export class GalleryPostEntityService {
tags: post.tags.length > 0 ? post.tags : undefined, tags: post.tags.length > 0 ? post.tags : undefined,
isSensitive: post.isSensitive, isSensitive: post.isSensitive,
likedCount: post.likedCount, likedCount: post.likedCount,
isLiked: meId ? await this.galleryLikesRepository.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined, isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined,
}); });
} }

View File

@ -106,16 +106,14 @@ export class NoteEntityService implements OnModuleInit {
hide = false; hide = false;
} else { } else {
// フォロワーかどうか // フォロワーかどうか
const following = await this.followingsRepository.findOneBy({ const isFollowing = await this.followingsRepository.exist({
followeeId: packedNote.userId, where: {
followerId: meId, followeeId: packedNote.userId,
followerId: meId,
},
}); });
if (following == null) { hide = !isFollowing;
hide = true;
} else {
hide = false;
}
} }
} }

View File

@ -97,7 +97,7 @@ export class PageEntityService {
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null, eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)), attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)),
likedCount: page.likedCount, likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined, isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined,
}); });
} }

View File

@ -230,12 +230,14 @@ export class UserEntityService implements OnModuleInit {
/* /*
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({ const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({
antennaId: In(myAntennas.map(x => x.id)), where: {
read: false, antennaId: In(myAntennas.map(x => x.id)),
}) : null; read: false,
},
}) : false);
return unread != null; return isUnread;
*/ */
return false; // TODO return false; // TODO
} }

View File

@ -128,12 +128,12 @@ export class SignupApiService {
} }
if (instance.emailRequiredForSignup) { if (instance.emailRequiredForSignup) {
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
} }
// Check deleted username duplication // Check deleted username duplication
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
throw new FastifyReplyError(400, 'USED_USERNAME'); throw new FastifyReplyError(400, 'USED_USERNAME');
} }

View File

@ -50,9 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw e; throw e;
}); });
const exist = await this.promoNotesRepository.findOneBy({ noteId: note.id }); const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } });
if (exist != null) { if (exist) {
throw new ApiError(meta.errors.alreadyPromoted); throw new ApiError(meta.errors.alreadyPromoted);
} }

View File

@ -69,8 +69,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps) => { super(meta, paramDef, async (ps) => {
const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } });
if (role == null) { if (!roleExist) {
throw new ApiError(meta.errors.noSuchRole); throw new ApiError(meta.errors.noSuchRole);
} }

View File

@ -58,12 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const accessToken = secureRndstr(32); const accessToken = secureRndstr(32);
// Fetch exist access token // Fetch exist access token
const exist = await this.accessTokensRepository.findOneBy({ const exist = await this.accessTokensRepository.exist({
appId: session.appId, where: {
userId: me.id, appId: session.appId,
userId: me.id,
},
}); });
if (exist == null) { if (!exist) {
const app = await this.appsRepository.findOneByOrFail({ id: session.appId }); const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
// Generate Hash // Generate Hash

View File

@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}); });
// Check if already blocking // Check if already blocking
const exist = await this.blockingsRepository.findOneBy({ const exist = await this.blockingsRepository.exist({
blockerId: blocker.id, where: {
blockeeId: blockee.id, blockerId: blocker.id,
blockeeId: blockee.id,
},
}); });
if (exist != null) { if (exist) {
throw new ApiError(meta.errors.alreadyBlocking); throw new ApiError(meta.errors.alreadyBlocking);
} }

View File

@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}); });
// Check not blocking // Check not blocking
const exist = await this.blockingsRepository.findOneBy({ const exist = await this.blockingsRepository.exist({
blockerId: blocker.id, where: {
blockeeId: blockee.id, blockerId: blocker.id,
blockeeId: blockee.id,
}
}); });
if (exist == null) { if (!exist) {
throw new ApiError(meta.errors.notBlocking); throw new ApiError(meta.errors.notBlocking);
} }

View File

@ -87,12 +87,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw e; throw e;
}); });
const exist = await this.clipNotesRepository.findOneBy({ const exist = await this.clipNotesRepository.exist({
noteId: note.id, where: {
clipId: clip.id, noteId: note.id,
clipId: clip.id,
},
}); });
if (exist != null) { if (exist) {
throw new ApiError(meta.errors.alreadyClipped); throw new ApiError(meta.errors.alreadyClipped);
} }

View File

@ -58,12 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchClip); throw new ApiError(meta.errors.noSuchClip);
} }
const exist = await this.clipFavoritesRepository.findOneBy({ const exist = await this.clipFavoritesRepository.exist({
clipId: clip.id, where: {
userId: me.id, clipId: clip.id,
userId: me.id,
},
}); });
if (exist != null) { if (exist) {
throw new ApiError(meta.errors.alreadyFavorited); throw new ApiError(meta.errors.alreadyFavorited);
} }

View File

@ -34,12 +34,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ const exist = await this.driveFilesRepository.exist({
md5: ps.md5, where: {
userId: me.id, md5: ps.md5,
userId: me.id,
},
}); });
return file != null; return exist;
}); });
} }
} }

View File

@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
// if already liked // if already liked
const exist = await this.flashLikesRepository.findOneBy({ const exist = await this.flashLikesRepository.exist({
flashId: flash.id, where: {
userId: me.id, flashId: flash.id,
userId: me.id,
},
}); });
if (exist != null) { if (exist) {
throw new ApiError(meta.errors.alreadyLiked); throw new ApiError(meta.errors.alreadyLiked);
} }

View File

@ -99,12 +99,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}); });
// Check if already following // Check if already following
const exist = await this.followingsRepository.findOneBy({ const exist = await this.followingsRepository.exist({
followerId: follower.id, where: {
followeeId: followee.id, followerId: follower.id,
followeeId: followee.id,
},
}); });
if (exist != null) { if (exist) {
throw new ApiError(meta.errors.alreadyFollowing); throw new ApiError(meta.errors.alreadyFollowing);
} }

View File

@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}); });
// Check not following // Check not following
const exist = await this.followingsRepository.findOneBy({ const exist = await this.followingsRepository.exist({
followerId: follower.id, where: {
followeeId: followee.id, followerId: follower.id,
followeeId: followee.id,
},
}); });
if (exist == null) { if (!exist) {
throw new ApiError(meta.errors.notFollowing); throw new ApiError(meta.errors.notFollowing);
} }

View File

@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
// if already liked // if already liked
const exist = await this.galleryLikesRepository.findOneBy({ const exist = await this.galleryLikesRepository.exist({
postId: post.id, where: {
userId: me.id, postId: post.id,
userId: me.id,
},
}); });
if (exist != null) { if (exist) {
throw new ApiError(meta.errors.alreadyLiked); throw new ApiError(meta.errors.alreadyLiked);
} }

View File

@ -66,8 +66,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private downloadService: DownloadService, private downloadService: DownloadService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const users = await this.usersRepository.findOneBy({ id: me.id }); const userExist = await this.usersRepository.exist({ where: { id: me.id } });
if (users === null) throw new ApiError(meta.errors.noSuchUser); if (!userExist) throw new ApiError(meta.errors.noSuchUser);
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (file === null) throw new ApiError(meta.errors.noSuchFile); if (file === null) throw new ApiError(meta.errors.noSuchFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile); if (file.size === 0) throw new ApiError(meta.errors.emptyFile);

View File

@ -47,19 +47,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
// Check if announcement exists // Check if announcement exists
const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId }); const announcementExist = await this.announcementsRepository.exist({ where: { id: ps.announcementId } });
if (announcement == null) { if (!announcementExist) {
throw new ApiError(meta.errors.noSuchAnnouncement); throw new ApiError(meta.errors.noSuchAnnouncement);
} }
// Check if already read // Check if already read
const read = await this.announcementReadsRepository.findOneBy({ const alreadyRead = await this.announcementReadsRepository.exist({
announcementId: ps.announcementId, where: {
userId: me.id, announcementId: ps.announcementId,
userId: me.id,
},
}); });
if (read != null) { if (alreadyRead) {
return; return;
} }

View File

@ -28,9 +28,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const token = await this.accessTokensRepository.findOneBy({ id: ps.tokenId }); const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
if (token) { if (tokenExist) {
await this.accessTokensRepository.delete({ await this.accessTokensRepository.delete({
id: ps.tokenId, id: ps.tokenId,
userId: me.id, userId: me.id,

View File

@ -79,12 +79,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}); });
// Check if already muting // Check if already muting
const exist = await this.mutingsRepository.findOneBy({ const exist = await this.mutingsRepository.exist({
muterId: muter.id, where: {
muteeId: mutee.id, muterId: muter.id,
muteeId: mutee.id,
},
}); });
if (exist != null) { if (exist) {
throw new ApiError(meta.errors.alreadyMuting); throw new ApiError(meta.errors.alreadyMuting);
} }

View File

@ -217,11 +217,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Check blocking // Check blocking
if (renote.userId !== me.id) { if (renote.userId !== me.id) {
const block = await this.blockingsRepository.findOneBy({ const blockExist = await this.blockingsRepository.exist({
blockerId: renote.userId, where: {
blockeeId: me.id, blockerId: renote.userId,
blockeeId: me.id,
},
}); });
if (block) { if (blockExist) {
throw new ApiError(meta.errors.youHaveBeenBlocked); throw new ApiError(meta.errors.youHaveBeenBlocked);
} }
} }
@ -240,11 +242,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Check blocking // Check blocking
if (reply.userId !== me.id) { if (reply.userId !== me.id) {
const block = await this.blockingsRepository.findOneBy({ const blockExist = await this.blockingsRepository.exist({
blockerId: reply.userId, where: {
blockeeId: me.id, blockerId: reply.userId,
blockeeId: me.id,
},
}); });
if (block) { if (blockExist) {
throw new ApiError(meta.errors.youHaveBeenBlocked); throw new ApiError(meta.errors.youHaveBeenBlocked);
} }
} }

View File

@ -63,12 +63,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}); });
// if already favorited // if already favorited
const exist = await this.noteFavoritesRepository.findOneBy({ const exist = await this.noteFavoritesRepository.exist({
noteId: note.id, where: {
userId: me.id, noteId: note.id,
userId: me.id,
},
}); });
if (exist != null) { if (exist) {
throw new ApiError(meta.errors.alreadyFavorited); throw new ApiError(meta.errors.alreadyFavorited);
} }

View File

@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
// if already liked // if already liked
const exist = await this.pageLikesRepository.findOneBy({ const exist = await this.pageLikesRepository.exist({
pageId: page.id, where: {
userId: me.id, pageId: page.id,
userId: me.id,
},
}); });
if (exist != null) { if (exist) {
throw new ApiError(meta.errors.alreadyLiked); throw new ApiError(meta.errors.alreadyLiked);
} }

View File

@ -44,12 +44,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw err; throw err;
}); });
const exist = await this.promoReadsRepository.findOneBy({ const exist = await this.promoReadsRepository.exist({
noteId: note.id, where: {
userId: me.id, noteId: note.id,
userId: me.id,
},
}); });
if (exist != null) { if (exist) {
return; return;
} }

View File

@ -97,11 +97,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (me == null) { if (me == null) {
throw new ApiError(meta.errors.forbidden); throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) { } else if (me.id !== user.id) {
const following = await this.followingsRepository.findOneBy({ const isFollowing = await this.followingsRepository.exist({
followeeId: user.id, where: {
followerId: me.id, followeeId: user.id,
followerId: me.id,
},
}); });
if (following == null) { if (!isFollowing) {
throw new ApiError(meta.errors.forbidden); throw new ApiError(meta.errors.forbidden);
} }
} }

View File

@ -97,11 +97,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (me == null) { if (me == null) {
throw new ApiError(meta.errors.forbidden); throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) { } else if (me.id !== user.id) {
const following = await this.followingsRepository.findOneBy({ const isFollowing = await this.followingsRepository.exist({
followeeId: user.id, where: {
followerId: me.id, followeeId: user.id,
followerId: me.id,
},
}); });
if (following == null) { if (!isFollowing) {
throw new ApiError(meta.errors.forbidden); throw new ApiError(meta.errors.forbidden);
} }
} }

View File

@ -84,11 +84,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private roleService: RoleService, private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const list = await this.userListsRepository.findOneBy({ const listExist = await this.userListsRepository.exist({
id: ps.listId, where: {
isPublic: true, id: ps.listId,
isPublic: true,
},
}); });
if (list === null) throw new ApiError(meta.errors.noSuchList); if (!listExist) throw new ApiError(meta.errors.noSuchList);
const currentCount = await this.userListsRepository.countBy({ const currentCount = await this.userListsRepository.countBy({
userId: me.id, userId: me.id,
}); });
@ -114,18 +116,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}); });
if (currentUser.id !== me.id) { if (currentUser.id !== me.id) {
const block = await this.blockingsRepository.findOneBy({ const blockExist = await this.blockingsRepository.exist({
blockerId: currentUser.id, where: {
blockeeId: me.id, blockerId: currentUser.id,
blockeeId: me.id,
},
}); });
if (block) { if (blockExist) {
throw new ApiError(meta.errors.youHaveBeenBlocked); throw new ApiError(meta.errors.youHaveBeenBlocked);
} }
} }
const exist = await this.userListJoiningsRepository.findOneBy({ const exist = await this.userListJoiningsRepository.exist({
userListId: userList.id, where: {
userId: currentUser.id, userListId: userList.id,
userId: currentUser.id,
},
}); });
if (exist) { if (exist) {

View File

@ -41,21 +41,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private idService: IdService, private idService: IdService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const userList = await this.userListsRepository.findOneBy({ const userListExist = await this.userListsRepository.exist({
id: ps.listId, where: {
isPublic: true, id: ps.listId,
isPublic: true,
},
}); });
if (userList === null) { if (!userListExist) {
throw new ApiError(meta.errors.noSuchList); throw new ApiError(meta.errors.noSuchList);
} }
const exist = await this.userListFavoritesRepository.findOneBy({ const exist = await this.userListFavoritesRepository.exist({
userId: me.id, where: {
userListId: ps.listId, userId: me.id,
userListId: ps.listId,
},
}); });
if (exist !== null) { if (exist) {
throw new ApiError(meta.errors.alreadyFavorited); throw new ApiError(meta.errors.alreadyFavorited);
} }

View File

@ -100,18 +100,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Check blocking // Check blocking
if (user.id !== me.id) { if (user.id !== me.id) {
const block = await this.blockingsRepository.findOneBy({ const blockExist = await this.blockingsRepository.exist({
blockerId: user.id, where: {
blockeeId: me.id, blockerId: user.id,
blockeeId: me.id,
},
}); });
if (block) { if (blockExist) {
throw new ApiError(meta.errors.youHaveBeenBlocked); throw new ApiError(meta.errors.youHaveBeenBlocked);
} }
} }
const exist = await this.userListJoiningsRepository.findOneBy({ const exist = await this.userListJoiningsRepository.exist({
userListId: userList.id, where: {
userId: user.id, userListId: userList.id,
userId: user.id,
},
}); });
if (exist) { if (exist) {

View File

@ -69,10 +69,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userListId: ps.listId, userListId: ps.listId,
}); });
if (me !== null) { if (me !== null) {
additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({ additionalProperties.isLiked = await this.userListFavoritesRepository.exist({
userId: me.id, where: {
userListId: ps.listId, userId: me.id,
}) !== null); userListId: ps.listId,
},
});
} else { } else {
additionalProperties.isLiked = false; additionalProperties.isLiked = false;
} }

View File

@ -39,12 +39,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userListFavoritesRepository: UserListFavoritesRepository, private userListFavoritesRepository: UserListFavoritesRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const userList = await this.userListsRepository.findOneBy({ const userListExist = await this.userListsRepository.exist({
id: ps.listId, where: {
isPublic: true, id: ps.listId,
isPublic: true,
},
}); });
if (userList === null) { if (!userListExist) {
throw new ApiError(meta.errors.noSuchList); throw new ApiError(meta.errors.noSuchList);
} }

View File

@ -34,11 +34,13 @@ class UserListChannel extends Channel {
this.listId = params.listId as string; this.listId = params.listId as string;
// Check existence and owner // Check existence and owner
const list = await this.userListsRepository.findOneBy({ const listExist = await this.userListsRepository.exist({
id: this.listId, where: {
userId: this.user!.id, id: this.listId,
userId: this.user!.id,
},
}); });
if (!list) return; if (!listExist) return;
// Subscribe stream // Subscribe stream
this.subscriber.on(`userListStream:${this.listId}`, this.send); this.subscriber.on(`userListStream:${this.listId}`, this.send);

View File

@ -4,3 +4,4 @@ import { Cache } from '@/scripts/cache';
export const clipsCache = new Cache<misskey.entities.Clip[]>(Infinity); export const clipsCache = new Cache<misskey.entities.Clip[]>(Infinity);
export const rolesCache = new Cache(Infinity); export const rolesCache = new Cache(Infinity);
export const userListsCache = new Cache<misskey.entities.UserList[]>(Infinity); export const userListsCache = new Cache<misskey.entities.UserList[]>(Infinity);
export const antennasCache = new Cache<misskey.entities.Antenna[]>(Infinity);

View File

@ -1,24 +1,29 @@
<template> <template>
<div> <div>
<div v-for="user in users" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;"> <div v-for="user in users.slice(0, limit)" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
<MkAvatar :user="user" style="width:32px; height:32px;" indicator link preview/> <MkAvatar :user="user" style="width:32px; height:32px;" indicator link preview/>
</div> </div>
<div v-if="users.length > limit" style="display: inline-block;">...</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import { UserLite } from 'misskey-js/built/entities';
const props = defineProps<{ const props = withDefaults(defineProps<{
userIds: string[]; userIds: string[];
}>(); limit?: number;
}>(), {
limit: Infinity,
});
const users = ref([]); const users = ref<UserLite[]>([]);
onMounted(async () => { onMounted(async () => {
users.value = await os.api('users/show', { users.value = await os.api('users/show', {
userIds: props.userIds, userIds: props.userIds,
}); }) as unknown as UserLite[];
}); });
</script> </script>

View File

@ -113,7 +113,7 @@ onMounted(() => {
right: 0, right: 0,
}, },
imageClickAction: 'close', imageClickAction: 'close',
tapAction: 'toggle-controls', tapAction: 'close',
bgOpacity: 1, bgOpacity: 1,
showAnimationDuration: 100, showAnimationDuration: 100,
hideAnimationDuration: 100, hideAnimationDuration: 100,

View File

@ -87,7 +87,7 @@ const props = defineProps<{
channelId: string; channelId: string;
}>(); }>();
let tab = $ref('timeline'); let tab = $ref('overview');
let channel = $ref(null); let channel = $ref(null);
let favorited = $ref(false); let favorited = $ref(false);
let searchQuery = $ref(''); let searchQuery = $ref('');
@ -107,6 +107,9 @@ watch(() => props.channelId, async () => {
channelId: props.channelId, channelId: props.channelId,
}); });
favorited = channel.isFavorited; favorited = channel.isFavorited;
if (favorited || channel.isFollowing) {
tab = 'timeline';
}
}, { immediate: true }); }, { immediate: true });
function edit() { function edit() {

View File

@ -18,7 +18,7 @@
<MkButton inline @click="setTagBulk">Set tag</MkButton> <MkButton inline @click="setTagBulk">Set tag</MkButton>
<MkButton inline @click="addTagBulk">Add tag</MkButton> <MkButton inline @click="addTagBulk">Add tag</MkButton>
<MkButton inline @click="removeTagBulk">Remove tag</MkButton> <MkButton inline @click="removeTagBulk">Remove tag</MkButton>
<MkButton inline @click="setLisenceBulk">Set Lisence</MkButton> <MkButton inline @click="setLicenseBulk">Set License</MkButton>
<MkButton inline danger @click="delBulk">Delete</MkButton> <MkButton inline danger @click="delBulk">Delete</MkButton>
</div> </div>
<MkPagination ref="emojisPaginationComponent" :pagination="pagination"> <MkPagination ref="emojisPaginationComponent" :pagination="pagination">
@ -221,7 +221,7 @@ const setCategoryBulk = async () => {
emojisPaginationComponent.value.reload(); emojisPaginationComponent.value.reload();
}; };
const setLisenceBulk = async () => { const setLicenseBulk = async () => {
const { canceled, result } = await os.inputText({ const { canceled, result } = await os.inputText({
title: 'License', title: 'License',
}); });

View File

@ -9,6 +9,7 @@ import XAntenna from './editor.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { useRouter } from '@/router'; import { useRouter } from '@/router';
import { antennasCache } from '@/cache';
const router = useRouter(); const router = useRouter();
@ -26,13 +27,10 @@ let draft = $ref({
}); });
function onAntennaCreated() { function onAntennaCreated() {
antennasCache.delete();
router.push('/my/antennas'); router.push('/my/antennas');
} }
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({ definePageMetadata({
title: i18n.ts.manageAntennas, title: i18n.ts.manageAntennas,
icon: 'ti ti-antenna', icon: 'ti ti-antenna',

View File

@ -10,6 +10,7 @@ import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { useRouter } from '@/router'; import { useRouter } from '@/router';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { antennasCache } from '@/cache';
const router = useRouter(); const router = useRouter();
@ -20,6 +21,7 @@ const props = defineProps<{
}>(); }>();
function onAntennaUpdated() { function onAntennaUpdated() {
antennasCache.delete();
router.push('/my/antennas'); router.push('/my/antennas');
} }
@ -27,10 +29,6 @@ os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) =
antenna = antennaResponse; antenna = antennaResponse;
}); });
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({ definePageMetadata({
title: i18n.ts.manageAntennas, title: i18n.ts.manageAntennas,
icon: 'ti ti-antenna', icon: 'ti ti-antenna',

View File

@ -2,15 +2,20 @@
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700"> <MkSpacer :contentMax="700">
<div class="ieepwinx"> <div>
<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <div v-if="antennas.length === 0" class="empty">
<div class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div>
</div>
</div>
<div class=""> <MkButton :link="true" to="/my/antennas/create" primary :class="$style.add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
<MkA v-for="antenna in items" :key="antenna.id" class="ljoevbzj" :to="`/my/antennas/${antenna.id}`"> <div v-if="antennas.length > 0" class="_gaps">
<div class="name">{{ antenna.name }}</div> <MkA v-for="antenna in antennas" :key="antenna.id" :class="$style.antenna" :to="`/my/antennas/${antenna.id}`">
</MkA> <div class="name">{{ antenna.name }}</div>
</MkPagination> </MkA>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>
@ -18,19 +23,31 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { antennasCache } from '@/cache';
import { api } from '@/os';
import { onActivated } from 'vue';
import { infoImageUrl } from '@/instance';
const pagination = { const antennas = $computed(() => antennasCache.value.value ?? []);
endpoint: 'antennas/list' as const,
noPaging: true,
limit: 10,
};
const headerActions = $computed(() => []); function fetch() {
antennasCache.fetch(() => api('antennas/list'));
}
fetch();
const headerActions = $computed(() => [{
asFullButton: true,
icon: 'ti ti-refresh',
text: i18n.ts.reload,
handler: () => {
antennasCache.delete();
fetch();
},
}]);
const headerTabs = $computed(() => []); const headerTabs = $computed(() => []);
@ -38,30 +55,30 @@ definePageMetadata({
title: i18n.ts.manageAntennas, title: i18n.ts.manageAntennas,
icon: 'ti ti-antenna', icon: 'ti ti-antenna',
}); });
onActivated(() => {
antennasCache.fetch(() => api('antennas/list'));
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.ieepwinx { .add {
margin: 0 auto 16px auto;
}
> .add { .antenna {
margin: 0 auto 16px auto; display: block;
} padding: 16px;
border: solid 1px var(--divider);
border-radius: 6px;
.ljoevbzj { &:hover {
display: block; border: solid 1px var(--accent);
padding: 16px; text-decoration: none;
margin-bottom: 8px;
border: solid 1px var(--divider);
border-radius: 6px;
&:hover {
border: solid 1px var(--accent);
text-decoration: none;
}
> .name {
font-weight: bold;
}
} }
} }
.name {
font-weight: bold;
}
</style> </style>

View File

@ -3,38 +3,43 @@
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700"> <MkSpacer :contentMax="700">
<div class="_gaps"> <div class="_gaps">
<div v-if="items.length === 0" class="empty">
<div class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
<div>{{ i18n.ts.nothing }}</div>
</div>
</div>
<MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton> <MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton>
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination"> <div v-if="items.length > 0" class="_gaps">
<div class="_gaps"> <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> <div style="margin-bottom: 4px;">{{ list.name }}</div>
<div style="margin-bottom: 4px;">{{ list.name }}</div> <MkAvatars :userIds="list.userIds" :limit="10"/>
<MkAvatars :userIds="list.userIds"/> </MkA>
</MkA> </div>
</div>
</MkPagination>
</div> </div>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { onActivated } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkAvatars from '@/components/MkAvatars.vue'; import MkAvatars from '@/components/MkAvatars.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { userListsCache } from '@/cache'; import { userListsCache } from '@/cache';
import { infoImageUrl } from '@/instance';
const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>(); const items = $computed(() => userListsCache.value.value ?? []);
const pagination = { function fetch() {
endpoint: 'users/lists/list' as const, userListsCache.fetch(() => os.api('users/lists/list'));
noPaging: true, }
limit: 10,
}; fetch();
async function create() { async function create() {
const { canceled, result: name } = await os.inputText({ const { canceled, result: name } = await os.inputText({
@ -43,10 +48,18 @@ async function create() {
if (canceled) return; if (canceled) return;
await os.apiWithDialog('users/lists/create', { name: name }); await os.apiWithDialog('users/lists/create', { name: name });
userListsCache.delete(); userListsCache.delete();
pagingComponent.reload(); fetch();
} }
const headerActions = $computed(() => []); const headerActions = $computed(() => [{
asFullButton: true,
icon: 'ti ti-refresh',
text: i18n.ts.reload,
handler: () => {
userListsCache.delete();
fetch();
},
}]);
const headerTabs = $computed(() => []); const headerTabs = $computed(() => []);
@ -58,6 +71,10 @@ definePageMetadata({
handler: create, handler: create,
}, },
}); });
onActivated(() => {
fetch();
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@ -166,7 +166,7 @@ const menuDef = computed(() => [{
active: currentPage?.route.name === 'import-export', active: currentPage?.route.name === 'import-export',
}, { }, {
icon: 'ti ti-plane', icon: 'ti ti-plane',
text: `${i18n.ts.accountMigration} (${i18n.ts.experimental})`, text: `${i18n.ts.accountMigration}`,
to: '/settings/migration', to: '/settings/migration',
active: currentPage?.route.name === 'migration', active: currentPage?.route.name === 'migration',
}, { }, {

View File

@ -1,8 +1,5 @@
<template> <template>
<div class="_gaps_m"> <div class="_gaps_m">
<FormInfo warn>
{{ i18n.ts.thisIsExperimentalFeature }}
</FormInfo>
<MkFolder :defaultOpen="true"> <MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-plane-arrival"></i></template> <template #icon><i class="ti ti-plane-arrival"></i></template>
<template #label>{{ i18n.ts._accountMigration.moveFrom }}</template> <template #label>{{ i18n.ts._accountMigration.moveFrom }}</template>

View File

@ -1,7 +1,8 @@
import { ref } from "vue";
export class Cache<T> { export class Cache<T> {
private cachedAt: number | null = null; private cachedAt: number | null = null;
private value: T | undefined; public value = ref<T | undefined>();
private lifetime: number; private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) { constructor(lifetime: Cache<never>['lifetime']) {
@ -10,21 +11,20 @@ export class Cache<T> {
public set(value: T): void { public set(value: T): void {
this.cachedAt = Date.now(); this.cachedAt = Date.now();
this.value = value; this.value.value = value;
} }
public get(): T | undefined { private get(): T | undefined {
if (this.cachedAt == null) return undefined; if (this.cachedAt == null) return undefined;
if ((Date.now() - this.cachedAt) > this.lifetime) { if ((Date.now() - this.cachedAt) > this.lifetime) {
this.value = undefined; this.value.value = undefined;
this.cachedAt = null; this.cachedAt = null;
return undefined; return undefined;
} }
return this.value; return this.value.value;
} }
public delete() { public delete() {
this.value = undefined;
this.cachedAt = null; this.cachedAt = null;
} }

View File

@ -1,3 +1,4 @@
import { toUnicode } from 'punycode';
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@ -8,8 +9,7 @@ import { defaultStore, userActions } from '@/store';
import { $i, iAmModerator } from '@/account'; import { $i, iAmModerator } from '@/account';
import { mainRouter } from '@/router'; import { mainRouter } from '@/router';
import { Router } from '@/nirax'; import { Router } from '@/nirax';
import { rolesCache, userListsCache } from '@/cache'; import { antennasCache, rolesCache, userListsCache } from '@/cache';
import { toUnicode } from 'punycode';
export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) { export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
const meId = $i ? $i.id : null; const meId = $i ? $i.id : null;
@ -166,11 +166,39 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
return lists.map(list => ({ return lists.map(list => ({
text: list.name, text: list.name,
action: () => { action: async () => {
os.apiWithDialog('users/lists/push', { await os.apiWithDialog('users/lists/push', {
listId: list.id, listId: list.id,
userId: user.id, userId: user.id,
}); });
userListsCache.delete();
},
}));
},
}, {
type: 'parent',
icon: 'ti ti-antenna',
text: i18n.ts.addToAntenna,
children: async () => {
const antennas = await antennasCache.fetch(() => os.api('antennas/list'));
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
return antennas.filter((a) => a.src === 'users').map(antenna => ({
text: antenna.name,
action: async () => {
await os.apiWithDialog('antennas/update', {
antennaId: antenna.id,
name: antenna.name,
keywords: antenna.keywords,
excludeKeywords: antenna.excludeKeywords,
src: antenna.src,
userListId: antenna.userListId,
users: [...antenna.users, canonical],
caseSensitive: antenna.caseSensitive,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
notify: antenna.notify,
});
antennasCache.delete();
}, },
})); }));
}, },

View File

@ -21,6 +21,10 @@ globalThis.addEventListener('activate', ev => {
); );
}); });
function offlineContentHTML(): string {
return `<!doctype html>Offline. Service Worker @${_VERSION_} <button onclick="location.reload()">reload</button>`
}
globalThis.addEventListener('fetch', ev => { globalThis.addEventListener('fetch', ev => {
let isHTMLRequest = false; let isHTMLRequest = false;
if (ev.request.headers.get('sec-fetch-dest') === 'document') { if (ev.request.headers.get('sec-fetch-dest') === 'document') {
@ -34,7 +38,14 @@ globalThis.addEventListener('fetch', ev => {
if (!isHTMLRequest) return; if (!isHTMLRequest) return;
ev.respondWith( ev.respondWith(
fetch(ev.request) fetch(ev.request)
.catch(() => new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 })), .catch(() => {
return new Response(offlineContentHTML(), {
status: 200,
headers: {
'content-type': 'text/html',
},
});
}),
); );
}); });