forked from mirror/misskey
Merge branch 'develop'
This commit is contained in:
commit
1cf9f52eca
14
CHANGELOG.md
14
CHANGELOG.md
@ -54,6 +54,20 @@ mongodb:
|
|||||||
8. master ブランチに戻す
|
8. master ブランチに戻す
|
||||||
9. enjoy
|
9. enjoy
|
||||||
|
|
||||||
|
11.17.0 (2019/05/19)
|
||||||
|
--------------------
|
||||||
|
### 注意
|
||||||
|
このアップデートを適用した後、プロセスを起動(もしくは再起動)する前に[マイグレーション](#migration)の手順を実行してください
|
||||||
|
|
||||||
|
### ✨Improvements
|
||||||
|
* ユーザーグループを招待制に
|
||||||
|
|
||||||
|
### 🐛Fixes
|
||||||
|
* フォロー申請が表示されない問題を修正
|
||||||
|
* トークの通知インジケーターが点灯し続ける問題を修正
|
||||||
|
* 自分を自分のグループから削除できる問題を修正
|
||||||
|
* 新しいタブでグループトークを開くことができない問題を修正
|
||||||
|
|
||||||
11.16.1 (2019/05/19)
|
11.16.1 (2019/05/19)
|
||||||
--------------------
|
--------------------
|
||||||
### 🐛Fixes
|
### 🐛Fixes
|
||||||
|
@ -150,6 +150,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
|||||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo" width="100"></td>
|
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo" width="100"></td>
|
||||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG" width="100"></td>
|
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG" width="100"></td>
|
||||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpe?token-time=2145916800&token-hash=qA8j97lIZNc-74AuZ0p4F3ms6sKPeKjtNt2vEuwpsyo%3D" alt="Hekovic" width="100"></td>
|
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpe?token-time=2145916800&token-hash=qA8j97lIZNc-74AuZ0p4F3ms6sKPeKjtNt2vEuwpsyo%3D" alt="Hekovic" width="100"></td>
|
||||||
|
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/10789744/97175095d8f04c0f86225ff47cb98d40/1.jpeg?token-time=2145916800&token-hash=l4AoMR7Nj7K4yAHrkrk2hAoggPkbSPm12m1nmbe9Pb8%3D" alt="Naoki Hirayama" width="100"></td>
|
||||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1.jpeg?token-time=2145916800&token-hash=L55UhJ0rcuNAH3w_ryeeGN4hC6taoOixyAhraEi0bzw%3D" alt="dansup" width="100"></td>
|
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1.jpeg?token-time=2145916800&token-hash=L55UhJ0rcuNAH3w_ryeeGN4hC6taoOixyAhraEi0bzw%3D" alt="dansup" width="100"></td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td>
|
<td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td>
|
||||||
@ -161,6 +162,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
|||||||
<td><a href="https://www.patreon.com/noellabo">noellabo</a></td>
|
<td><a href="https://www.patreon.com/noellabo">noellabo</a></td>
|
||||||
<td><a href="https://www.patreon.com/Corset">CG</a></td>
|
<td><a href="https://www.patreon.com/Corset">CG</a></td>
|
||||||
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
|
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
|
||||||
|
<td><a href="https://www.patreon.com/spinlock">Naoki Hirayama</a></td>
|
||||||
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
|
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
|
||||||
</tr></table>
|
</tr></table>
|
||||||
<table><tr>
|
<table><tr>
|
||||||
@ -173,7 +175,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
|||||||
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
|
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
|
||||||
</tr></table>
|
</tr></table>
|
||||||
|
|
||||||
**Last updated:** Mon, 13 May 2019 06:13:06 UTC
|
**Last updated:** Sun, 19 May 2019 02:37:06 UTC
|
||||||
<!-- PATREON_END -->
|
<!-- PATREON_END -->
|
||||||
|
|
||||||
:four_leaf_clover: Copyright
|
:four_leaf_clover: Copyright
|
||||||
|
@ -485,6 +485,7 @@ common/views/components/messaging.vue:
|
|||||||
group: "グループ"
|
group: "グループ"
|
||||||
start-with-user: "ユーザーとトークを開始"
|
start-with-user: "ユーザーとトークを開始"
|
||||||
start-with-group: "グループとトークを開始"
|
start-with-group: "グループとトークを開始"
|
||||||
|
select-group: "グループを選択してください"
|
||||||
|
|
||||||
common/views/components/messaging-room.vue:
|
common/views/components/messaging-room.vue:
|
||||||
not-talked-user: "このユーザーとの会話はありません"
|
not-talked-user: "このユーザーとの会話はありません"
|
||||||
@ -764,7 +765,8 @@ common/views/components/user-group-editor.vue:
|
|||||||
remove-user: "このグループから削除"
|
remove-user: "このグループから削除"
|
||||||
delete-are-you-sure: "グループ「$1」を削除しますか?"
|
delete-are-you-sure: "グループ「$1」を削除しますか?"
|
||||||
deleted: "削除しました"
|
deleted: "削除しました"
|
||||||
add-user: "メンバーを追加"
|
invite: "招待"
|
||||||
|
invited: "招待を送信しました"
|
||||||
|
|
||||||
common/views/components/user-lists.vue:
|
common/views/components/user-lists.vue:
|
||||||
user-lists: "リスト"
|
user-lists: "リスト"
|
||||||
@ -775,6 +777,11 @@ common/views/components/user-groups.vue:
|
|||||||
user-groups: "グループ"
|
user-groups: "グループ"
|
||||||
create-group: "グループを作成"
|
create-group: "グループを作成"
|
||||||
group-name: "グループ名"
|
group-name: "グループ名"
|
||||||
|
owned-groups: "自分のグループ"
|
||||||
|
joined-groups: "参加しているグループ"
|
||||||
|
invites: "招待"
|
||||||
|
accept-invite: "参加"
|
||||||
|
reject-invite: "拒否"
|
||||||
|
|
||||||
common/views/widgets/broadcast.vue:
|
common/views/widgets/broadcast.vue:
|
||||||
fetching: "確認中"
|
fetching: "確認中"
|
||||||
|
25
migration/1558257926829-UserGroupInvite.ts
Normal file
25
migration/1558257926829-UserGroupInvite.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class UserGroupInvite1558257926829 implements MigrationInterface {
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "user_group_invite" ("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_3893884af0d3a5f4d01e7921a97" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_1039988afa3bf991185b277fe0" ON "user_group_invite" ("userId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_e10924607d058004304611a436" ON "user_group_invite" ("userGroupId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_78787741f9010886796f2320a4" ON "user_group_invite" ("userId", "userGroupId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9ecaed8c6dc43f3592c229282" ON "user_group_joining" ("userId", "userGroupId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_1039988afa3bf991185b277fe03" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_e10924607d058004304611a436a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_e10924607d058004304611a436a"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_1039988afa3bf991185b277fe03"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_d9ecaed8c6dc43f3592c229282"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_78787741f9010886796f2320a4"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_e10924607d058004304611a436"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_1039988afa3bf991185b277fe0"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_group_invite"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
13
migration/1558266512381-UserListJoining.ts
Normal file
13
migration/1558266512381-UserListJoining.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class UserListJoining1558266512381 implements MigrationInterface {
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_90f7da835e4c10aca6853621e1" ON "user_list_joining" ("userId", "userListId") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_90f7da835e4c10aca6853621e1"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "11.16.1",
|
"version": "11.17.0",
|
||||||
"codename": "daybreak",
|
"codename": "daybreak",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -217,12 +217,13 @@ export default Vue.extend({
|
|||||||
this.navigate(user);
|
this.navigate(user);
|
||||||
},
|
},
|
||||||
async startGroup() {
|
async startGroup() {
|
||||||
const groups = await this.$root.api('users/groups/joined');
|
const groups1 = await this.$root.api('users/groups/owned');
|
||||||
|
const groups2 = await this.$root.api('users/groups/joined');
|
||||||
const { canceled, result: group } = await this.$root.dialog({
|
const { canceled, result: group } = await this.$root.dialog({
|
||||||
type: null,
|
type: null,
|
||||||
title: this.$t('select-group'),
|
title: this.$t('select-group'),
|
||||||
select: {
|
select: {
|
||||||
items: groups.map(group => ({
|
items: groups1.concat(groups2).map(group => ({
|
||||||
value: group, text: group.name
|
value: group, text: group.name
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
<ui-container :body-togglable="true">
|
<ui-container :body-togglable="true">
|
||||||
<template #header>{{ $t('received-follow-requests') }}</template>
|
<template #header>{{ $t('received-follow-requests') }}</template>
|
||||||
<div v-if="!fetching">
|
<div v-if="!fetching">
|
||||||
<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="mcbzkkaw">
|
<sequential-entrance animation="entranceFromTop" delay="25" tag="div">
|
||||||
<div v-for="req in requests">
|
<div v-for="req in requests" class="mcbzkkaw">
|
||||||
<router-link :key="req.id" :to="req.follower | userPage">
|
<router-link :key="req.id" :to="req.follower | userPage">
|
||||||
<mk-user-name :user="req.follower"/>
|
<mk-user-name :user="req.follower"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
@ -56,7 +56,6 @@ export default Vue.extend({
|
|||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.mcbzkkaw
|
.mcbzkkaw
|
||||||
> div
|
|
||||||
display flex
|
display flex
|
||||||
padding 16px
|
padding 16px
|
||||||
border solid 1px var(--faceDivider)
|
border solid 1px var(--faceDivider)
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<ui-margin>
|
<ui-margin>
|
||||||
<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add-user') }}</ui-button>
|
<ui-button @click="invite()"><fa :icon="faPlus"/> {{ $t('invite') }}</ui-button>
|
||||||
</ui-margin>
|
</ui-margin>
|
||||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||||
<div class="kjlrfbes" v-for="user in users">
|
<div class="kjlrfbes" v-for="user in users">
|
||||||
@ -134,18 +134,22 @@ export default Vue.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async add() {
|
async invite() {
|
||||||
|
const t = this.$t('invited');
|
||||||
const { result: user } = await this.$root.dialog({
|
const { result: user } = await this.$root.dialog({
|
||||||
user: {
|
user: {
|
||||||
local: true
|
local: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
this.$root.api('users/groups/push', {
|
this.$root.api('users/groups/invite', {
|
||||||
groupId: this.group.id,
|
groupId: this.group.id,
|
||||||
userId: user.id
|
userId: user.id
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.fetchUsers();
|
this.$root.dialog({
|
||||||
|
type: 'success',
|
||||||
|
text: t
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<ui-container>
|
<div>
|
||||||
<template #header><fa :icon="faUsers"/> {{ $t('user-groups') }}</template>
|
<ui-container>
|
||||||
|
<template #header><fa :icon="faUsers"/> {{ $t('owned-groups') }}</template>
|
||||||
<ui-margin>
|
<ui-margin>
|
||||||
<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-group') }}</ui-button>
|
<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-group') }}</ui-button>
|
||||||
</ui-margin>
|
</ui-margin>
|
||||||
<div class="hwgkdrbl" v-for="group in groups" :key="group.id">
|
<div class="hwgkdrbl" v-for="group in ownedGroups" :key="group.id">
|
||||||
<ui-hr/>
|
<ui-hr/>
|
||||||
<ui-margin>
|
<ui-margin>
|
||||||
<router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link>
|
<router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link>
|
||||||
</ui-margin>
|
</ui-margin>
|
||||||
</div>
|
</div>
|
||||||
</ui-container>
|
</ui-container>
|
||||||
|
|
||||||
|
<ui-container>
|
||||||
|
<template #header><fa :icon="faUsers"/> {{ $t('joined-groups') }}</template>
|
||||||
|
<div class="hwgkdrbl" v-for="(group, i) in joinedGroups" :key="group.id">
|
||||||
|
<ui-hr v-if="i != 0"/>
|
||||||
|
<ui-margin>
|
||||||
|
<router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link>
|
||||||
|
</ui-margin>
|
||||||
|
</div>
|
||||||
|
</ui-container>
|
||||||
|
|
||||||
|
<ui-container>
|
||||||
|
<template #header><fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</template>
|
||||||
|
<div class="fvlojuur" v-for="(invite, i) in invites" :key="invite.id">
|
||||||
|
<ui-hr v-if="i != 0"/>
|
||||||
|
<ui-margin>
|
||||||
|
<div class="name">{{ invite.group.name }}</div>
|
||||||
|
<ui-horizon-group>
|
||||||
|
<ui-button @click="acceptInvite(invite)"><fa :icon="faCheck"/> {{ $t('accept-invite') }}</ui-button>
|
||||||
|
<ui-button @click="rejectInvite(invite)"><fa :icon="faBan"/> {{ $t('reject-invite') }}</ui-button>
|
||||||
|
</ui-horizon-group>
|
||||||
|
</ui-margin>
|
||||||
|
</div>
|
||||||
|
</ui-container>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import { faUsers, faPlus } from '@fortawesome/free-solid-svg-icons';
|
import { faUsers, faPlus, faCheck, faBan, faEnvelopeOpenText } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('common/views/components/user-groups.vue'),
|
i18n: i18n('common/views/components/user-groups.vue'),
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
fetching: true,
|
ownedGroups: [],
|
||||||
groups: [],
|
joinedGroups: [],
|
||||||
faUsers, faPlus
|
invites: [],
|
||||||
|
faUsers, faPlus, faCheck, faBan, faEnvelopeOpenText
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$root.api('users/groups/owned').then(groups => {
|
this.$root.api('users/groups/owned').then(groups => {
|
||||||
this.fetching = false;
|
this.ownedGroups = groups;
|
||||||
this.groups = groups;
|
});
|
||||||
|
|
||||||
|
this.$root.api('users/groups/joined').then(groups => {
|
||||||
|
this.joinedGroups = groups;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$root.api('i/user-group-invites').then(invites => {
|
||||||
|
this.invites = invites;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$emit('init', {
|
this.$emit('init', {
|
||||||
@ -45,13 +79,35 @@ export default Vue.extend({
|
|||||||
input: true
|
input: true
|
||||||
}).then(async ({ canceled, result: name }) => {
|
}).then(async ({ canceled, result: name }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
const list = await this.$root.api('users/groups/create', {
|
const group = await this.$root.api('users/groups/create', {
|
||||||
name
|
name
|
||||||
});
|
});
|
||||||
|
|
||||||
this.groups.push(list)
|
this.ownedGroups.push(group)
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
acceptInvite(invite) {
|
||||||
|
this.$root.api('users/groups/invitations/accept', {
|
||||||
|
inviteId: invite.id
|
||||||
|
}).then(() => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'success',
|
||||||
|
splash: true
|
||||||
|
});
|
||||||
|
this.$root.api('i/user-group-invites').then(invites => {
|
||||||
|
this.invites = invites;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
rejectInvite(invite) {
|
||||||
|
this.$root.api('users/groups/invitations/reject', {
|
||||||
|
inviteId: invite.id
|
||||||
|
}).then(() => {
|
||||||
|
this.$root.api('i/user-group-invites').then(invites => {
|
||||||
|
this.invites = invites;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -26,6 +26,7 @@ import { UserList } from '../models/entities/user-list';
|
|||||||
import { UserListJoining } from '../models/entities/user-list-joining';
|
import { UserListJoining } from '../models/entities/user-list-joining';
|
||||||
import { UserGroup } from '../models/entities/user-group';
|
import { UserGroup } from '../models/entities/user-group';
|
||||||
import { UserGroupJoining } from '../models/entities/user-group-joining';
|
import { UserGroupJoining } from '../models/entities/user-group-joining';
|
||||||
|
import { UserGroupInvite } from '../models/entities/user-group-invite';
|
||||||
import { Hashtag } from '../models/entities/hashtag';
|
import { Hashtag } from '../models/entities/hashtag';
|
||||||
import { NoteFavorite } from '../models/entities/note-favorite';
|
import { NoteFavorite } from '../models/entities/note-favorite';
|
||||||
import { AbuseUserReport } from '../models/entities/abuse-user-report';
|
import { AbuseUserReport } from '../models/entities/abuse-user-report';
|
||||||
@ -110,6 +111,7 @@ export function initDb(justBorrow = false, sync = false, log = false) {
|
|||||||
UserListJoining,
|
UserListJoining,
|
||||||
UserGroup,
|
UserGroup,
|
||||||
UserGroupJoining,
|
UserGroupJoining,
|
||||||
|
UserGroupInvite,
|
||||||
UserNotePining,
|
UserNotePining,
|
||||||
Following,
|
Following,
|
||||||
FollowRequest,
|
FollowRequest,
|
||||||
|
42
src/models/entities/user-group-invite.ts
Normal file
42
src/models/entities/user-group-invite.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||||
|
import { User } from './user';
|
||||||
|
import { UserGroup } from './user-group';
|
||||||
|
import { id } from '../id';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['userId', 'userGroupId'], { unique: true })
|
||||||
|
export class UserGroupInvite {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
comment: 'The created date of the UserGroupInvite.'
|
||||||
|
})
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
comment: 'The user ID.'
|
||||||
|
})
|
||||||
|
public userId: User['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => User, {
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public user: User | null;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
comment: 'The group ID.'
|
||||||
|
})
|
||||||
|
public userGroupId: UserGroup['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => UserGroup, {
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public userGroup: UserGroup | null;
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { UserGroup } from './user-group';
|
|||||||
import { id } from '../id';
|
import { id } from '../id';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@Index(['userId', 'userGroupId'], { unique: true })
|
||||||
export class UserGroupJoining {
|
export class UserGroupJoining {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
@ -4,6 +4,7 @@ import { UserList } from './user-list';
|
|||||||
import { id } from '../id';
|
import { id } from '../id';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@Index(['userId', 'userListId'], { unique: true })
|
||||||
export class UserListJoining {
|
export class UserListJoining {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
@ -22,6 +22,7 @@ import { UserListRepository } from './repositories/user-list';
|
|||||||
import { UserListJoining } from './entities/user-list-joining';
|
import { UserListJoining } from './entities/user-list-joining';
|
||||||
import { UserGroupRepository } from './repositories/user-group';
|
import { UserGroupRepository } from './repositories/user-group';
|
||||||
import { UserGroupJoining } from './entities/user-group-joining';
|
import { UserGroupJoining } from './entities/user-group-joining';
|
||||||
|
import { UserGroupInviteRepository } from './repositories/user-group-invite';
|
||||||
import { FollowRequestRepository } from './repositories/follow-request';
|
import { FollowRequestRepository } from './repositories/follow-request';
|
||||||
import { MutingRepository } from './repositories/muting';
|
import { MutingRepository } from './repositories/muting';
|
||||||
import { BlockingRepository } from './repositories/blocking';
|
import { BlockingRepository } from './repositories/blocking';
|
||||||
@ -56,6 +57,7 @@ export const UserLists = getCustomRepository(UserListRepository);
|
|||||||
export const UserListJoinings = getRepository(UserListJoining);
|
export const UserListJoinings = getRepository(UserListJoining);
|
||||||
export const UserGroups = getCustomRepository(UserGroupRepository);
|
export const UserGroups = getCustomRepository(UserGroupRepository);
|
||||||
export const UserGroupJoinings = getRepository(UserGroupJoining);
|
export const UserGroupJoinings = getRepository(UserGroupJoining);
|
||||||
|
export const UserGroupInvites = getCustomRepository(UserGroupInviteRepository);
|
||||||
export const UserNotePinings = getRepository(UserNotePining);
|
export const UserNotePinings = getRepository(UserNotePining);
|
||||||
export const Followings = getCustomRepository(FollowingRepository);
|
export const Followings = getCustomRepository(FollowingRepository);
|
||||||
export const FollowRequests = getCustomRepository(FollowRequestRepository);
|
export const FollowRequests = getCustomRepository(FollowRequestRepository);
|
||||||
|
@ -35,7 +35,7 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
|
|||||||
user: await Users.pack(message.user || message.userId, me),
|
user: await Users.pack(message.user || message.userId, me),
|
||||||
recipientId: message.recipientId,
|
recipientId: message.recipientId,
|
||||||
recipient: message.recipientId && opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined,
|
recipient: message.recipientId && opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined,
|
||||||
groupId: message.recipientId,
|
groupId: message.groupId,
|
||||||
group: message.groupId && opts.populateGroup ? await UserGroups.pack(message.group || message.groupId) : undefined,
|
group: message.groupId && opts.populateGroup ? await UserGroups.pack(message.group || message.groupId) : undefined,
|
||||||
fileId: message.fileId,
|
fileId: message.fileId,
|
||||||
file: message.fileId ? await DriveFiles.pack(message.fileId) : null,
|
file: message.fileId ? await DriveFiles.pack(message.fileId) : null,
|
||||||
|
@ -13,6 +13,8 @@ export class NoteFavoriteRepository extends Repository<NoteFavorite> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: favorite.id,
|
id: favorite.id,
|
||||||
|
createdAt: favorite.createdAt,
|
||||||
|
noteId: favorite.noteId,
|
||||||
note: await Notes.pack(favorite.note || favorite.noteId, me),
|
note: await Notes.pack(favorite.note || favorite.noteId, me),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
24
src/models/repositories/user-group-invite.ts
Normal file
24
src/models/repositories/user-group-invite.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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<UserGroupInvite> {
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
@ -42,7 +42,7 @@ export async function readUserMessagingMessage(
|
|||||||
publishMessagingStream(otherpartyId, userId, 'read', messageIds);
|
publishMessagingStream(otherpartyId, userId, 'read', messageIds);
|
||||||
publishMessagingIndexStream(userId, 'read', messageIds);
|
publishMessagingIndexStream(userId, 'read', messageIds);
|
||||||
|
|
||||||
if (!Users.getHasUnreadMessagingMessage(userId)) {
|
if (!await Users.getHasUnreadMessagingMessage(userId)) {
|
||||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||||
publishMainStream(userId, 'readAllMessagingMessages');
|
publishMainStream(userId, 'readAllMessagingMessages');
|
||||||
}
|
}
|
||||||
@ -96,7 +96,7 @@ export async function readGroupMessagingMessage(
|
|||||||
});
|
});
|
||||||
publishMessagingIndexStream(userId, 'read', reads);
|
publishMessagingIndexStream(userId, 'read', reads);
|
||||||
|
|
||||||
if (!Users.getHasUnreadMessagingMessage(userId)) {
|
if (!await Users.getHasUnreadMessagingMessage(userId)) {
|
||||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||||
publishMainStream(userId, 'readAllMessagingMessages');
|
publishMainStream(userId, 'readAllMessagingMessages');
|
||||||
}
|
}
|
||||||
|
45
src/server/api/endpoints/i/user-group-invites.ts
Normal file
45
src/server/api/endpoints/i/user-group-invites.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import $ from 'cafy';
|
||||||
|
import { ID } from '../../../../misc/cafy-id';
|
||||||
|
import define from '../../define';
|
||||||
|
import { UserGroupInvites } from '../../../../models';
|
||||||
|
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
desc: {
|
||||||
|
'ja-JP': 'グループへの招待一覧を取得します。',
|
||||||
|
'en-US': 'Get user group invitations.'
|
||||||
|
},
|
||||||
|
|
||||||
|
tags: ['account', 'groups'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'read:user-groups',
|
||||||
|
|
||||||
|
params: {
|
||||||
|
limit: {
|
||||||
|
validator: $.optional.num.range(1, 100),
|
||||||
|
default: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
sinceId: {
|
||||||
|
validator: $.optional.type(ID),
|
||||||
|
},
|
||||||
|
|
||||||
|
untilId: {
|
||||||
|
validator: $.optional.type(ID),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 invites = await query
|
||||||
|
.take(ps.limit!)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
return await UserGroupInvites.packMany(invites);
|
||||||
|
});
|
63
src/server/api/endpoints/users/groups/invitations/accept.ts
Normal file
63
src/server/api/endpoints/users/groups/invitations/accept.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import $ from 'cafy';
|
||||||
|
import { ID } from '../../../../../../misc/cafy-id';
|
||||||
|
import define from '../../../../define';
|
||||||
|
import { ApiError } from '../../../../error';
|
||||||
|
import { UserGroupJoinings, UserGroupInvites } 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.'
|
||||||
|
},
|
||||||
|
|
||||||
|
tags: ['groups', 'users'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:user-groups',
|
||||||
|
|
||||||
|
params: {
|
||||||
|
inviteId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '招待ID',
|
||||||
|
'en-US': 'The invite ID'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchInvitation: {
|
||||||
|
message: 'No such invitation.',
|
||||||
|
code: 'NO_SUCH_INVITATION',
|
||||||
|
id: '98c11eca-c890-4f42-9806-c8c8303ebb5e'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, user) => {
|
||||||
|
// Fetch the invitation
|
||||||
|
const invite = await UserGroupInvites.findOne({
|
||||||
|
id: ps.inviteId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (invite == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchInvitation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invite.userId !== user.id) {
|
||||||
|
throw new ApiError(meta.errors.noSuchInvitation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the user
|
||||||
|
await UserGroupJoinings.save({
|
||||||
|
id: genId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
userId: user.id,
|
||||||
|
userGroupId: invite.userGroupId
|
||||||
|
} as UserGroupJoining);
|
||||||
|
|
||||||
|
UserGroupInvites.delete(invite.id);
|
||||||
|
});
|
53
src/server/api/endpoints/users/groups/invitations/reject.ts
Normal file
53
src/server/api/endpoints/users/groups/invitations/reject.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import $ from 'cafy';
|
||||||
|
import { ID } from '../../../../../../misc/cafy-id';
|
||||||
|
import define from '../../../../define';
|
||||||
|
import { ApiError } from '../../../../error';
|
||||||
|
import { UserGroupInvites } from '../../../../../../models';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
desc: {
|
||||||
|
'ja-JP': 'ユーザーグループへの招待を拒否します。',
|
||||||
|
'en-US': 'Reject invite of a user group.'
|
||||||
|
},
|
||||||
|
|
||||||
|
tags: ['groups', 'users'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:user-groups',
|
||||||
|
|
||||||
|
params: {
|
||||||
|
inviteId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '招待ID',
|
||||||
|
'en-US': 'The invite ID'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchInvitation: {
|
||||||
|
message: 'No such invitation.',
|
||||||
|
code: 'NO_SUCH_INVITATION',
|
||||||
|
id: 'ad7471d4-2cd9-44b4-ac68-e7136b4ce656'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, user) => {
|
||||||
|
// Fetch the invitation
|
||||||
|
const invite = await UserGroupInvites.findOne({
|
||||||
|
id: ps.inviteId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (invite == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchInvitation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invite.userId !== user.id) {
|
||||||
|
throw new ApiError(meta.errors.noSuchInvitation);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserGroupInvites.delete(invite.id);
|
||||||
|
});
|
@ -3,14 +3,14 @@ import { ID } from '../../../../../misc/cafy-id';
|
|||||||
import define from '../../../define';
|
import define from '../../../define';
|
||||||
import { ApiError } from '../../../error';
|
import { ApiError } from '../../../error';
|
||||||
import { getUser } from '../../../common/getters';
|
import { getUser } from '../../../common/getters';
|
||||||
import { UserGroups, UserGroupJoinings } from '../../../../../models';
|
import { UserGroups, UserGroupJoinings, UserGroupInvites } from '../../../../../models';
|
||||||
import { genId } from '../../../../../misc/gen-id';
|
import { genId } from '../../../../../misc/gen-id';
|
||||||
import { UserGroupJoining } from '../../../../../models/entities/user-group-joining';
|
import { UserGroupInvite } from '../../../../../models/entities/user-group-invite';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '指定したユーザーグループに指定したユーザーを追加します。',
|
'ja-JP': '指定したユーザーグループに指定したユーザーを招待します。',
|
||||||
'en-US': 'Add a user to a user group.'
|
'en-US': 'Invite a user to a user group.'
|
||||||
},
|
},
|
||||||
|
|
||||||
tags: ['groups', 'users'],
|
tags: ['groups', 'users'],
|
||||||
@ -50,6 +50,12 @@ export const meta = {
|
|||||||
message: 'That user has already been added to that group.',
|
message: 'That user has already been added to that group.',
|
||||||
code: 'ALREADY_ADDED',
|
code: 'ALREADY_ADDED',
|
||||||
id: '7e35c6a0-39b2-4488-aea6-6ee20bd5da2c'
|
id: '7e35c6a0-39b2-4488-aea6-6ee20bd5da2c'
|
||||||
|
},
|
||||||
|
|
||||||
|
alreadyInvited: {
|
||||||
|
message: 'That user has already been invited to that group.',
|
||||||
|
code: 'ALREADY_INVITED',
|
||||||
|
id: 'ee0f58b4-b529-4d13-b761-b9a3e69f97e6'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -71,20 +77,28 @@ export default define(meta, async (ps, me) => {
|
|||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
const exist = await UserGroupJoinings.findOne({
|
const joining = await UserGroupJoinings.findOne({
|
||||||
userGroupId: userGroup.id,
|
userGroupId: userGroup.id,
|
||||||
userId: user.id
|
userId: user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist) {
|
if (joining) {
|
||||||
throw new ApiError(meta.errors.alreadyAdded);
|
throw new ApiError(meta.errors.alreadyAdded);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push the user
|
const invite = await UserGroupInvites.findOne({
|
||||||
await UserGroupJoinings.save({
|
userGroupId: userGroup.id,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (invite) {
|
||||||
|
throw new ApiError(meta.errors.alreadyInvited);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserGroupInvites.save({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
userGroupId: userGroup.id
|
userGroupId: userGroup.id
|
||||||
} as UserGroupJoining);
|
} as UserGroupInvite);
|
||||||
});
|
});
|
@ -1,6 +1,7 @@
|
|||||||
import define from '../../../define';
|
import define from '../../../define';
|
||||||
import { UserGroups, UserGroupJoinings } from '../../../../../models';
|
import { UserGroups, UserGroupJoinings } from '../../../../../models';
|
||||||
import { types, bool } from '../../../../../misc/schema';
|
import { types, bool } from '../../../../../misc/schema';
|
||||||
|
import { Not, In } from 'typeorm';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
@ -25,8 +26,13 @@ export const meta = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default define(meta, async (ps, me) => {
|
export default define(meta, async (ps, me) => {
|
||||||
|
const ownedGroups = await UserGroups.find({
|
||||||
|
userId: me.id,
|
||||||
|
});
|
||||||
|
|
||||||
const joinings = await UserGroupJoinings.find({
|
const joinings = await UserGroupJoinings.find({
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
userGroupId: Not(In(ownedGroups.map(x => x.id)))
|
||||||
});
|
});
|
||||||
|
|
||||||
return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId)));
|
return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId)));
|
||||||
|
@ -42,7 +42,13 @@ export const meta = {
|
|||||||
message: 'No such user.',
|
message: 'No such user.',
|
||||||
code: 'NO_SUCH_USER',
|
code: 'NO_SUCH_USER',
|
||||||
id: '0b5cc374-3681-41da-861e-8bc1146f7a55'
|
id: '0b5cc374-3681-41da-861e-8bc1146f7a55'
|
||||||
}
|
},
|
||||||
|
|
||||||
|
isOwner: {
|
||||||
|
message: 'The user is the owner.',
|
||||||
|
code: 'IS_OWNER',
|
||||||
|
id: '1546eed5-4414-4dea-81c1-b0aec4f6d2af'
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,6 +69,10 @@ export default define(meta, async (ps, me) => {
|
|||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (user.id === userGroup.userId) {
|
||||||
|
throw new ApiError(meta.errors.isOwner);
|
||||||
|
}
|
||||||
|
|
||||||
// Pull the user
|
// Pull the user
|
||||||
await UserGroupJoinings.delete({ userId: user.id });
|
await UserGroupJoinings.delete({ userId: user.id });
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user