1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-06-21 14:54:47 +02:00

Compare commits

...

7 Commits

Author SHA1 Message Date
github-actions[bot]
2bc0ccb108 Release: 2026.5.2 2026-05-17 22:14:54 +00:00
おさむのひと
fc6c45d175 fix: add-i18n-keyの記述が間違っていたので修正 (#17418) 2026-05-17 19:30:35 +09:00
github-actions[bot]
99081be9fd Bump version to 2026.5.2-beta.1 2026-05-17 09:59:00 +00:00
かっこかり
9410bc5194 fix: move users/notify/list to following/list (#17416)
* fix: move `users/notify/list` to `following/list`

* fix

* fix lint

* fix test

* fix test

* fix test title
2026-05-17 18:51:10 +09:00
かっこかり
baad1c51d8 Update CHANGELOG.md 2026-05-16 12:54:19 +09:00
syuilo
e6375fb756 Update CONTRIBUTING.md 2026-05-15 10:12:17 +09:00
syuilo
92c1dc06f2 Update CONTRIBUTING.md 2026-05-15 10:11:42 +09:00
14 changed files with 180 additions and 145 deletions

View File

@@ -26,7 +26,9 @@ _settings:
general: "全般"
appearance: "外観"
# パラメータ付き (ICU MessageFormat 互換)
# パラメータ付き (単純なプレースホルダ置換)
# ICU MessageFormat の plural / select / number / date などは非対応
# 使えるのは `{name}` のような単純な置換のみ
greeting: "こんにちは、{name}さん"
```

View File

@@ -8,7 +8,8 @@
### General
- Enhance: Unicode 17.0 に収録されている絵文字の処理・表示に対応
- Fluent Emojiや端末ネイティブの絵文字を利用している場合は、最新の絵文字に対応しておらず正しく表示できない可能性があります。絵文字が表示できない場合は、表示に使用する絵文字をTwemojiに切り替えてご利用ください。
- 投稿通知設定したユーザーをリストで見ることができるように
- Enhance: 投稿通知設定したユーザーをリストで見ることができるように
- 依存関係の更新
### Client
- Enhance: テーマのプレビュー時、リロードせずにもとのテーマに戻せるように

View File

@@ -189,6 +189,14 @@ pnpm migrate
After finishing the migration, you can proceed.
#### Cloudflare tunnel
Cloudflare tunnelを使うとローカルのMisskeyサーバーをインターネットに公開できます。
HTTPSでしか動作しない機能を検証したい時や、スマホなど別のデバイスからローカルのMisskeyサーバーを検証したい時に便利です。
##### Cloudflare warpと併用する際のtips
> cloudflared (Cloudflare Tunnel) は region1.v2.argotunnel.com / region2.v2.argotunnel.com に QUIC/HTTP2 でアウトバウンド接続するのですが、WARP を有効化するとこのトラフィックが WARP 経由になってループ/切断します。これら 2 ホストを WARP のトンネル除外split tunnelに追加することで、cloudflared だけは WARP をバイパスして直接 Cloudflare エッジへ接続できるようになります。
### Start developing
During development, it is useful to use the
```

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2026.5.2-beta.0",
"version": "2026.5.2",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@@ -217,6 +217,7 @@ export * as 'flash/search' from './endpoints/flash/search.js';
export * as 'following/create' from './endpoints/following/create.js';
export * as 'following/delete' from './endpoints/following/delete.js';
export * as 'following/invalidate' from './endpoints/following/invalidate.js';
export * as 'following/list' from './endpoints/following/list.js';
export * as 'following/requests/accept' from './endpoints/following/requests/accept.js';
export * as 'following/requests/cancel' from './endpoints/following/requests/cancel.js';
export * as 'following/requests/list' from './endpoints/following/requests/list.js';
@@ -391,7 +392,6 @@ export * as 'users/featured-notes' from './endpoints/users/featured-notes.js';
export * as 'users/flashs' from './endpoints/users/flashs.js';
export * as 'users/followers' from './endpoints/users/followers.js';
export * as 'users/following' from './endpoints/users/following.js';
export * as 'users/notify/list' from './endpoints/users/notify/list.js';
export * as 'users/get-following-users-by-birthday' from './endpoints/users/get-following-users-by-birthday.js';
export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js';
export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js';

View File

@@ -4,7 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
import type { FollowingsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
@@ -15,7 +15,7 @@ export const meta = {
requireCredential: true,
kind: 'read:following',
description: 'List of following users with notification enabled.',
description: 'List of following users',
res: {
type: 'array',
@@ -23,7 +23,7 @@ export const meta = {
items: {
type: 'object',
optional: false, nullable: false,
ref: 'UserDetailed',
ref: 'Following',
},
},
} as const;
@@ -31,6 +31,7 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
notification: { type: 'boolean', default: false },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
@@ -45,22 +46,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
private followingEntityService: FollowingEntityService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.innerJoinAndSelect('following.followee', 'followee')
.andWhere('following.followerId = :userId', { userId: me.id })
.andWhere('following.notify IS NOT NULL');
.andWhere('following.followerId = :userId', { userId: me.id });
if (ps.notification) {
query.andWhere('following.notify IS NOT NULL');
}
query.innerJoinAndSelect('following.followee', 'followee');
const followings = await query
.limit(ps.limit)
.getMany();
const users = followings.map(f => f.followee!);
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
return await this.followingEntityService.packMany(followings, me, { populateFollowee: true });
});
}
}

View File

@@ -9,7 +9,7 @@ import { describe, beforeAll, test } from 'vitest';
import { api, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('users/notify/list', () => {
describe('following/list', () => {
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse;
@@ -24,11 +24,19 @@ describe('users/notify/list', () => {
// alice が bob を普通にフォロー(通知設定なし)
await api('following/create', { userId: bob.id }, alice);
const res = await api('users/notify/list', {}, alice);
const res1 = await api('following/list', { notification: true }, alice);
const res2 = await api('following/list', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 0);
// notification: true の場合は通知設定なしのフォローは返らない
assert.strictEqual(res1.status, 200);
assert.strictEqual(Array.isArray(res1.body), true);
assert.strictEqual(res1.body.length, 0);
// notification パラメータなしの場合は通知設定なしのフォローも返る
assert.strictEqual(res2.status, 200);
assert.strictEqual(Array.isArray(res2.body), true);
assert.strictEqual(res2.body.length, 1);
assert.strictEqual(res2.body[0].followeeId, bob.id);
});
test('通知設定ありのフォローがある場合、そのユーザーが返る', async () => {
@@ -36,34 +44,42 @@ describe('users/notify/list', () => {
await api('following/create', { userId: carol.id, withReplies: false }, alice);
await api('following/update', { userId: carol.id, notify: 'normal' }, alice);
const res = await api('users/notify/list', {}, alice);
const res = await api('following/list', { notification: true }, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.length, 1);
assert.strictEqual(res.body[0].id, carol.id);
assert.strictEqual(res.body[0].followeeId, carol.id);
});
test('複数ユーザーで通知設定ありの場合、全員返る', async () => {
// bob にも通知設定をON
await api('following/update', { userId: bob.id, notify: 'normal' }, alice);
const res = await api('users/notify/list', {}, alice);
const res = await api('following/list', { notification: true }, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.length, 2);
const ids = res.body.map((u: { id: string }) => u.id).sort();
const ids = res.body.map((u) => u.followeeId).sort();
assert.deepStrictEqual(ids, [bob.id, carol.id].sort());
});
test('通知設定をOFFnoneにすると一覧から外れる', async () => {
test('通知設定をOFFnoneにすると notification: true な一覧から外れる', async () => {
await api('following/update', { userId: bob.id, notify: 'none' }, alice);
const res = await api('users/notify/list', {}, alice);
const res1 = await api('following/list', { notification: true }, alice);
const res2 = await api('following/list', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.length, 1);
assert.strictEqual(res.body[0].id, carol.id);
// notification: true の場合は bob は返らない
assert.strictEqual(res1.status, 200);
assert.strictEqual(res1.body.length, 1);
assert.strictEqual(res1.body[0].followeeId, carol.id);
// notification パラメータなしの場合は bob も返る
assert.strictEqual(res2.status, 200);
assert.strictEqual(res2.body.length, 2);
const ids = res2.body.map((u) => u.followeeId).sort();
assert.deepStrictEqual(ids, [bob.id, carol.id].sort());
});
test('他のユーザーの通知対象は見えない', async () => {
@@ -72,14 +88,14 @@ describe('users/notify/list', () => {
await api('following/update', { userId: carol.id, notify: 'normal' }, bob);
// alice の一覧には bob の通知設定は反映されない
const aliceRes = await api('users/notify/list', {}, alice);
const aliceIds = aliceRes.body.map((u: { id: string }) => u.id);
const aliceRes = await api('following/list', { notification: true }, alice);
const aliceIds = aliceRes.body.map((u) => u.followeeId);
assert.strictEqual(aliceIds.includes(bob.id), false);
// bob の一覧には carol だけが含まれる
const bobRes = await api('users/notify/list', {}, bob);
const bobRes = await api('following/list', { notification: true }, bob);
assert.strictEqual(bobRes.body.length, 1);
assert.strictEqual(bobRes.body[0].id, carol.id);
assert.strictEqual(bobRes.body[0].followeeId, carol.id);
// 後片付け: bob → carol のフォローを解除
await api('following/delete', { userId: carol.id }, bob);
@@ -114,18 +130,18 @@ describe('users/notify/list', () => {
await api('following/update', { userId: bob.id, notify: 'normal' }, alice);
// limitなしだと2件返ることを確認
const allRes = await api('users/notify/list', {}, alice);
const allRes = await api('following/list', { notification: true }, alice);
assert.strictEqual(allRes.status, 200);
assert.strictEqual(allRes.body.length, 2);
// limit:1 で1件に絞られることを確認
const res = await api('users/notify/list', { limit: 1 }, alice);
const res = await api('following/list', { notification: true, limit: 1 }, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.length, 1);
});
test('未認証の場合はエラー', async () => {
const res = await api('users/notify/list', {});
const res = await api('following/list', {});
assert.strictEqual(res.status, 401);
});
});

View File

@@ -47,10 +47,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<div v-for="item in items" :key="item.id" :class="[$style.userItem ]">
<div :class="$style.userItemMain">
<MkA :class="$style.userItemMainBody" :to="userPage(item)">
<MkUserCardMini :user="item"/>
<MkA :class="$style.userItemMainBody" :to="userPage(item.followee!)">
<MkUserCardMini :user="item.followee!"/>
</MkA>
<button class="_button" :class="$style.notifyMenu" @click="showNotifyMenu(item, $event)"><i class="ti ti-dots"></i></button>
<button class="_button" :class="$style.notifyMenu" @click="showNotifyMenu(item.followee!, $event)"><i class="ti ti-dots"></i></button>
</div>
</div>
</div>
@@ -125,8 +125,11 @@ async function showNotifyMenu(user: Misskey.entities.UserDetailed, ev: PointerEv
}], ev.currentTarget ?? ev.target);
}
const notifyUserPaginator = markRaw(new Paginator('users/notify/list', {
const notifyUserPaginator = markRaw(new Paginator('following/list', {
limit: 10,
params: {
notification: true,
},
}));
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[];

View File

@@ -1884,6 +1884,8 @@ declare namespace entities {
FollowingDeleteResponse,
FollowingInvalidateRequest,
FollowingInvalidateResponse,
FollowingListRequest,
FollowingListResponse,
FollowingRequestsAcceptRequest,
FollowingRequestsCancelRequest,
FollowingRequestsCancelResponse,
@@ -2162,8 +2164,6 @@ declare namespace entities {
UsersListsUpdateMembershipRequest,
UsersNotesRequest,
UsersNotesResponse,
UsersNotifyListRequest,
UsersNotifyListResponse,
UsersPagesRequest,
UsersPagesResponse,
UsersReactionsRequest,
@@ -2403,6 +2403,12 @@ type FollowingInvalidateRequest = operations['following___invalidate']['requestB
// @public (undocumented)
type FollowingInvalidateResponse = operations['following___invalidate']['responses']['200']['content']['application/json'];
// @public (undocumented)
type FollowingListRequest = operations['following___list']['requestBody']['content']['application/json'];
// @public (undocumented)
type FollowingListResponse = operations['following___list']['responses']['200']['content']['application/json'];
// @public (undocumented)
type FollowingRequestsAcceptRequest = operations['following___requests___accept']['requestBody']['content']['application/json'];
@@ -3829,12 +3835,6 @@ type UsersNotesRequest = operations['users___notes']['requestBody']['content']['
// @public (undocumented)
type UsersNotesResponse = operations['users___notes']['responses']['200']['content']['application/json'];
// @public (undocumented)
type UsersNotifyListRequest = operations['users___notify___list']['requestBody']['content']['application/json'];
// @public (undocumented)
type UsersNotifyListResponse = operations['users___notify___list']['responses']['200']['content']['application/json'];
// @public (undocumented)
type UsersPagesRequest = operations['users___pages']['requestBody']['content']['application/json'];

View File

@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2026.5.2-beta.0",
"version": "2026.5.2",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@@ -2570,6 +2570,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* List of following users
*
* **Credential required**: *Yes* / **Permission**: *read:following*
*/
request<E extends 'following/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
@@ -4697,17 +4708,6 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* List of following users with notification enabled.
*
* **Credential required**: *Yes* / **Permission**: *read:following*
*/
request<E extends 'users/notify/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* Show all pages this user created.
*

View File

@@ -360,6 +360,8 @@ import type {
FollowingDeleteResponse,
FollowingInvalidateRequest,
FollowingInvalidateResponse,
FollowingListRequest,
FollowingListResponse,
FollowingRequestsAcceptRequest,
FollowingRequestsCancelRequest,
FollowingRequestsCancelResponse,
@@ -640,8 +642,6 @@ import type {
UsersListsUpdateMembershipRequest,
UsersNotesRequest,
UsersNotesResponse,
UsersNotifyListRequest,
UsersNotifyListResponse,
UsersPagesRequest,
UsersPagesResponse,
UsersReactionsRequest,
@@ -896,6 +896,7 @@ export type Endpoints = {
'following/create': { req: FollowingCreateRequest; res: FollowingCreateResponse };
'following/delete': { req: FollowingDeleteRequest; res: FollowingDeleteResponse };
'following/invalidate': { req: FollowingInvalidateRequest; res: FollowingInvalidateResponse };
'following/list': { req: FollowingListRequest; res: FollowingListResponse };
'following/requests/accept': { req: FollowingRequestsAcceptRequest; res: EmptyResponse };
'following/requests/cancel': { req: FollowingRequestsCancelRequest; res: FollowingRequestsCancelResponse };
'following/requests/list': { req: FollowingRequestsListRequest; res: FollowingRequestsListResponse };
@@ -1086,7 +1087,6 @@ export type Endpoints = {
'users/lists/update': { req: UsersListsUpdateRequest; res: UsersListsUpdateResponse };
'users/lists/update-membership': { req: UsersListsUpdateMembershipRequest; res: EmptyResponse };
'users/notes': { req: UsersNotesRequest; res: UsersNotesResponse };
'users/notify/list': { req: UsersNotifyListRequest; res: UsersNotifyListResponse };
'users/pages': { req: UsersPagesRequest; res: UsersPagesResponse };
'users/reactions': { req: UsersReactionsRequest; res: UsersReactionsResponse };
'users/recommendation': { req: UsersRecommendationRequest; res: UsersRecommendationResponse };

View File

@@ -363,6 +363,8 @@ export type FollowingDeleteRequest = operations['following___delete']['requestBo
export type FollowingDeleteResponse = operations['following___delete']['responses']['200']['content']['application/json'];
export type FollowingInvalidateRequest = operations['following___invalidate']['requestBody']['content']['application/json'];
export type FollowingInvalidateResponse = operations['following___invalidate']['responses']['200']['content']['application/json'];
export type FollowingListRequest = operations['following___list']['requestBody']['content']['application/json'];
export type FollowingListResponse = operations['following___list']['responses']['200']['content']['application/json'];
export type FollowingRequestsAcceptRequest = operations['following___requests___accept']['requestBody']['content']['application/json'];
export type FollowingRequestsCancelRequest = operations['following___requests___cancel']['requestBody']['content']['application/json'];
export type FollowingRequestsCancelResponse = operations['following___requests___cancel']['responses']['200']['content']['application/json'];
@@ -643,8 +645,6 @@ export type UsersListsUpdateResponse = operations['users___lists___update']['res
export type UsersListsUpdateMembershipRequest = operations['users___lists___update-membership']['requestBody']['content']['application/json'];
export type UsersNotesRequest = operations['users___notes']['requestBody']['content']['application/json'];
export type UsersNotesResponse = operations['users___notes']['responses']['200']['content']['application/json'];
export type UsersNotifyListRequest = operations['users___notify___list']['requestBody']['content']['application/json'];
export type UsersNotifyListResponse = operations['users___notify___list']['responses']['200']['content']['application/json'];
export type UsersPagesRequest = operations['users___pages']['requestBody']['content']['application/json'];
export type UsersPagesResponse = operations['users___pages']['responses']['200']['content']['application/json'];
export type UsersReactionsRequest = operations['users___reactions']['requestBody']['content']['application/json'];

View File

@@ -2105,6 +2105,15 @@ export type paths = {
*/
post: operations['following___invalidate'];
};
'/following/list': {
/**
* following/list
* @description List of following users
*
* **Credential required**: *Yes* / **Permission**: *read:following*
*/
post: operations['following___list'];
};
'/following/requests/accept': {
/**
* following/requests/accept
@@ -3852,15 +3861,6 @@ export type paths = {
*/
post: operations['users___notes'];
};
'/users/notify/list': {
/**
* users/notify/list
* @description List of following users with notification enabled.
*
* **Credential required**: *Yes* / **Permission**: *read:following*
*/
post: operations['users___notify___list'];
};
'/users/pages': {
/**
* users/pages
@@ -22521,6 +22521,80 @@ export interface operations {
};
};
};
following___list: {
requestBody: {
content: {
'application/json': {
/** @default false */
notification?: boolean;
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
sinceDate?: number;
untilDate?: number;
/** @default 10 */
limit?: number;
};
};
};
responses: {
/** @description OK (with results) */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Following'][];
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
following___requests___accept: {
requestBody: {
content: {
@@ -36056,78 +36130,6 @@ export interface operations {
};
};
};
users___notify___list: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
sinceDate?: number;
untilDate?: number;
/** @default 10 */
limit?: number;
};
};
};
responses: {
/** @description OK (with results) */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['UserDetailed'][];
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
users___pages: {
requestBody: {
content: {