diff --git a/CHANGELOG.md b/CHANGELOG.md index 60eb6e15c..702d77f64 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 e8c0b6957..d6cf37608 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 000000000..26ea54e0b --- /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 e2a220a07..50aff29dd 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 c41139164..9b3acbb3e 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 3e12db3a0..38c779440 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 e359640e8..cd3fe9b01 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 2adf2c024..6fe8f2013 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 15a5c5470..ea8fa6f91 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 6407c19d4..f020714f8 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 000000000..0d3ad525c --- /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 1d4c2aa15..000000000 --- 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 9d07fa31a..37eaba06d 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 33779dd34..cb5cb7bd0 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 e9e7bc8b4..b9c25c767 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 503184a92..bd32b00a6 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 f9cf04dc6..c5c6e7144 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