diff --git a/CHANGELOG.md b/CHANGELOG.md index 60eb6e15c6..702d77f64a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ unrekleassaf ### ✨Improvements * タイムラインなどを遡っているときは新しいアイテムが来てもスクロールしないように * 表示言語を切り替えられるように +* グループに招待されたときの通知を追加 ### 🐛Fixes * リストを追加するとエラーが出る問題を修正 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index e8c0b69575..d6cf376083 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -385,6 +385,7 @@ signinWith: "{x}でログイン" tapSecurityKey: "セキュリティーキーにタッチ" or: "もしくは" uiLanguage: "UIの表示言語" +groupInvited: "グループに招待されました" _ago: unknown: "謎" diff --git a/migration/1581526429287-user-group-invitation.ts b/migration/1581526429287-user-group-invitation.ts new file mode 100644 index 0000000000..26ea54e0b8 --- /dev/null +++ b/migration/1581526429287-user-group-invitation.ts @@ -0,0 +1,38 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class userGroupInvitation1581526429287 implements MigrationInterface { + name = 'userGroupInvitation1581526429287' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "user_group_invitation" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_160c63ec02bf23f6a5c5e8140d6" PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE INDEX "IDX_bfbc6305547539369fe73eb144" ON "user_group_invitation" ("userId") `, undefined); + await queryRunner.query(`CREATE INDEX "IDX_5cc8c468090e129857e9fecce5" ON "user_group_invitation" ("userGroupId") `, undefined); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e9793f65f504e5a31fbaedbf2f" ON "user_group_invitation" ("userId", "userGroupId") `, undefined); + await queryRunner.query(`ALTER TABLE "notification" ADD "userGroupInvitationId" character varying(32)`, undefined); + await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`, undefined); + await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited')`, undefined); + await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum" USING "type"::"text"::"notification_type_enum"`, undefined); + await queryRunner.query(`DROP TYPE "notification_type_enum_old"`, undefined); + await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS 'The type of the Notification.'`, undefined); + await queryRunner.query(`ALTER TABLE "user_group_invitation" ADD CONSTRAINT "FK_bfbc6305547539369fe73eb144a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + await queryRunner.query(`ALTER TABLE "user_group_invitation" ADD CONSTRAINT "FK_5cc8c468090e129857e9fecce5a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_8fe87814e978053a53b1beb7e98" FOREIGN KEY ("userGroupInvitationId") REFERENCES "user_group_invitation"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_8fe87814e978053a53b1beb7e98"`, undefined); + await queryRunner.query(`ALTER TABLE "user_group_invitation" DROP CONSTRAINT "FK_5cc8c468090e129857e9fecce5a"`, undefined); + await queryRunner.query(`ALTER TABLE "user_group_invitation" DROP CONSTRAINT "FK_bfbc6305547539369fe73eb144a"`, undefined); + await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS ''`, undefined); + await queryRunner.query(`CREATE TYPE "notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted')`, undefined); + await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum_old" USING "type"::"text"::"notification_type_enum_old"`, undefined); + await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined); + await queryRunner.query(`ALTER TYPE "notification_type_enum_old" RENAME TO "notification_type_enum"`, undefined); + await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "userGroupInvitationId"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_e9793f65f504e5a31fbaedbf2f"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_5cc8c468090e129857e9fecce5"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_bfbc6305547539369fe73eb144"`, undefined); + await queryRunner.query(`DROP TABLE "user_group_invitation"`, undefined); + } + +} diff --git a/src/client/components/notification.vue b/src/client/components/notification.vue index e2a220a074..50aff29dd7 100644 --- a/src/client/components/notification.vue +++ b/src/client/components/notification.vue @@ -6,6 +6,7 @@ + @@ -40,13 +41,14 @@ {{ $t('youGotNewFollower') }}
{{ $t('followRequestAccepted') }} {{ $t('receiveFollowRequest') }}
|
+ {{ $t('groupInvited') }}: {{ notification.invitation.group.name }}
|
@@ -149,7 +164,7 @@ export default Vue.extend({ height: 100%; } - &.follow, &.followRequestAccepted, &.receiveFollowRequest { + &.follow, &.followRequestAccepted, &.receiveFollowRequest, &.groupInvited { padding: 3px; background: #36aed2; } diff --git a/src/client/pages/my-groups/index.vue b/src/client/pages/my-groups/index.vue index c41139164f..9b3acbb3eb 100644 --- a/src/client/pages/my-groups/index.vue +++ b/src/client/pages/my-groups/index.vue @@ -17,13 +17,13 @@ - -
-
{{ invite.group.name }}
-
+ +
+
{{ invitation.group.name }}
+
@@ -73,7 +73,7 @@ export default Vue.extend({ endpoint: 'users/groups/joined', limit: 10, }, - invitePagination: { + invitationPagination: { endpoint: 'i/user-group-invites', limit: 10, }, @@ -95,23 +95,23 @@ export default Vue.extend({ iconOnly: true, autoClose: true }); }, - acceptInvite(invite) { + acceptInvite(invitation) { this.$root.api('users/groups/invitations/accept', { - inviteId: invite.id + invitationId: invitation.id }).then(() => { this.$root.dialog({ type: 'success', iconOnly: true, autoClose: true }); - this.$refs.invites.reload(); + this.$refs.invitations.reload(); this.$refs.joined.reload(); }); }, - rejectInvite(invite) { + rejectInvite(invitation) { this.$root.api('users/groups/invitations/reject', { - inviteId: invite.id + invitationId: invitation.id }).then(() => { - this.$refs.invites.reload(); + this.$refs.invitations.reload(); }); } } diff --git a/src/db/postgre.ts b/src/db/postgre.ts index 3e12db3a07..38c7794402 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -26,7 +26,7 @@ import { UserList } from '../models/entities/user-list'; import { UserListJoining } from '../models/entities/user-list-joining'; import { UserGroup } from '../models/entities/user-group'; import { UserGroupJoining } from '../models/entities/user-group-joining'; -import { UserGroupInvite } from '../models/entities/user-group-invite'; +import { UserGroupInvitation } from '../models/entities/user-group-invitation'; import { Hashtag } from '../models/entities/hashtag'; import { NoteFavorite } from '../models/entities/note-favorite'; import { AbuseUserReport } from '../models/entities/abuse-user-report'; @@ -106,7 +106,7 @@ export const entities = [ UserListJoining, UserGroup, UserGroupJoining, - UserGroupInvite, + UserGroupInvitation, UserNotePining, UserSecurityKey, UsedUsername, diff --git a/src/models/entities/notification.ts b/src/models/entities/notification.ts index e359640e82..cd3fe9b01e 100644 --- a/src/models/entities/notification.ts +++ b/src/models/entities/notification.ts @@ -3,6 +3,7 @@ import { User } from './user'; import { id } from '../id'; import { Note } from './note'; import { FollowRequest } from './follow-request'; +import { UserGroupInvitation } from './user-group-invitation'; @Entity() export class Notification { @@ -57,12 +58,13 @@ export class Notification { * pollVote - (自分または自分がWatchしている)投稿の投票に投票された * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された + * groupInvited - グループに招待された */ @Column('enum', { - enum: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted'], + enum: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited'], comment: 'The type of the Notification.' }) - public type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollVote' | 'receiveFollowRequest' | 'followRequestAccepted'; + public type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollVote' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited'; /** * 通知が読まれたかどうか @@ -97,6 +99,18 @@ export class Notification { @JoinColumn() public followRequest: FollowRequest | null; + @Column({ + ...id(), + nullable: true + }) + public userGroupInvitationId: UserGroupInvitation['id'] | null; + + @ManyToOne(type => UserGroupInvitation, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public userGroupInvitation: UserGroupInvitation | null; + @Column('varchar', { length: 128, nullable: true }) diff --git a/src/models/entities/user-group-invite.ts b/src/models/entities/user-group-invitation.ts similarity index 89% rename from src/models/entities/user-group-invite.ts rename to src/models/entities/user-group-invitation.ts index 2adf2c024e..6fe8f20134 100644 --- a/src/models/entities/user-group-invite.ts +++ b/src/models/entities/user-group-invitation.ts @@ -5,12 +5,12 @@ import { id } from '../id'; @Entity() @Index(['userId', 'userGroupId'], { unique: true }) -export class UserGroupInvite { +export class UserGroupInvitation { @PrimaryColumn(id()) public id: string; @Column('timestamp with time zone', { - comment: 'The created date of the UserGroupInvite.' + comment: 'The created date of the UserGroupInvitation.' }) public createdAt: Date; diff --git a/src/models/index.ts b/src/models/index.ts index 15a5c5470d..ea8fa6f911 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -24,7 +24,7 @@ import { UserListRepository } from './repositories/user-list'; import { UserListJoining } from './entities/user-list-joining'; import { UserGroupRepository } from './repositories/user-group'; import { UserGroupJoining } from './entities/user-group-joining'; -import { UserGroupInviteRepository } from './repositories/user-group-invite'; +import { UserGroupInvitationRepository } from './repositories/user-group-invitation'; import { FollowRequestRepository } from './repositories/follow-request'; import { MutingRepository } from './repositories/muting'; import { BlockingRepository } from './repositories/blocking'; @@ -71,7 +71,7 @@ export const UserLists = getCustomRepository(UserListRepository); export const UserListJoinings = getRepository(UserListJoining); export const UserGroups = getCustomRepository(UserGroupRepository); export const UserGroupJoinings = getRepository(UserGroupJoining); -export const UserGroupInvites = getCustomRepository(UserGroupInviteRepository); +export const UserGroupInvitations = getCustomRepository(UserGroupInvitationRepository); export const UserNotePinings = getRepository(UserNotePining); export const UsedUsernames = getRepository(UsedUsername); export const Followings = getCustomRepository(FollowingRepository); diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts index 6407c19d4c..f020714f80 100644 --- a/src/models/repositories/notification.ts +++ b/src/models/repositories/notification.ts @@ -1,5 +1,5 @@ import { EntityRepository, Repository } from 'typeorm'; -import { Users, Notes } from '..'; +import { Users, Notes, UserGroupInvitations } from '..'; import { Notification } from '../entities/notification'; import { ensure } from '../../prelude/ensure'; import { awaitAll } from '../../prelude/await-all'; @@ -39,7 +39,10 @@ export class NotificationRepository extends Repository { ...(notification.type === 'pollVote' ? { note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId), choice: notification.choice - } : {}) + } : {}), + ...(notification.type === 'groupInvited' ? { + invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!), + } : {}), }); } diff --git a/src/models/repositories/user-group-invitation.ts b/src/models/repositories/user-group-invitation.ts new file mode 100644 index 0000000000..0d3ad525c3 --- /dev/null +++ b/src/models/repositories/user-group-invitation.ts @@ -0,0 +1,24 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { UserGroupInvitation } from '../entities/user-group-invitation'; +import { UserGroups } from '..'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(UserGroupInvitation) +export class UserGroupInvitationRepository extends Repository { + public async pack( + src: UserGroupInvitation['id'] | UserGroupInvitation, + ) { + const invitation = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return { + id: invitation.id, + group: await UserGroups.pack(invitation.userGroup || invitation.userGroupId), + }; + } + + public packMany( + invitations: any[], + ) { + return Promise.all(invitations.map(x => this.pack(x))); + } +} diff --git a/src/models/repositories/user-group-invite.ts b/src/models/repositories/user-group-invite.ts deleted file mode 100644 index 1d4c2aa15f..0000000000 --- a/src/models/repositories/user-group-invite.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { EntityRepository, Repository } from 'typeorm'; -import { UserGroupInvite } from '../entities/user-group-invite'; -import { UserGroups } from '..'; -import { ensure } from '../../prelude/ensure'; - -@EntityRepository(UserGroupInvite) -export class UserGroupInviteRepository extends Repository { - public async pack( - src: UserGroupInvite['id'] | UserGroupInvite, - ) { - const invite = typeof src === 'object' ? src : await this.findOne(src).then(ensure); - - return { - id: invite.id, - group: await UserGroups.pack(invite.userGroup || invite.userGroupId), - }; - } - - public packMany( - invites: any[], - ) { - return Promise.all(invites.map(x => this.pack(x))); - } -} diff --git a/src/server/api/endpoints/i/user-group-invites.ts b/src/server/api/endpoints/i/user-group-invites.ts index 9d07fa31a4..37eaba06d2 100644 --- a/src/server/api/endpoints/i/user-group-invites.ts +++ b/src/server/api/endpoints/i/user-group-invites.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import { UserGroupInvites } from '../../../../models'; +import { UserGroupInvitations } from '../../../../models'; import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { @@ -33,13 +33,13 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const query = makePaginationQuery(UserGroupInvites.createQueryBuilder('invite'), ps.sinceId, ps.untilId) - .andWhere(`invite.userId = :meId`, { meId: user.id }) - .leftJoinAndSelect('invite.userGroup', 'user_group'); + const query = makePaginationQuery(UserGroupInvitations.createQueryBuilder('invitation'), ps.sinceId, ps.untilId) + .andWhere(`invitation.userId = :meId`, { meId: user.id }) + .leftJoinAndSelect('invitation.userGroup', 'user_group'); - const invites = await query + const invitations = await query .take(ps.limit!) .getMany(); - return await UserGroupInvites.packMany(invites); + return await UserGroupInvitations.packMany(invitations); }); diff --git a/src/server/api/endpoints/users/groups/invitations/accept.ts b/src/server/api/endpoints/users/groups/invitations/accept.ts index 33779dd349..cb5cb7bd05 100644 --- a/src/server/api/endpoints/users/groups/invitations/accept.ts +++ b/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -2,14 +2,14 @@ import $ from 'cafy'; import { ID } from '../../../../../../misc/cafy-id'; import define from '../../../../define'; import { ApiError } from '../../../../error'; -import { UserGroupJoinings, UserGroupInvites } from '../../../../../../models'; +import { UserGroupJoinings, UserGroupInvitations } from '../../../../../../models'; import { genId } from '../../../../../../misc/gen-id'; import { UserGroupJoining } from '../../../../../../models/entities/user-group-joining'; export const meta = { desc: { 'ja-JP': 'ユーザーグループへの招待を承認します。', - 'en-US': 'Accept invite of a user group.' + 'en-US': 'Accept invitation of a user group.' }, tags: ['groups', 'users'], @@ -19,11 +19,11 @@ export const meta = { kind: 'write:user-groups', params: { - inviteId: { + invitationId: { validator: $.type(ID), desc: { 'ja-JP': '招待ID', - 'en-US': 'The invite ID' + 'en-US': 'The invitation ID' } }, }, @@ -39,15 +39,15 @@ export const meta = { export default define(meta, async (ps, user) => { // Fetch the invitation - const invite = await UserGroupInvites.findOne({ - id: ps.inviteId, + const invitation = await UserGroupInvitations.findOne({ + id: ps.invitationId, }); - if (invite == null) { + if (invitation == null) { throw new ApiError(meta.errors.noSuchInvitation); } - if (invite.userId !== user.id) { + if (invitation.userId !== user.id) { throw new ApiError(meta.errors.noSuchInvitation); } @@ -56,8 +56,8 @@ export default define(meta, async (ps, user) => { id: genId(), createdAt: new Date(), userId: user.id, - userGroupId: invite.userGroupId + userGroupId: invitation.userGroupId } as UserGroupJoining); - UserGroupInvites.delete(invite.id); + UserGroupInvitations.delete(invitation.id); }); diff --git a/src/server/api/endpoints/users/groups/invitations/reject.ts b/src/server/api/endpoints/users/groups/invitations/reject.ts index e9e7bc8b48..b9c25c7670 100644 --- a/src/server/api/endpoints/users/groups/invitations/reject.ts +++ b/src/server/api/endpoints/users/groups/invitations/reject.ts @@ -2,12 +2,12 @@ import $ from 'cafy'; import { ID } from '../../../../../../misc/cafy-id'; import define from '../../../../define'; import { ApiError } from '../../../../error'; -import { UserGroupInvites } from '../../../../../../models'; +import { UserGroupInvitations } from '../../../../../../models'; export const meta = { desc: { 'ja-JP': 'ユーザーグループへの招待を拒否します。', - 'en-US': 'Reject invite of a user group.' + 'en-US': 'Reject invitation of a user group.' }, tags: ['groups', 'users'], @@ -17,11 +17,11 @@ export const meta = { kind: 'write:user-groups', params: { - inviteId: { + invitationId: { validator: $.type(ID), desc: { 'ja-JP': '招待ID', - 'en-US': 'The invite ID' + 'en-US': 'The invitation ID' } }, }, @@ -37,17 +37,17 @@ export const meta = { export default define(meta, async (ps, user) => { // Fetch the invitation - const invite = await UserGroupInvites.findOne({ - id: ps.inviteId, + const invitation = await UserGroupInvitations.findOne({ + id: ps.invitationId, }); - if (invite == null) { + if (invitation == null) { throw new ApiError(meta.errors.noSuchInvitation); } - if (invite.userId !== user.id) { + if (invitation.userId !== user.id) { throw new ApiError(meta.errors.noSuchInvitation); } - await UserGroupInvites.delete(invite.id); + await UserGroupInvitations.delete(invitation.id); }); diff --git a/src/server/api/endpoints/users/groups/invite.ts b/src/server/api/endpoints/users/groups/invite.ts index 503184a92f..bd32b00a63 100644 --- a/src/server/api/endpoints/users/groups/invite.ts +++ b/src/server/api/endpoints/users/groups/invite.ts @@ -3,9 +3,10 @@ import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; -import { UserGroups, UserGroupJoinings, UserGroupInvites } from '../../../../../models'; +import { UserGroups, UserGroupJoinings, UserGroupInvitations } from '../../../../../models'; import { genId } from '../../../../../misc/gen-id'; -import { UserGroupInvite } from '../../../../../models/entities/user-group-invite'; +import { UserGroupInvitation } from '../../../../../models/entities/user-group-invitation'; +import { createNotification } from '../../../../../services/create-notification'; export const meta = { desc: { @@ -86,19 +87,24 @@ export default define(meta, async (ps, me) => { throw new ApiError(meta.errors.alreadyAdded); } - const invite = await UserGroupInvites.findOne({ + const existInvitation = await UserGroupInvitations.findOne({ userGroupId: userGroup.id, userId: user.id }); - if (invite) { + if (existInvitation) { throw new ApiError(meta.errors.alreadyInvited); } - await UserGroupInvites.save({ + const invitation = await UserGroupInvitations.save({ id: genId(), createdAt: new Date(), userId: user.id, userGroupId: userGroup.id - } as UserGroupInvite); + } as UserGroupInvitation); + + // 通知を作成 + createNotification(user.id, me.id, 'groupInvited', { + userGroupInvitationId: invitation.id + }); }); diff --git a/src/services/create-notification.ts b/src/services/create-notification.ts index f9cf04dc69..c5c6e7144b 100644 --- a/src/services/create-notification.ts +++ b/src/services/create-notification.ts @@ -6,16 +6,18 @@ import { User } from '../models/entities/user'; import { Note } from '../models/entities/note'; import { Notification } from '../models/entities/notification'; import { FollowRequest } from '../models/entities/follow-request'; +import { UserGroupInvitation } from '../models/entities/user-group-invitation'; export async function createNotification( notifieeId: User['id'], notifierId: User['id'], - type: string, + type: Notification['type'], content?: { noteId?: Note['id']; reaction?: string; choice?: number; followRequestId?: FollowRequest['id']; + userGroupInvitationId?: UserGroupInvitation['id']; } ) { if (notifieeId === notifierId) { @@ -36,6 +38,7 @@ export async function createNotification( if (content.reaction) data.reaction = content.reaction; if (content.choice) data.choice = content.choice; if (content.followRequestId) data.followRequestId = content.followRequestId; + if (content.userGroupInvitationId) data.userGroupInvitationId = content.userGroupInvitationId; } // Create notification