Merge remote-tracking branch 'misskey-dev/develop' into prismisskey
# Conflicts: # package.json
This commit is contained in:
commit
6e0b64bebe
|
@ -26,6 +26,8 @@
|
||||||
- Feat: 二要素認証のバックアップコードが生成されるようになりました
|
- Feat: 二要素認証のバックアップコードが生成されるようになりました
|
||||||
- ref. https://github.com/MisskeyIO/misskey/pull/121
|
- ref. https://github.com/MisskeyIO/misskey/pull/121
|
||||||
- Feat: 二要素認証でパスキーをサポートするようになりました
|
- Feat: 二要素認証でパスキーをサポートするようになりました
|
||||||
|
- Feat: 指定したユーザーが投稿したときに通知できるようになりました
|
||||||
|
- Feat: プロフィールでのリンク検証
|
||||||
- Feat: 通知をテストできるようになりました
|
- Feat: 通知をテストできるようになりました
|
||||||
- Feat: PWAのアイコンが設定できるようになりました
|
- Feat: PWAのアイコンが設定できるようになりました
|
||||||
- Enhance: manifest.jsonをオーバーライド可能に
|
- Enhance: manifest.jsonをオーバーライド可能に
|
||||||
|
@ -62,6 +64,7 @@
|
||||||
- Enhance: ScratchpadでAsync:系関数やボタンのコールバックなどのエラーにもダイアログを出すように(試験的なためPlayなどには未実装)
|
- Enhance: ScratchpadでAsync:系関数やボタンのコールバックなどのエラーにもダイアログを出すように(試験的なためPlayなどには未実装)
|
||||||
- Enhance: タイムラインでリスト/アンテナ選択時のパフォーマンスを改善
|
- Enhance: タイムラインでリスト/アンテナ選択時のパフォーマンスを改善
|
||||||
- Enhance: 「Moderation note」、「Add moderation note」をローカライズできるように
|
- Enhance: 「Moderation note」、「Add moderation note」をローカライズできるように
|
||||||
|
- Enhance: 細かなデザインの調整
|
||||||
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
|
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
|
||||||
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
|
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
|
||||||
- Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正
|
- Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正
|
||||||
|
|
|
@ -1123,6 +1123,9 @@ export interface Locale {
|
||||||
"loadConversation": string;
|
"loadConversation": string;
|
||||||
"pinnedList": string;
|
"pinnedList": string;
|
||||||
"keepScreenOn": string;
|
"keepScreenOn": string;
|
||||||
|
"verifiedLink": string;
|
||||||
|
"notifyNotes": string;
|
||||||
|
"unnotifyNotes": string;
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
|
@ -2029,6 +2032,7 @@ export interface Locale {
|
||||||
"metadataContent": string;
|
"metadataContent": string;
|
||||||
"changeAvatar": string;
|
"changeAvatar": string;
|
||||||
"changeBanner": string;
|
"changeBanner": string;
|
||||||
|
"verifiedLinkDescription": string;
|
||||||
};
|
};
|
||||||
"_exportOrImport": {
|
"_exportOrImport": {
|
||||||
"allNotes": string;
|
"allNotes": string;
|
||||||
|
@ -2158,6 +2162,7 @@ export interface Locale {
|
||||||
"youReceivedFollowRequest": string;
|
"youReceivedFollowRequest": string;
|
||||||
"yourFollowRequestAccepted": string;
|
"yourFollowRequestAccepted": string;
|
||||||
"pollEnded": string;
|
"pollEnded": string;
|
||||||
|
"newNote": string;
|
||||||
"unreadAntennaNote": string;
|
"unreadAntennaNote": string;
|
||||||
"emptyPushNotificationMessage": string;
|
"emptyPushNotificationMessage": string;
|
||||||
"achievementEarned": string;
|
"achievementEarned": string;
|
||||||
|
|
|
@ -1120,6 +1120,9 @@ loadReplies: "返信を見る"
|
||||||
loadConversation: "会話を見る"
|
loadConversation: "会話を見る"
|
||||||
pinnedList: "ピン留めされたリスト"
|
pinnedList: "ピン留めされたリスト"
|
||||||
keepScreenOn: "デバイスの画面を常にオンにする"
|
keepScreenOn: "デバイスの画面を常にオンにする"
|
||||||
|
verifiedLink: "このリンク先の所有者であることが確認されました"
|
||||||
|
notifyNotes: "投稿を通知"
|
||||||
|
unnotifyNotes: "投稿の通知を解除"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
|
@ -1944,6 +1947,7 @@ _profile:
|
||||||
metadataContent: "内容"
|
metadataContent: "内容"
|
||||||
changeAvatar: "アイコン画像を変更"
|
changeAvatar: "アイコン画像を変更"
|
||||||
changeBanner: "バナー画像を変更"
|
changeBanner: "バナー画像を変更"
|
||||||
|
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
|
||||||
|
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "全てのノート"
|
allNotes: "全てのノート"
|
||||||
|
@ -2072,6 +2076,7 @@ _notification:
|
||||||
youReceivedFollowRequest: "フォローリクエストが来ました"
|
youReceivedFollowRequest: "フォローリクエストが来ました"
|
||||||
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
||||||
pollEnded: "アンケートの結果が出ました"
|
pollEnded: "アンケートの結果が出ました"
|
||||||
|
newNote: "新しい投稿"
|
||||||
unreadAntennaNote: "アンテナ {name}"
|
unreadAntennaNote: "アンテナ {name}"
|
||||||
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
||||||
achievementEarned: "実績を獲得"
|
achievementEarned: "実績を獲得"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2023.9.0-beta.9-prismisskey.3",
|
"version": "2023.9.0-beta.10",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
"cssnano": "6.0.1",
|
"cssnano": "6.0.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"postcss": "8.4.30",
|
"postcss": "8.4.30",
|
||||||
"terser": "5.19.4",
|
"terser": "5.20.0",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class VerifiedLinks1695260774117 {
|
||||||
|
name = 'VerifiedLinks1695260774117'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "verifiedLinks" character varying array NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "verifiedLinks"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
export class FollowingNotify1695288787870 {
|
||||||
|
name = 'FollowingNotify1695288787870'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" ADD "notify" character varying(32)`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_5108098457488634a4768e1d12" ON "following" ("notify") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_5108098457488634a4768e1d12"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "notify"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -64,7 +64,7 @@
|
||||||
"@bull-board/ui": "5.8.4",
|
"@bull-board/ui": "5.8.4",
|
||||||
"@discordapp/twemoji": "14.1.2",
|
"@discordapp/twemoji": "14.1.2",
|
||||||
"@fastify/accepts": "4.2.0",
|
"@fastify/accepts": "4.2.0",
|
||||||
"@fastify/cookie": "9.0.4",
|
"@fastify/cookie": "9.1.0",
|
||||||
"@fastify/cors": "8.4.0",
|
"@fastify/cors": "8.4.0",
|
||||||
"@fastify/express": "2.3.0",
|
"@fastify/express": "2.3.0",
|
||||||
"@fastify/http-proxy": "9.2.1",
|
"@fastify/http-proxy": "9.2.1",
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.2",
|
||||||
"bullmq": "4.11.1",
|
"bullmq": "4.11.2",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.1",
|
"cbor": "9.0.1",
|
||||||
"chalk": "5.3.0",
|
"chalk": "5.3.0",
|
||||||
|
@ -158,7 +158,7 @@
|
||||||
"systeminformation": "5.21.8",
|
"systeminformation": "5.21.8",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.7",
|
"tsc-alias": "1.8.8",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typeorm": "0.3.17",
|
"typeorm": "0.3.17",
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
|
||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
import type { ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiApp } from '@/models/App.js';
|
import type { MiApp } from '@/models/App.js';
|
||||||
import { concat } from '@/misc/prelude/array.js';
|
import { concat } from '@/misc/prelude/array.js';
|
||||||
|
@ -185,6 +185,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
@Inject(DI.noteThreadMutingsRepository)
|
@Inject(DI.noteThreadMutingsRepository)
|
||||||
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
@ -505,6 +508,20 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
this.saveReply(data.reply, note);
|
this.saveReply(data.reply, note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.reply == null) {
|
||||||
|
this.followingsRepository.findBy({
|
||||||
|
followeeId: user.id,
|
||||||
|
notify: 'normal',
|
||||||
|
}).then(followings => {
|
||||||
|
for (const following of followings) {
|
||||||
|
this.notificationService.createNotification(following.followerId, 'note', {
|
||||||
|
notifierId: user.id,
|
||||||
|
noteId: note.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
|
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
|
||||||
if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
|
if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
|
||||||
if (!user.isBot) this.incRenoteCount(data.renote);
|
if (!user.isBot) this.incRenoteCount(data.renote);
|
||||||
|
|
|
@ -20,7 +20,7 @@ import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||||
import type { UserEntityService } from './UserEntityService.js';
|
import type { UserEntityService } from './UserEntityService.js';
|
||||||
import type { NoteEntityService } from './NoteEntityService.js';
|
import type { NoteEntityService } from './NoteEntityService.js';
|
||||||
|
|
||||||
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
|
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationEntityService implements OnModuleInit {
|
export class NotificationEntityService implements OnModuleInit {
|
||||||
|
|
|
@ -146,15 +146,14 @@ export class UserEntityService implements OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getRelation(me: MiUser['id'], target: MiUser['id']) {
|
public async getRelation(me: MiUser['id'], target: MiUser['id']) {
|
||||||
return awaitAll({
|
const following = await this.followingsRepository.findOneBy({
|
||||||
id: target,
|
|
||||||
isFollowing: this.followingsRepository.count({
|
|
||||||
where: {
|
|
||||||
followerId: me,
|
followerId: me,
|
||||||
followeeId: target,
|
followeeId: target,
|
||||||
},
|
});
|
||||||
take: 1,
|
return awaitAll({
|
||||||
}).then(n => n > 0),
|
id: target,
|
||||||
|
following,
|
||||||
|
isFollowing: following != null,
|
||||||
isFollowed: this.followingsRepository.count({
|
isFollowed: this.followingsRepository.count({
|
||||||
where: {
|
where: {
|
||||||
followerId: target,
|
followerId: target,
|
||||||
|
@ -384,6 +383,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
birthday: profile!.birthday,
|
birthday: profile!.birthday,
|
||||||
lang: profile!.lang,
|
lang: profile!.lang,
|
||||||
fields: profile!.fields,
|
fields: profile!.fields,
|
||||||
|
verifiedLinks: profile!.verifiedLinks,
|
||||||
followersCount: followersCount ?? 0,
|
followersCount: followersCount ?? 0,
|
||||||
followingCount: followingCount ?? 0,
|
followingCount: followingCount ?? 0,
|
||||||
notesCount: user.notesCount,
|
notesCount: user.notesCount,
|
||||||
|
@ -485,6 +485,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
isBlocked: relation.isBlocked,
|
isBlocked: relation.isBlocked,
|
||||||
isMuted: relation.isMuted,
|
isMuted: relation.isMuted,
|
||||||
isRenoteMuted: relation.isRenoteMuted,
|
isRenoteMuted: relation.isRenoteMuted,
|
||||||
|
notify: relation.following?.notify ?? 'none',
|
||||||
} : {}),
|
} : {}),
|
||||||
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,13 @@ export class MiFollowing {
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public follower: MiUser | null;
|
public follower: MiUser | null;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 32,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public notify: 'normal' | null;
|
||||||
|
|
||||||
//#region Denormalized fields
|
//#region Denormalized fields
|
||||||
@Index()
|
@Index()
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
|
|
|
@ -22,18 +22,6 @@ export type MiNotification = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知の種類。
|
* 通知の種類。
|
||||||
* follow - フォローされた
|
|
||||||
* mention - 投稿で自分が言及された
|
|
||||||
* reply - 投稿に返信された
|
|
||||||
* renote - 投稿がRenoteされた
|
|
||||||
* quote - 投稿が引用Renoteされた
|
|
||||||
* reaction - 投稿にリアクションされた
|
|
||||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
|
||||||
* receiveFollowRequest - フォローリクエストされた
|
|
||||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
|
||||||
* achievementEarned - 実績を獲得
|
|
||||||
* app - アプリ通知
|
|
||||||
* test - テスト通知(サーバー側)
|
|
||||||
*/
|
*/
|
||||||
type: typeof notificationTypes[number];
|
type: typeof notificationTypes[number];
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,12 @@ export class MiUserProfile {
|
||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
array: true,
|
||||||
|
default: '{}',
|
||||||
|
})
|
||||||
|
public verifiedLinks: string[];
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 32, nullable: true,
|
length: 32, nullable: true,
|
||||||
})
|
})
|
||||||
|
|
|
@ -169,6 +169,15 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
verifiedLinks: {
|
||||||
|
type: 'array',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
format: 'url',
|
||||||
|
},
|
||||||
|
},
|
||||||
followersCount: {
|
followersCount: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
@ -264,6 +273,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
},
|
},
|
||||||
|
notify: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -160,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js';
|
||||||
import * as ep___federation_stats from './endpoints/federation/stats.js';
|
import * as ep___federation_stats from './endpoints/federation/stats.js';
|
||||||
import * as ep___following_create from './endpoints/following/create.js';
|
import * as ep___following_create from './endpoints/following/create.js';
|
||||||
import * as ep___following_delete from './endpoints/following/delete.js';
|
import * as ep___following_delete from './endpoints/following/delete.js';
|
||||||
|
import * as ep___following_update from './endpoints/following/update.js';
|
||||||
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
|
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
|
||||||
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
|
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
|
||||||
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
|
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
|
||||||
|
@ -508,6 +509,7 @@ const $federation_users: Provider = { provide: 'ep:federation/users', useClass:
|
||||||
const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default };
|
const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default };
|
||||||
const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default };
|
const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default };
|
||||||
const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default };
|
const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default };
|
||||||
|
const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default };
|
||||||
const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default };
|
const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default };
|
||||||
const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
|
const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
|
||||||
const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
|
const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
|
||||||
|
@ -860,6 +862,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$federation_stats,
|
$federation_stats,
|
||||||
$following_create,
|
$following_create,
|
||||||
$following_delete,
|
$following_delete,
|
||||||
|
$following_update,
|
||||||
$following_invalidate,
|
$following_invalidate,
|
||||||
$following_requests_accept,
|
$following_requests_accept,
|
||||||
$following_requests_cancel,
|
$following_requests_cancel,
|
||||||
|
@ -1206,6 +1209,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$federation_stats,
|
$federation_stats,
|
||||||
$following_create,
|
$following_create,
|
||||||
$following_delete,
|
$following_delete,
|
||||||
|
$following_update,
|
||||||
$following_invalidate,
|
$following_invalidate,
|
||||||
$following_requests_accept,
|
$following_requests_accept,
|
||||||
$following_requests_cancel,
|
$following_requests_cancel,
|
||||||
|
|
|
@ -160,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js';
|
||||||
import * as ep___federation_stats from './endpoints/federation/stats.js';
|
import * as ep___federation_stats from './endpoints/federation/stats.js';
|
||||||
import * as ep___following_create from './endpoints/following/create.js';
|
import * as ep___following_create from './endpoints/following/create.js';
|
||||||
import * as ep___following_delete from './endpoints/following/delete.js';
|
import * as ep___following_delete from './endpoints/following/delete.js';
|
||||||
|
import * as ep___following_update from './endpoints/following/update.js';
|
||||||
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
|
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
|
||||||
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
|
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
|
||||||
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
|
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
|
||||||
|
@ -506,6 +507,7 @@ const eps = [
|
||||||
['federation/stats', ep___federation_stats],
|
['federation/stats', ep___federation_stats],
|
||||||
['following/create', ep___following_create],
|
['following/create', ep___following_create],
|
||||||
['following/delete', ep___following_delete],
|
['following/delete', ep___following_delete],
|
||||||
|
['following/update', ep___following_update],
|
||||||
['following/invalidate', ep___following_invalidate],
|
['following/invalidate', ep___following_invalidate],
|
||||||
['following/requests/accept', ep___following_requests_accept],
|
['following/requests/accept', ep___following_requests_accept],
|
||||||
['following/requests/cancel', ep___following_requests_cancel],
|
['following/requests/cancel', ep___following_requests_cancel],
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const meta = {
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1hour'),
|
duration: ms('1hour'),
|
||||||
max: 50,
|
max: 100,
|
||||||
},
|
},
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
|
@ -29,7 +29,7 @@ export const meta = {
|
||||||
noSuchUser: {
|
noSuchUser: {
|
||||||
message: 'No such user.',
|
message: 'No such user.',
|
||||||
code: 'NO_SUCH_USER',
|
code: 'NO_SUCH_USER',
|
||||||
id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8',
|
id: 'b77e6ae6-a3e5-40da-9cc8-c240115479cc',
|
||||||
},
|
},
|
||||||
|
|
||||||
followerIsYourself: {
|
followerIsYourself: {
|
||||||
|
@ -41,7 +41,7 @@ export const meta = {
|
||||||
notFollowing: {
|
notFollowing: {
|
||||||
message: 'The other use is not following you.',
|
message: 'The other use is not following you.',
|
||||||
code: 'NOT_FOLLOWING',
|
code: 'NOT_FOLLOWING',
|
||||||
id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09',
|
id: '918faac3-074f-41ae-9c43-ed5d2946770d',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ms from 'ms';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { FollowingsRepository } from '@/models/_.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['following', 'users'],
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: ms('1hour'),
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:following',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: '14318698-f67e-492a-99da-5353a5ac52be',
|
||||||
|
},
|
||||||
|
|
||||||
|
followeeIsYourself: {
|
||||||
|
message: 'Followee is yourself.',
|
||||||
|
code: 'FOLLOWEE_IS_YOURSELF',
|
||||||
|
id: '4c4cbaf9-962a-463b-8418-a5e365dbf2eb',
|
||||||
|
},
|
||||||
|
|
||||||
|
notFollowing: {
|
||||||
|
message: 'You are not following that user.',
|
||||||
|
code: 'NOT_FOLLOWING',
|
||||||
|
id: 'b8dc75cf-1cb5-46c9-b14b-5f1ffbd782c9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'UserLite',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
notify: { type: 'string', enum: ['normal', 'none'] },
|
||||||
|
},
|
||||||
|
required: ['userId', 'notify'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
|
private getterService: GetterService,
|
||||||
|
private userFollowingService: UserFollowingService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const follower = me;
|
||||||
|
|
||||||
|
// Check if the follower is yourself
|
||||||
|
if (me.id === ps.userId) {
|
||||||
|
throw new ApiError(meta.errors.followeeIsYourself);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get followee
|
||||||
|
const followee = await this.getterService.getUser(ps.userId).catch(err => {
|
||||||
|
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check not following
|
||||||
|
const exist = await this.followingsRepository.findOneBy({
|
||||||
|
followerId: follower.id,
|
||||||
|
followeeId: followee.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exist == null) {
|
||||||
|
throw new ApiError(meta.errors.notFollowing);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.followingsRepository.update({
|
||||||
|
id: exist.id,
|
||||||
|
}, {
|
||||||
|
notify: ps.notify === 'none' ? null : ps.notify,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.userEntityService.pack(follower.id, me);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,11 +6,13 @@
|
||||||
import RE2 from 're2';
|
import RE2 from 're2';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import ms from 'ms';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
|
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
|
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
|
||||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
import { notificationTypes } from '@/types.js';
|
import { notificationTypes } from '@/types.js';
|
||||||
|
@ -27,6 +29,9 @@ import { RoleService } from '@/core/RoleService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import { safeForSql } from '@/misc/safe-for-sql.js';
|
||||||
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
@ -37,6 +42,11 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: ms('1hour'),
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchAvatar: {
|
noSuchAvatar: {
|
||||||
message: 'No such avatar file.',
|
message: 'No such avatar file.',
|
||||||
|
@ -173,6 +183,9 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -195,9 +208,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private hashtagService: HashtagService,
|
private hashtagService: HashtagService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
|
private httpRequestService: HttpRequestService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, _user, token) => {
|
super(meta, paramDef, async (ps, _user, token) => {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: _user.id });
|
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
|
||||||
const isSecure = token == null;
|
const isSecure = token == null;
|
||||||
|
|
||||||
const updates = {} as Partial<MiUser>;
|
const updates = {} as Partial<MiUser>;
|
||||||
|
@ -296,9 +310,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
if (ps.fields) {
|
if (ps.fields) {
|
||||||
profileUpdates.fields = ps.fields
|
profileUpdates.fields = ps.fields
|
||||||
.filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '')
|
.filter(x => typeof x.name === 'string' && x.name.trim() !== '' && typeof x.value === 'string' && x.value.trim() !== '')
|
||||||
.map(x => {
|
.map(x => {
|
||||||
return { name: x.name, value: x.value };
|
return { name: x.name.trim(), value: x.value.trim() };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,7 +378,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (Object.keys(updates).includes('alsoKnownAs')) {
|
if (Object.keys(updates).includes('alsoKnownAs')) {
|
||||||
this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates });
|
this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates });
|
||||||
}
|
}
|
||||||
if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates);
|
|
||||||
|
await this.userProfilesRepository.update(user.id, {
|
||||||
|
...profileUpdates,
|
||||||
|
verifiedLinks: [],
|
||||||
|
});
|
||||||
|
|
||||||
const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
|
const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
|
||||||
detail: true,
|
detail: true,
|
||||||
|
@ -386,7 +404,34 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
// フォロワーにUpdateを配信
|
// フォロワーにUpdateを配信
|
||||||
this.accountUpdateService.publishToFollowers(user.id);
|
this.accountUpdateService.publishToFollowers(user.id);
|
||||||
|
|
||||||
|
const urls = updatedProfile.fields.filter(x => x.value.startsWith('https://'));
|
||||||
|
for (const url of urls) {
|
||||||
|
this.verifyLink(url.value, user);
|
||||||
|
}
|
||||||
|
|
||||||
return iObj;
|
return iObj;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async verifyLink(url: string, user: MiLocalUser) {
|
||||||
|
if (!safeForSql(url)) return;
|
||||||
|
|
||||||
|
const html = await this.httpRequestService.getHtml(url);
|
||||||
|
|
||||||
|
const { window } = new JSDOM(html);
|
||||||
|
const doc = window.document;
|
||||||
|
|
||||||
|
const myLink = `${this.config.url}/@${user.username}`;
|
||||||
|
|
||||||
|
const includesMyLink = Array.from(doc.getElementsByTagName('a')).some(a => a.href === myLink);
|
||||||
|
|
||||||
|
if (includesMyLink) {
|
||||||
|
await this.userProfilesRepository.createQueryBuilder('profile').update()
|
||||||
|
.where('userId = :userId', { userId: user.id })
|
||||||
|
.set({
|
||||||
|
verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,22 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const;
|
/**
|
||||||
|
* note - 通知オンにしているユーザーが投稿した
|
||||||
|
* follow - フォローされた
|
||||||
|
* mention - 投稿で自分が言及された
|
||||||
|
* reply - 投稿に返信された
|
||||||
|
* renote - 投稿がRenoteされた
|
||||||
|
* quote - 投稿が引用Renoteされた
|
||||||
|
* reaction - 投稿にリアクションされた
|
||||||
|
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||||
|
* receiveFollowRequest - フォローリクエストされた
|
||||||
|
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||||
|
* achievementEarned - 実績を獲得
|
||||||
|
* app - アプリ通知
|
||||||
|
* test - テスト通知(サーバー側)
|
||||||
|
*/
|
||||||
|
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const;
|
||||||
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
||||||
|
|
||||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||||
|
|
|
@ -102,6 +102,7 @@ describe('ユーザー', () => {
|
||||||
birthday: user.birthday,
|
birthday: user.birthday,
|
||||||
lang: user.lang,
|
lang: user.lang,
|
||||||
fields: user.fields,
|
fields: user.fields,
|
||||||
|
verifiedLinks: user.verifiedLinks,
|
||||||
followersCount: user.followersCount,
|
followersCount: user.followersCount,
|
||||||
followingCount: user.followingCount,
|
followingCount: user.followingCount,
|
||||||
notesCount: user.notesCount,
|
notesCount: user.notesCount,
|
||||||
|
@ -369,6 +370,7 @@ describe('ユーザー', () => {
|
||||||
assert.strictEqual(response.birthday, null);
|
assert.strictEqual(response.birthday, null);
|
||||||
assert.strictEqual(response.lang, null);
|
assert.strictEqual(response.lang, null);
|
||||||
assert.deepStrictEqual(response.fields, []);
|
assert.deepStrictEqual(response.fields, []);
|
||||||
|
assert.deepStrictEqual(response.verifiedLinks, []);
|
||||||
assert.strictEqual(response.followersCount, 0);
|
assert.strictEqual(response.followersCount, 0);
|
||||||
assert.strictEqual(response.followingCount, 0);
|
assert.strictEqual(response.followingCount, 0);
|
||||||
assert.strictEqual(response.notesCount, 0);
|
assert.strictEqual(response.notesCount, 0);
|
||||||
|
@ -492,7 +494,7 @@ describe('ユーザー', () => {
|
||||||
{ parameters: (): object => ({ mutedWords: [] }) },
|
{ parameters: (): object => ({ mutedWords: [] }) },
|
||||||
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
|
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
|
||||||
{ parameters: (): object => ({ mutedInstances: [] }) },
|
{ parameters: (): object => ({ mutedInstances: [] }) },
|
||||||
{ parameters: (): object => ({ mutingNotificationTypes: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
|
{ parameters: (): object => ({ mutingNotificationTypes: ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
|
||||||
{ parameters: (): object => ({ mutingNotificationTypes: [] }) },
|
{ parameters: (): object => ({ mutingNotificationTypes: [] }) },
|
||||||
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
|
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
|
||||||
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
|
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
|
||||||
|
|
|
@ -89,6 +89,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
|
||||||
value: 'https://misskey-hub.net',
|
value: 'https://misskey-hub.net',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
verifiedLinks: [],
|
||||||
followersCount: 1024,
|
followersCount: 1024,
|
||||||
followingCount: 16,
|
followingCount: 16,
|
||||||
hasPendingFollowRequestFromYou: false,
|
hasPendingFollowRequestFromYou: false,
|
||||||
|
@ -119,6 +120,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
uri: null,
|
uri: null,
|
||||||
url: null,
|
url: null,
|
||||||
|
notify: 'none',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,13 +59,13 @@
|
||||||
"querystring": "0.2.1",
|
"querystring": "0.2.1",
|
||||||
"rollup": "3.29.2",
|
"rollup": "3.29.2",
|
||||||
"sanitize-html": "2.11.0",
|
"sanitize-html": "2.11.0",
|
||||||
"sass": "1.67.0",
|
"sass": "1.68.0",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.156.1",
|
"three": "0.156.1",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tsc-alias": "1.8.7",
|
"tsc-alias": "1.8.8",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
|
@ -77,24 +77,24 @@
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-actions": "7.4.2",
|
"@storybook/addon-actions": "7.4.3",
|
||||||
"@storybook/addon-essentials": "7.4.2",
|
"@storybook/addon-essentials": "7.4.3",
|
||||||
"@storybook/addon-interactions": "7.4.2",
|
"@storybook/addon-interactions": "7.4.3",
|
||||||
"@storybook/addon-links": "7.4.2",
|
"@storybook/addon-links": "7.4.3",
|
||||||
"@storybook/addon-storysource": "7.4.2",
|
"@storybook/addon-storysource": "7.4.3",
|
||||||
"@storybook/addons": "7.4.2",
|
"@storybook/addons": "7.4.3",
|
||||||
"@storybook/blocks": "7.4.2",
|
"@storybook/blocks": "7.4.3",
|
||||||
"@storybook/core-events": "7.4.2",
|
"@storybook/core-events": "7.4.3",
|
||||||
"@storybook/jest": "0.2.2",
|
"@storybook/jest": "0.2.2",
|
||||||
"@storybook/manager-api": "7.4.2",
|
"@storybook/manager-api": "7.4.3",
|
||||||
"@storybook/preview-api": "7.4.2",
|
"@storybook/preview-api": "7.4.3",
|
||||||
"@storybook/react": "7.4.2",
|
"@storybook/react": "7.4.3",
|
||||||
"@storybook/react-vite": "7.4.2",
|
"@storybook/react-vite": "7.4.3",
|
||||||
"@storybook/testing-library": "0.2.1",
|
"@storybook/testing-library": "0.2.1",
|
||||||
"@storybook/theming": "7.4.2",
|
"@storybook/theming": "7.4.3",
|
||||||
"@storybook/types": "7.4.2",
|
"@storybook/types": "7.4.3",
|
||||||
"@storybook/vue3": "7.4.2",
|
"@storybook/vue3": "7.4.3",
|
||||||
"@storybook/vue3-vite": "7.4.2",
|
"@storybook/vue3-vite": "7.4.3",
|
||||||
"@testing-library/vue": "7.0.0",
|
"@testing-library/vue": "7.0.0",
|
||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/estree": "1.0.1",
|
"@types/estree": "1.0.1",
|
||||||
|
|
|
@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div ref="elRef" :class="$style.root">
|
<div ref="elRef" :class="$style.root">
|
||||||
<div :class="$style.head">
|
<div :class="$style.head">
|
||||||
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
|
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
|
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
|
||||||
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
|
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
|
||||||
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
|
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
|
||||||
|
@ -47,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.tail">
|
<div :class="$style.tail">
|
||||||
<header :class="$style.header">
|
<header :class="$style.header">
|
||||||
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
||||||
|
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: {{ notification.note.user.name ?? notification.note.user.username }}</span>
|
||||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||||
|
@ -73,6 +75,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkA v-else-if="notification.type === 'quote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA v-else-if="notification.type === 'quote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
<MkA v-else-if="notification.type === 'note'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
|
</MkA>
|
||||||
<MkA v-else-if="notification.type === 'pollEnded'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA v-else-if="notification.type === 'pollEnded'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
|
|
|
@ -170,6 +170,10 @@ useTooltip(buttonEl, async (showing) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
.count {
|
.count {
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
line-height: 42px;
|
line-height: 42px;
|
||||||
|
|
|
@ -54,7 +54,7 @@ https://github.com/sindresorhus/file-type/blob/main/core.js
|
||||||
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
|
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const;
|
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const;
|
||||||
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
||||||
|
|
||||||
export const ROLE_POLICIES = [
|
export const ROLE_POLICIES = [
|
||||||
|
|
|
@ -76,6 +76,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sortable>
|
</Sortable>
|
||||||
|
|
||||||
|
<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
||||||
|
@ -119,6 +121,7 @@ import { langmap } from '@/scripts/langmap.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||||
|
|
||||||
|
|
|
@ -101,6 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="value">
|
<dd class="value">
|
||||||
<Mfm :text="field.value" :author="user" :i="$i" :colored="false"/>
|
<Mfm :text="field.value" :author="user" :i="$i" :colored="false"/>
|
||||||
|
<i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
@ -674,4 +675,9 @@ onUnmounted(() => {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.verifiedLink {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -80,6 +80,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleNotify() {
|
||||||
|
os.apiWithDialog('following/update', {
|
||||||
|
userId: user.id,
|
||||||
|
notify: user.notify === 'normal' ? 'none' : 'normal',
|
||||||
|
}).then(() => {
|
||||||
|
user.notify = user.notify === 'normal' ? 'none' : 'normal';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function reportAbuse() {
|
function reportAbuse() {
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
|
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
|
||||||
user: user,
|
user: user,
|
||||||
|
@ -270,6 +279,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため
|
||||||
|
//if (user.isFollowing) {
|
||||||
|
menu = menu.concat([{
|
||||||
|
icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off',
|
||||||
|
text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes,
|
||||||
|
action: toggleNotify,
|
||||||
|
}]);
|
||||||
|
//}
|
||||||
|
|
||||||
menu = menu.concat([null, {
|
menu = menu.concat([null, {
|
||||||
icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
|
icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
|
||||||
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
|
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
|
||||||
|
|
|
@ -2609,7 +2609,12 @@ type Notification_2 = {
|
||||||
userId: User['id'];
|
userId: User['id'];
|
||||||
note: Note;
|
note: Note;
|
||||||
} | {
|
} | {
|
||||||
type: 'pollVote';
|
type: 'note';
|
||||||
|
user: User;
|
||||||
|
userId: User['id'];
|
||||||
|
note: Note;
|
||||||
|
} | {
|
||||||
|
type: 'pollEnded';
|
||||||
user: User;
|
user: User;
|
||||||
userId: User['id'];
|
userId: User['id'];
|
||||||
note: Note;
|
note: Note;
|
||||||
|
@ -2640,7 +2645,7 @@ type Notification_2 = {
|
||||||
});
|
});
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const notificationTypes: readonly ["follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app"];
|
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app"];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type OriginType = 'combined' | 'local' | 'remote';
|
type OriginType = 'combined' | 'local' | 'remote';
|
||||||
|
@ -2778,6 +2783,7 @@ type UserDetailed = UserLite & {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
|
verifiedLinks: string[];
|
||||||
followersCount: number;
|
followersCount: number;
|
||||||
followingCount: number;
|
followingCount: number;
|
||||||
hasPendingFollowRequestFromYou: boolean;
|
hasPendingFollowRequestFromYou: boolean;
|
||||||
|
@ -2809,6 +2815,7 @@ type UserDetailed = UserLite & {
|
||||||
updatedAt: DateString | null;
|
updatedAt: DateString | null;
|
||||||
uri: string | null;
|
uri: string | null;
|
||||||
url: string | null;
|
url: string | null;
|
||||||
|
notify: 'normal' | 'none';
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
|
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
|
||||||
|
|
||||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ export type UserDetailed = UserLite & {
|
||||||
description: string | null;
|
description: string | null;
|
||||||
ffVisibility: 'public' | 'followers' | 'private';
|
ffVisibility: 'public' | 'followers' | 'private';
|
||||||
fields: {name: string; value: string}[];
|
fields: {name: string; value: string}[];
|
||||||
|
verifiedLinks: string[];
|
||||||
followersCount: number;
|
followersCount: number;
|
||||||
followingCount: number;
|
followingCount: number;
|
||||||
hasPendingFollowRequestFromYou: boolean;
|
hasPendingFollowRequestFromYou: boolean;
|
||||||
|
@ -69,6 +70,7 @@ export type UserDetailed = UserLite & {
|
||||||
updatedAt: DateString | null;
|
updatedAt: DateString | null;
|
||||||
uri: string | null;
|
uri: string | null;
|
||||||
url: string | null;
|
url: string | null;
|
||||||
|
notify: 'normal' | 'none';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserGroup = TODO;
|
export type UserGroup = TODO;
|
||||||
|
@ -232,7 +234,12 @@ export type Notification = {
|
||||||
userId: User['id'];
|
userId: User['id'];
|
||||||
note: Note;
|
note: Note;
|
||||||
} | {
|
} | {
|
||||||
type: 'pollVote';
|
type: 'note';
|
||||||
|
user: User;
|
||||||
|
userId: User['id'];
|
||||||
|
note: Note;
|
||||||
|
} | {
|
||||||
|
type: 'pollEnded';
|
||||||
user: User;
|
user: User;
|
||||||
userId: User['id'];
|
userId: User['id'];
|
||||||
note: Note;
|
note: Note;
|
||||||
|
|
|
@ -134,6 +134,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
],
|
],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
case 'note':
|
||||||
|
return [t('_notification.newNote') + ': ' + getUserName(data.body.user), {
|
||||||
|
body: data.body.note.text ?? '',
|
||||||
|
icon: data.body.user.avatarUrl,
|
||||||
|
data,
|
||||||
|
}];
|
||||||
|
|
||||||
case 'reaction': {
|
case 'reaction': {
|
||||||
let reaction = data.body.reaction;
|
let reaction = data.body.reaction;
|
||||||
let badge: string | undefined;
|
let badge: string | undefined;
|
||||||
|
|
802
pnpm-lock.yaml
802
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue