parent
							
								
									61f54f8f74
								
							
						
					
					
						commit
						c7cc3dcdfd
					
				
					 65 changed files with 1797 additions and 638 deletions
				
			
		|  | @ -265,6 +265,7 @@ common: | ||||||
|   my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" |   my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" | ||||||
|   hide-password: "パスワードを隠す" |   hide-password: "パスワードを隠す" | ||||||
|   show-password: "パスワードを表示する" |   show-password: "パスワードを表示する" | ||||||
|  |   enter-username: "ユーザー名を入力してください" | ||||||
| 
 | 
 | ||||||
|   do-not-use-in-production: "これは開発ビルドです。本番環境で使用しないでください。" |   do-not-use-in-production: "これは開発ビルドです。本番環境で使用しないでください。" | ||||||
|   user-suspended: "このユーザーは凍結されています。" |   user-suspended: "このユーザーは凍結されています。" | ||||||
|  | @ -480,20 +481,24 @@ common/views/components/messaging.vue: | ||||||
|   search-user: "ユーザーを探す" |   search-user: "ユーザーを探す" | ||||||
|   you: "あなた" |   you: "あなた" | ||||||
|   no-history: "履歴はありません" |   no-history: "履歴はありません" | ||||||
|  |   user: "ユーザー" | ||||||
|  |   group: "グループ" | ||||||
|  |   start-with-user: "ユーザーとトークを開始" | ||||||
|  |   start-with-group: "グループとトークを開始" | ||||||
| 
 | 
 | ||||||
| common/views/components/messaging-room.vue: | common/views/components/messaging-room.vue: | ||||||
|   empty: "このユーザーと話したことはありません" |   not-talked-user: "このユーザーとの会話はありません" | ||||||
|  |   not-talked-group: "このグループでの会話はありません" | ||||||
|   no-history: "これより過去の履歴はありません" |   no-history: "これより過去の履歴はありません" | ||||||
|   resize-form: "ドラッグしてフォームの広さを調整" |  | ||||||
|   new-message: "新しいメッセージがあります" |   new-message: "新しいメッセージがあります" | ||||||
|   only-one-file-attached: "メッセージに添付できるのはひとつのファイルのみです" |   only-one-file-attached: "メッセージに添付できるファイルはひとつです" | ||||||
| 
 | 
 | ||||||
| common/views/components/messaging-room.form.vue: | common/views/components/messaging-room.form.vue: | ||||||
|   input-message-here: "ここにメッセージを入力" |   input-message-here: "ここにメッセージを入力" | ||||||
|   send: "送信" |   send: "送信" | ||||||
|   attach-from-local: "PCからファイルを添付する" |   attach-from-local: "PCからファイルを添付する" | ||||||
|   attach-from-drive: "ドライブからファイルを添付する" |   attach-from-drive: "ドライブからファイルを添付する" | ||||||
|   only-one-file-attached: "メッセージに添付できるのはひとつのファイルのみです" |   only-one-file-attached: "メッセージに添付できるファイルはひとつです" | ||||||
| 
 | 
 | ||||||
| common/views/components/messaging-room.message.vue: | common/views/components/messaging-room.message.vue: | ||||||
|   is-read: "既読" |   is-read: "既読" | ||||||
|  | @ -750,11 +755,27 @@ common/views/components/user-list-editor.vue: | ||||||
|   remove-user: "このリストから削除" |   remove-user: "このリストから削除" | ||||||
|   delete-are-you-sure: "リスト「$1」を削除しますか?" |   delete-are-you-sure: "リスト「$1」を削除しますか?" | ||||||
|   deleted: "削除しました" |   deleted: "削除しました" | ||||||
|  |   add-user: "ユーザーを追加" | ||||||
|  | 
 | ||||||
|  | common/views/components/user-group-editor.vue: | ||||||
|  |   users: "メンバー" | ||||||
|  |   rename: "グループ名を変更" | ||||||
|  |   delete: "グループを削除" | ||||||
|  |   remove-user: "このグループから削除" | ||||||
|  |   delete-are-you-sure: "グループ「$1」を削除しますか?" | ||||||
|  |   deleted: "削除しました" | ||||||
|  |   add-user: "メンバーを追加" | ||||||
| 
 | 
 | ||||||
| common/views/components/user-lists.vue: | common/views/components/user-lists.vue: | ||||||
|  |   user-lists: "リスト" | ||||||
|   create-list: "リストを作成" |   create-list: "リストを作成" | ||||||
|   list-name: "リスト名" |   list-name: "リスト名" | ||||||
| 
 | 
 | ||||||
|  | common/views/components/user-groups.vue: | ||||||
|  |   user-groups: "グループ" | ||||||
|  |   create-group: "グループを作成" | ||||||
|  |   group-name: "グループ名" | ||||||
|  | 
 | ||||||
| common/views/widgets/broadcast.vue: | common/views/widgets/broadcast.vue: | ||||||
|   fetching: "確認中" |   fetching: "確認中" | ||||||
|   no-broadcasts: "お知らせはありません" |   no-broadcasts: "お知らせはありません" | ||||||
|  | @ -827,6 +848,11 @@ common/views/pages/follow.vue: | ||||||
|   follow-processing: "フォロー処理中" |   follow-processing: "フォロー処理中" | ||||||
|   follow-request: "フォロー申請" |   follow-request: "フォロー申請" | ||||||
| 
 | 
 | ||||||
|  | common/views/pages/follow-requests.vue: | ||||||
|  |   received-follow-requests: "フォロー申請" | ||||||
|  |   accept: "承認" | ||||||
|  |   reject: "拒否" | ||||||
|  | 
 | ||||||
| desktop: | desktop: | ||||||
|   banner-crop-title: "バナーとして表示する部分を選択" |   banner-crop-title: "バナーとして表示する部分を選択" | ||||||
|   banner: "バナー" |   banner: "バナー" | ||||||
|  | @ -1139,6 +1165,7 @@ desktop/views/components/ui.header.vue: | ||||||
| desktop/views/components/ui.header.account.vue: | desktop/views/components/ui.header.account.vue: | ||||||
|   profile: "プロフィール" |   profile: "プロフィール" | ||||||
|   lists: "リスト" |   lists: "リスト" | ||||||
|  |   groups: "グループ" | ||||||
|   follow-requests: "フォロー申請" |   follow-requests: "フォロー申請" | ||||||
|   admin: "管理" |   admin: "管理" | ||||||
| 
 | 
 | ||||||
|  | @ -1154,14 +1181,6 @@ desktop/views/components/ui.header.post.vue: | ||||||
| desktop/views/components/ui.header.search.vue: | desktop/views/components/ui.header.search.vue: | ||||||
|   placeholder: "検索" |   placeholder: "検索" | ||||||
| 
 | 
 | ||||||
| desktop/views/components/received-follow-requests-window.vue: |  | ||||||
|   title: "フォロー申請" |  | ||||||
|   accept: "承認" |  | ||||||
|   reject: "拒否" |  | ||||||
| 
 |  | ||||||
| desktop/views/components/user-lists-window.vue: |  | ||||||
|   title: "リスト" |  | ||||||
| 
 |  | ||||||
| desktop/views/components/user-preview.vue: | desktop/views/components/user-preview.vue: | ||||||
|   notes: "投稿" |   notes: "投稿" | ||||||
|   following: "フォロー" |   following: "フォロー" | ||||||
|  | @ -1749,11 +1768,6 @@ mobile/views/pages/widgets/activity.vue: | ||||||
| mobile/views/pages/share.vue: | mobile/views/pages/share.vue: | ||||||
|   share-with: "{name}で共有" |   share-with: "{name}で共有" | ||||||
| 
 | 
 | ||||||
| mobile/views/pages/received-follow-requests.vue: |  | ||||||
|   title: "フォロー申請" |  | ||||||
|   accept: "承認" |  | ||||||
|   reject: "拒否" |  | ||||||
| 
 |  | ||||||
| mobile/views/pages/note.vue: | mobile/views/pages/note.vue: | ||||||
|   title: "投稿" |   title: "投稿" | ||||||
|   prev: "前の投稿" |   prev: "前の投稿" | ||||||
|  |  | ||||||
							
								
								
									
										41
									
								
								migration/1558103093633-UserGroup.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								migration/1558103093633-UserGroup.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | ||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  | 
 | ||||||
|  | export class UserGroup1558103093633 implements MigrationInterface { | ||||||
|  | 
 | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<any> { | ||||||
|  |         await queryRunner.query(`CREATE TABLE "user_group" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "isPrivate" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_3c29fba6fe013ec8724378ce7c9" PRIMARY KEY ("id"))`); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_20e30aa35180e317e133d75316" ON "user_group" ("createdAt") `); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_3d6b372788ab01be58853003c9" ON "user_group" ("userId") `); | ||||||
|  |         await queryRunner.query(`CREATE TABLE "user_group_joining" ("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_15f2425885253c5507e1599cfe7" PRIMARY KEY ("id"))`); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23" ON "user_group_joining" ("userId") `); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_67dc758bc0566985d1b3d39986" ON "user_group_joining" ("userGroupId") `); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "messaging_message" ADD "groupId" character varying(32)`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "messaging_message" ADD "reads" character varying(32) array NOT NULL DEFAULT '{}'::varchar[]`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" DROP NOT NULL`); | ||||||
|  |         await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS 'The recipient user ID.'`); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_2c4be03b446884f9e9c502135b" ON "messaging_message" ("groupId") `); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_2c4be03b446884f9e9c502135be" FOREIGN KEY ("groupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_group" ADD CONSTRAINT "FK_3d6b372788ab01be58853003c93" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_67dc758bc0566985d1b3d399865" 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_joining" DROP CONSTRAINT "FK_67dc758bc0566985d1b3d399865"`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_group_joining" DROP CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231"`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_group" DROP CONSTRAINT "FK_3d6b372788ab01be58853003c93"`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_2c4be03b446884f9e9c502135be"`); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_2c4be03b446884f9e9c502135b"`); | ||||||
|  |         await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS ''`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" SET NOT NULL`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "reads"`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "groupId"`); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_67dc758bc0566985d1b3d39986"`); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23"`); | ||||||
|  |         await queryRunner.query(`DROP TABLE "user_group_joining"`); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_3d6b372788ab01be58853003c9"`); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_20e30aa35180e317e133d75316"`); | ||||||
|  |         await queryRunner.query(`DROP TABLE "user_group"`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -18,6 +18,7 @@ | ||||||
| 				<fa icon="spinner" pulse v-if="type === 'waiting'"/> | 				<fa icon="spinner" pulse v-if="type === 'waiting'"/> | ||||||
| 			</div> | 			</div> | ||||||
| 			<header v-if="title" v-html="title"></header> | 			<header v-if="title" v-html="title"></header> | ||||||
|  | 			<header v-if="title == null && user">{{ $t('@.enter-username') }}</header> | ||||||
| 			<div class="body" v-if="text" v-html="text"></div> | 			<div class="body" v-if="text" v-html="text"></div> | ||||||
| 			<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input> | 			<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input> | ||||||
| 			<ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></ui-input> | 			<ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></ui-input> | ||||||
|  |  | ||||||
|  | @ -44,6 +44,8 @@ import uiSwitch from './ui/switch.vue'; | ||||||
| import uiRadio from './ui/radio.vue'; | import uiRadio from './ui/radio.vue'; | ||||||
| import uiSelect from './ui/select.vue'; | import uiSelect from './ui/select.vue'; | ||||||
| import uiInfo from './ui/info.vue'; | import uiInfo from './ui/info.vue'; | ||||||
|  | import uiMargin from './ui/margin.vue'; | ||||||
|  | import uiHr from './ui/hr.vue'; | ||||||
| import formButton from './ui/form/button.vue'; | import formButton from './ui/form/button.vue'; | ||||||
| import formRadio from './ui/form/radio.vue'; | import formRadio from './ui/form/radio.vue'; | ||||||
| 
 | 
 | ||||||
|  | @ -91,5 +93,7 @@ Vue.component('ui-switch', uiSwitch); | ||||||
| Vue.component('ui-radio', uiRadio); | Vue.component('ui-radio', uiRadio); | ||||||
| Vue.component('ui-select', uiSelect); | Vue.component('ui-select', uiSelect); | ||||||
| Vue.component('ui-info', uiInfo); | Vue.component('ui-info', uiInfo); | ||||||
|  | Vue.component('ui-margin', uiMargin); | ||||||
|  | Vue.component('ui-hr', uiHr); | ||||||
| Vue.component('form-button', formButton); | Vue.component('form-button', formButton); | ||||||
| Vue.component('form-radio', formRadio); | Vue.component('form-radio', formRadio); | ||||||
|  |  | ||||||
|  | @ -33,7 +33,16 @@ import * as autosize from 'autosize'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	i18n: i18n('common/views/components/messaging-room.form.vue'), | 	i18n: i18n('common/views/components/messaging-room.form.vue'), | ||||||
| 	props: ['user'], | 	props: { | ||||||
|  | 		user: { | ||||||
|  | 			type: Object, | ||||||
|  | 			requird: false, | ||||||
|  | 		}, | ||||||
|  | 		group: { | ||||||
|  | 			type: Object, | ||||||
|  | 			requird: false, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			text: null, | 			text: null, | ||||||
|  | @ -43,7 +52,7 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 	computed: { | 	computed: { | ||||||
| 		draftId(): string { | 		draftId(): string { | ||||||
| 			return this.user.id; | 			return this.user ? 'user:' + this.user.id : 'group:' + this.group.id; | ||||||
| 		}, | 		}, | ||||||
| 		canSend(): boolean { | 		canSend(): boolean { | ||||||
| 			return (this.text != null && this.text != '') || this.file != null; | 			return (this.text != null && this.text != '') || this.file != null; | ||||||
|  | @ -159,7 +168,8 @@ export default Vue.extend({ | ||||||
| 		send() { | 		send() { | ||||||
| 			this.sending = true; | 			this.sending = true; | ||||||
| 			this.$root.api('messaging/messages/create', { | 			this.$root.api('messaging/messages/create', { | ||||||
| 				userId: this.user.id, | 				userId: this.user ? this.user.id : undefined, | ||||||
|  | 				groupId: this.group ? this.group.id : undefined, | ||||||
| 				text: this.text ? this.text : undefined, | 				text: this.text ? this.text : undefined, | ||||||
| 				fileId: this.file ? this.file.id : undefined | 				fileId: this.file ? this.file.id : undefined | ||||||
| 			}).then(message => { | 			}).then(message => { | ||||||
|  |  | ||||||
|  | @ -23,7 +23,12 @@ | ||||||
| 		<div></div> | 		<div></div> | ||||||
| 		<mk-url-preview v-for="url in urls" :url="url" :key="url"/> | 		<mk-url-preview v-for="url in urls" :url="url" :key="url"/> | ||||||
| 		<footer> | 		<footer> | ||||||
| 			<span class="read" v-if="isMe && message.isRead">{{ $t('is-read') }}</span> | 			<template v-if="isGroup"> | ||||||
|  | 				<span class="read" v-if="message.reads.length > 0">{{ $t('is-read') }} {{ message.reads.length }}</span> | ||||||
|  | 			</template> | ||||||
|  | 			<template v-else> | ||||||
|  | 				<span class="read" v-if="isMe && message.isRead">{{ $t('is-read') }}</span> | ||||||
|  | 			</template> | ||||||
| 			<mk-time :time="message.createdAt"/> | 			<mk-time :time="message.createdAt"/> | ||||||
| 			<template v-if="message.is_edited"><fa icon="pencil-alt"/></template> | 			<template v-if="message.is_edited"><fa icon="pencil-alt"/></template> | ||||||
| 		</footer> | 		</footer> | ||||||
|  | @ -42,6 +47,9 @@ export default Vue.extend({ | ||||||
| 	props: { | 	props: { | ||||||
| 		message: { | 		message: { | ||||||
| 			required: true | 			required: true | ||||||
|  | 		}, | ||||||
|  | 		isGroup: { | ||||||
|  | 			required: false | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 	computed: { | 	computed: { | ||||||
|  |  | ||||||
|  | @ -4,14 +4,14 @@ | ||||||
| 	@drop.prevent.stop="onDrop" | 	@drop.prevent.stop="onDrop" | ||||||
| > | > | ||||||
| 	<div class="body"> | 	<div class="body"> | ||||||
| 		<p class="init" v-if="init"><fa icon="spinner .spin"/>{{ $t('@.loading') }}</p> | 		<p class="init" v-if="init"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}</p> | ||||||
| 		<p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ $t('empty') }}</p> | 		<p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ user ? $t('not-talked-user') : $t('not-talked-group') }}</p> | ||||||
| 		<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('no-history') }}</p> | 		<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('no-history') }}</p> | ||||||
| 		<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages"> | 		<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages"> | ||||||
| 			<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }} | 			<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }} | ||||||
| 		</button> | 		</button> | ||||||
| 		<template v-for="(message, i) in _messages"> | 		<template v-for="(message, i) in _messages"> | ||||||
| 			<x-message :message="message" :key="message.id"/> | 			<x-message :message="message" :key="message.id" :is-group="group != null"/> | ||||||
| 			<p class="date" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date"> | 			<p class="date" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date"> | ||||||
| 				<span>{{ _messages[i + 1]._datetext }}</span> | 				<span>{{ _messages[i + 1]._datetext }}</span> | ||||||
| 			</p> | 			</p> | ||||||
|  | @ -23,7 +23,7 @@ | ||||||
| 				<button @click="onIndicatorClick"><i><fa :icon="faArrowCircleDown"/></i>{{ $t('new-message') }}</button> | 				<button @click="onIndicatorClick"><i><fa :icon="faArrowCircleDown"/></i>{{ $t('new-message') }}</button> | ||||||
| 			</div> | 			</div> | ||||||
| 		</transition> | 		</transition> | ||||||
| 		<x-form :user="user" ref="form"/> | 		<x-form :user="user" :group="group" ref="form"/> | ||||||
| 	</footer> | 	</footer> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  | @ -34,17 +34,30 @@ import i18n from '../../../i18n'; | ||||||
| import XMessage from './messaging-room.message.vue'; | import XMessage from './messaging-room.message.vue'; | ||||||
| import XForm from './messaging-room.form.vue'; | import XForm from './messaging-room.form.vue'; | ||||||
| import { url } from '../../../config'; | import { url } from '../../../config'; | ||||||
| import { faArrowCircleDown } from '@fortawesome/free-solid-svg-icons'; | import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faFlag } from '@fortawesome/free-regular-svg-icons'; |  | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	i18n: i18n('common/views/components/messaging-room.vue'), | 	i18n: i18n('common/views/components/messaging-room.vue'), | ||||||
|  | 
 | ||||||
| 	components: { | 	components: { | ||||||
| 		XMessage, | 		XMessage, | ||||||
| 		XForm | 		XForm | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: ['user', 'isNaked'], | 	props: { | ||||||
|  | 		user: { | ||||||
|  | 			type: Object, | ||||||
|  | 			requird: false, | ||||||
|  | 		}, | ||||||
|  | 		group: { | ||||||
|  | 			type: Object, | ||||||
|  | 			requird: false, | ||||||
|  | 		}, | ||||||
|  | 		isNaked: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			requird: false, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
| 
 | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  | @ -76,7 +89,10 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		this.connection = this.$root.stream.connectToChannel('messaging', { otherparty: this.user.id }); | 		this.connection = this.$root.stream.connectToChannel('messaging', { | ||||||
|  | 			otherparty: this.user ? this.user.id : undefined, | ||||||
|  | 			group: this.group ? this.group.id : undefined, | ||||||
|  | 		}); | ||||||
| 
 | 
 | ||||||
| 		this.connection.on('message', this.onMessage); | 		this.connection.on('message', this.onMessage); | ||||||
| 		this.connection.on('read', this.onRead); | 		this.connection.on('read', this.onRead); | ||||||
|  | @ -147,7 +163,8 @@ export default Vue.extend({ | ||||||
| 				const max = this.existMoreMessages ? 20 : 10; | 				const max = this.existMoreMessages ? 20 : 10; | ||||||
| 
 | 
 | ||||||
| 				this.$root.api('messaging/messages', { | 				this.$root.api('messaging/messages', { | ||||||
| 					userId: this.user.id, | 					userId: this.user ? this.user.id : undefined, | ||||||
|  | 					groupId: this.group ? this.group.id : undefined, | ||||||
| 					limit: max + 1, | 					limit: max + 1, | ||||||
| 					untilId: this.existMoreMessages ? this.messages[0].id : undefined | 					untilId: this.existMoreMessages ? this.messages[0].id : undefined | ||||||
| 				}).then(messages => { | 				}).then(messages => { | ||||||
|  | @ -199,12 +216,21 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onRead(ids) { | 		onRead(x) { | ||||||
| 			if (!Array.isArray(ids)) ids = [ids]; | 			if (this.user) { | ||||||
| 			for (const id of ids) { | 				if (!Array.isArray(x)) x = [x]; | ||||||
| 				if (this.messages.some(x => x.id == id)) { | 				for (const id of x) { | ||||||
| 					const exist = this.messages.map(x => x.id).indexOf(id); | 					if (this.messages.some(x => x.id == id)) { | ||||||
| 					this.messages[exist].isRead = true; | 						const exist = this.messages.map(x => x.id).indexOf(id); | ||||||
|  | 						this.messages[exist].isRead = true; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} else if (this.group) { | ||||||
|  | 				for (const id of x.ids) { | ||||||
|  | 					if (this.messages.some(x => x.id == id)) { | ||||||
|  | 						const exist = this.messages.map(x => x.id).indexOf(id); | ||||||
|  | 						this.messages[exist].reads.push(x.userId); | ||||||
|  | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | @ -21,36 +21,62 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="history" v-if="messages.length > 0"> | 	<div class="history" v-if="messages.length > 0"> | ||||||
| 		<template> | 		<div class="title">{{ $t('user') }}</div> | ||||||
| 			<a v-for="message in messages" | 		<a v-for="message in messages" | ||||||
| 				class="user" | 			class="user" | ||||||
| 				:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" | 			:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" | ||||||
| 				:data-is-me="isMe(message)" | 			:data-is-me="isMe(message)" | ||||||
| 				:data-is-read="message.isRead" | 			:data-is-read="message.isRead" | ||||||
| 				@click.prevent="navigate(isMe(message) ? message.recipient : message.user)" | 			@click.prevent="navigate(isMe(message) ? message.recipient : message.user)" | ||||||
| 				:key="message.id" | 			:key="message.id" | ||||||
| 			> | 		> | ||||||
| 				<div> | 			<div> | ||||||
| 					<mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/> | 				<mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/> | ||||||
| 					<header> | 				<header> | ||||||
| 						<span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span> | 					<span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span> | ||||||
| 						<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span> | 					<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span> | ||||||
| 						<mk-time :time="message.createdAt"/> | 					<mk-time :time="message.createdAt"/> | ||||||
| 					</header> | 				</header> | ||||||
| 					<div class="body"> | 				<div class="body"> | ||||||
| 						<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p> | 					<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p> | ||||||
| 					</div> |  | ||||||
| 				</div> | 				</div> | ||||||
| 			</a> | 			</div> | ||||||
| 		</template> | 		</a> | ||||||
| 	</div> | 	</div> | ||||||
| 	<p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p> | 	<div class="history" v-if="groupMessages.length > 0"> | ||||||
|  | 		<div class="title">{{ $t('group') }}</div> | ||||||
|  | 		<a v-for="message in groupMessages" | ||||||
|  | 			class="user" | ||||||
|  | 			:href="`/i/messaging/group/${message.groupId}`" | ||||||
|  | 			:data-is-me="isMe(message)" | ||||||
|  | 			:data-is-read="message.reads.includes($store.state.i.id)" | ||||||
|  | 			@click.prevent="navigateGroup(message.group)" | ||||||
|  | 			:key="message.id" | ||||||
|  | 		> | ||||||
|  | 			<div> | ||||||
|  | 				<mk-avatar class="avatar" :user="message.user"/> | ||||||
|  | 				<header> | ||||||
|  | 					<span class="name">{{ message.group.name }}</span> | ||||||
|  | 					<mk-time :time="message.createdAt"/> | ||||||
|  | 				</header> | ||||||
|  | 				<div class="body"> | ||||||
|  | 					<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</a> | ||||||
|  | 	</div> | ||||||
|  | 	<p class="no-history" v-if="!fetching && (messages.length == 0 && groupMessages.length == 0)">{{ $t('no-history') }}</p> | ||||||
| 	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | 	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p> | ||||||
|  | 	<ui-margin> | ||||||
|  | 		<ui-button @click="startUser()"><fa :icon="faUser"/> {{ $t('start-with-user') }}</ui-button> | ||||||
|  | 		<ui-button @click="startGroup()"><fa :icon="faUsers"/> {{ $t('start-with-group') }}</ui-button> | ||||||
|  | 	</ui-margin> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
|  | import { faUser, faUsers } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import i18n from '../../../i18n'; | import i18n from '../../../i18n'; | ||||||
| import getAcct from '../../../../../misc/acct/render'; | import getAcct from '../../../../../misc/acct/render'; | ||||||
| 
 | 
 | ||||||
|  | @ -71,9 +97,11 @@ export default Vue.extend({ | ||||||
| 			fetching: true, | 			fetching: true, | ||||||
| 			moreFetching: false, | 			moreFetching: false, | ||||||
| 			messages: [], | 			messages: [], | ||||||
|  | 			groupMessages: [], | ||||||
| 			q: null, | 			q: null, | ||||||
| 			result: [], | 			result: [], | ||||||
| 			connection: null | 			connection: null, | ||||||
|  | 			faUser, faUsers | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	mounted() { | 	mounted() { | ||||||
|  | @ -82,9 +110,12 @@ export default Vue.extend({ | ||||||
| 		this.connection.on('message', this.onMessage); | 		this.connection.on('message', this.onMessage); | ||||||
| 		this.connection.on('read', this.onRead); | 		this.connection.on('read', this.onRead); | ||||||
| 
 | 
 | ||||||
| 		this.$root.api('messaging/history').then(messages => { | 		this.$root.api('messaging/history', { group: false }).then(messages => { | ||||||
| 			this.messages = messages; | 			this.$root.api('messaging/history', { group: true }).then(groupMessages => { | ||||||
| 			this.fetching = false; | 				this.messages = messages; | ||||||
|  | 				this.groupMessages = groupMessages; | ||||||
|  | 				this.fetching = false; | ||||||
|  | 			}); | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
| 	beforeDestroy() { | 	beforeDestroy() { | ||||||
|  | @ -96,16 +127,27 @@ export default Vue.extend({ | ||||||
| 			return message.userId == this.$store.state.i.id; | 			return message.userId == this.$store.state.i.id; | ||||||
| 		}, | 		}, | ||||||
| 		onMessage(message) { | 		onMessage(message) { | ||||||
| 			this.messages = this.messages.filter(m => !( | 			if (message.recipientId) { | ||||||
| 				(m.recipientId == message.recipientId && m.userId == message.userId) || | 				this.messages = this.messages.filter(m => !( | ||||||
| 				(m.recipientId == message.userId && m.userId == message.recipientId))); | 					(m.recipientId == message.recipientId && m.userId == message.userId) || | ||||||
|  | 					(m.recipientId == message.userId && m.userId == message.recipientId))); | ||||||
| 
 | 
 | ||||||
| 			this.messages.unshift(message); | 				this.messages.unshift(message); | ||||||
|  | 			} else if (message.groupId) { | ||||||
|  | 				this.groupMessages = this.groupMessages.filter(m => m.groupId !== message.groupId); | ||||||
|  | 				this.groupMessages.unshift(message); | ||||||
|  | 			} | ||||||
| 		}, | 		}, | ||||||
| 		onRead(ids) { | 		onRead(ids) { | ||||||
| 			for (const id of ids) { | 			for (const id of ids) { | ||||||
| 				const found = this.messages.find(m => m.id == id); | 				const found = this.messages.find(m => m.id == id); | ||||||
| 				if (found) found.isRead = true; | 				if (found) { | ||||||
|  | 					if (found.recipientId) { | ||||||
|  | 						found.isRead = true; | ||||||
|  | 					} else if (found.groupId) { | ||||||
|  | 						found.reads.push(this.$store.state.i.id); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		search() { | 		search() { | ||||||
|  | @ -125,6 +167,9 @@ export default Vue.extend({ | ||||||
| 		navigate(user) { | 		navigate(user) { | ||||||
| 			this.$emit('navigate', user); | 			this.$emit('navigate', user); | ||||||
| 		}, | 		}, | ||||||
|  | 		navigateGroup(group) { | ||||||
|  | 			this.$emit('navigateGroup', group); | ||||||
|  | 		}, | ||||||
| 		onSearchKeydown(e) { | 		onSearchKeydown(e) { | ||||||
| 			switch (e.which) { | 			switch (e.which) { | ||||||
| 				case 9: // [TAB] | 				case 9: // [TAB] | ||||||
|  | @ -161,6 +206,30 @@ export default Vue.extend({ | ||||||
| 					(list.childNodes[i].nextElementSibling || list.childNodes[0]).focus(); | 					(list.childNodes[i].nextElementSibling || list.childNodes[0]).focus(); | ||||||
| 					break; | 					break; | ||||||
| 			} | 			} | ||||||
|  | 		}, | ||||||
|  | 		async startUser() { | ||||||
|  | 			const { result: user } = await this.$root.dialog({ | ||||||
|  | 				user: { | ||||||
|  | 					local: true | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 			if (user == null) return; | ||||||
|  | 			this.navigate(user); | ||||||
|  | 		}, | ||||||
|  | 		async startGroup() { | ||||||
|  | 			const groups = await this.$root.api('users/groups/joined'); | ||||||
|  | 			const { canceled, result: group } = await this.$root.dialog({ | ||||||
|  | 				type: null, | ||||||
|  | 				title: this.$t('select-group'), | ||||||
|  | 				select: { | ||||||
|  | 					items: groups.map(group => ({ | ||||||
|  | 						value: group, text: group.name | ||||||
|  | 					})) | ||||||
|  | 				}, | ||||||
|  | 				showCancelButton: true | ||||||
|  | 			}); | ||||||
|  | 			if (canceled) return; | ||||||
|  | 			this.navigateGroup(group); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | @ -173,6 +242,9 @@ export default Vue.extend({ | ||||||
| 		font-size 0.8em | 		font-size 0.8em | ||||||
| 
 | 
 | ||||||
| 		> .history | 		> .history | ||||||
|  | 			> .title | ||||||
|  | 				padding 8px | ||||||
|  | 
 | ||||||
| 			> a | 			> a | ||||||
| 				&:last-child | 				&:last-child | ||||||
| 					border-bottom none | 					border-bottom none | ||||||
|  | @ -311,6 +383,13 @@ export default Vue.extend({ | ||||||
| 						color rgba(#000, 0.3) | 						color rgba(#000, 0.3) | ||||||
| 
 | 
 | ||||||
| 	> .history | 	> .history | ||||||
|  | 		> .title | ||||||
|  | 			padding 6px 16px | ||||||
|  | 			margin 0 auto | ||||||
|  | 			max-width 500px | ||||||
|  | 			background rgba(0, 0, 0, 0.05) | ||||||
|  | 			color var(--text) | ||||||
|  | 			font-size 85% | ||||||
| 
 | 
 | ||||||
| 		> a | 		> a | ||||||
| 			display block | 			display block | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								src/client/app/common/views/components/ui/hr.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/client/app/common/views/components/ui/hr.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | ||||||
|  | <template> | ||||||
|  | <div class="evrzpitu"></div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | export default Vue.extend({}); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="stylus" scoped> | ||||||
|  | .evrzpitu | ||||||
|  | 	margin 16px 0 | ||||||
|  | 	border-bottom solid var(--lineWidth) var(--faceDivider) | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
							
								
								
									
										16
									
								
								src/client/app/common/views/components/ui/margin.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/client/app/common/views/components/ui/margin.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | <template> | ||||||
|  | <div class="zdcrxcne"> | ||||||
|  | 	<slot></slot> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | export default Vue.extend({}); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="stylus" scoped> | ||||||
|  | .zdcrxcne | ||||||
|  | 	margin 16px | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
|  | @ -1,95 +0,0 @@ | ||||||
| <template> |  | ||||||
| <div class="xkxvokkjlptzyewouewmceqcxhpgzprp"> |  | ||||||
| 	<button class="ui" @click="add">{{ $t('create-list') }}</button> |  | ||||||
| 	<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.name }}</a> |  | ||||||
| </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import i18n from '../../../i18n'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	i18n: i18n('common/views/components/user-lists.vue'), |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			fetching: true, |  | ||||||
| 			lists: [] |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	mounted() { |  | ||||||
| 		this.$root.api('users/lists/list').then(lists => { |  | ||||||
| 			this.fetching = false; |  | ||||||
| 			this.lists = lists; |  | ||||||
| 		}); |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		add() { |  | ||||||
| 			this.$root.dialog({ |  | ||||||
| 				title: this.$t('list-name'), |  | ||||||
| 				input: true |  | ||||||
| 			}).then(async ({ canceled, result: name }) => { |  | ||||||
| 				if (canceled) return; |  | ||||||
| 				const list = await this.$root.api('users/lists/create', { |  | ||||||
| 					name |  | ||||||
| 				}); |  | ||||||
| 
 |  | ||||||
| 				this.lists.push(list) |  | ||||||
| 				this.$emit('choosen', list); |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 		choice(list) { |  | ||||||
| 			this.$emit('choosen', list); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="stylus" scoped> |  | ||||||
| .xkxvokkjlptzyewouewmceqcxhpgzprp |  | ||||||
| 	padding 16px |  | ||||||
| 	background: var(--bg) |  | ||||||
| 
 |  | ||||||
| 	> button |  | ||||||
| 		display block |  | ||||||
| 		margin-bottom 16px |  | ||||||
| 		color var(--primaryForeground) |  | ||||||
| 		background var(--primary) |  | ||||||
| 		width 100% |  | ||||||
| 		border-radius 38px |  | ||||||
| 		user-select none |  | ||||||
| 		cursor pointer |  | ||||||
| 		padding 0 16px |  | ||||||
| 		min-width 100px |  | ||||||
| 		line-height 38px |  | ||||||
| 		font-size 14px |  | ||||||
| 		font-weight 700 |  | ||||||
| 
 |  | ||||||
| 		&:hover |  | ||||||
| 			background var(--primaryLighten10) |  | ||||||
| 
 |  | ||||||
| 		&:active |  | ||||||
| 			background var(--primaryDarken10) |  | ||||||
| 
 |  | ||||||
| 	a |  | ||||||
| 		display block |  | ||||||
| 		margin 8px 0 |  | ||||||
| 		padding 8px |  | ||||||
| 		color var(--text) |  | ||||||
| 		background var(--face) |  | ||||||
| 		box-shadow 0 2px 16px var(--reversiListItemShadow) |  | ||||||
| 		border-radius 6px |  | ||||||
| 		cursor pointer |  | ||||||
| 		line-height 32px |  | ||||||
| 
 |  | ||||||
| 		* |  | ||||||
| 			pointer-events none |  | ||||||
| 			user-select none |  | ||||||
| 
 |  | ||||||
| 		&:hover |  | ||||||
| 			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05) |  | ||||||
| 
 |  | ||||||
| 		&:active |  | ||||||
| 			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1) |  | ||||||
| 
 |  | ||||||
| </style> |  | ||||||
|  | @ -27,7 +27,7 @@ export default Vue.extend({ | ||||||
| 			text: this.$t('push-to-list'), | 			text: this.$t('push-to-list'), | ||||||
| 			action: this.pushList | 			action: this.pushList | ||||||
| 		}] as any; | 		}] as any; | ||||||
| 		 | 
 | ||||||
| 		if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) { | 		if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) { | ||||||
| 			menu = menu.concat([null, { | 			menu = menu.concat([null, { | ||||||
| 				icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'], | 				icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'], | ||||||
|  |  | ||||||
|  | @ -1,34 +1,45 @@ | ||||||
| <template> | <template> | ||||||
| <x-column> | <x-column> | ||||||
| 	<template #header> | 	<template #header> | ||||||
| 		<fa :icon="faHashtag"/>{{ $t('@.explore') }} | 		<fa :icon="icon"/>{{ title }} | ||||||
| 	</template> | 	</template> | ||||||
| 
 | 
 | ||||||
| 	<div> | 	<div> | ||||||
| 		<x-explore v-bind="$attrs"/> | 		<component :is="component" @init="init" v-bind="$attrs"/> | ||||||
| 	</div> | 	</div> | ||||||
| </x-column> | </x-column> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import i18n from '../../../i18n'; |  | ||||||
| import XColumn from './deck.column.vue'; | import XColumn from './deck.column.vue'; | ||||||
| import XExplore from '../../../common/views/pages/explore.vue'; |  | ||||||
| import { faHashtag } from '@fortawesome/free-solid-svg-icons'; |  | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	i18n: i18n(), |  | ||||||
| 
 |  | ||||||
| 	components: { | 	components: { | ||||||
| 		XColumn, | 		XColumn, | ||||||
| 		XExplore, | 	}, | ||||||
|  | 
 | ||||||
|  | 	props: { | ||||||
|  | 		component: { | ||||||
|  | 			required: true | ||||||
|  | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			faHashtag | 			title: null, | ||||||
|  | 			icon: null, | ||||||
| 		}; | 		}; | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	mounted() { | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		init(v) { | ||||||
|  | 			this.title = v.title; | ||||||
|  | 			this.icon = v.icon; | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  | @ -116,6 +116,10 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	created() { | 	created() { | ||||||
|  | 		this.$emit('init', { | ||||||
|  | 			title: this.$t('@.explore'), | ||||||
|  | 			icon: faHashtag | ||||||
|  | 		}); | ||||||
| 		this.$root.api('hashtags/list', { | 		this.$root.api('hashtags/list', { | ||||||
| 			sort: '+attachedLocalUsers', | 			sort: '+attachedLocalUsers', | ||||||
| 			attachedToLocalUserOnly: true, | 			attachedToLocalUserOnly: true, | ||||||
|  |  | ||||||
|  | @ -1,27 +1,30 @@ | ||||||
| <template> | <template> | ||||||
| <mk-ui> | <div> | ||||||
| 	<template #header><fa :icon="['far', 'envelope']"/>{{ $t('title') }}</template> | 	<ui-container :body-togglable="true"> | ||||||
| 
 | 		<template #header>{{ $t('received-follow-requests') }}</template> | ||||||
| 	<main> | 		<div v-if="!fetching"> | ||||||
| 		<div v-for="req in requests"> | 			<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="mcbzkkaw"> | ||||||
| 			<router-link :key="req.id" :to="req.follower | userPage"> | 				<div v-for="req in requests"> | ||||||
| 				<mk-user-name :user="req.follower"/> | 					<router-link :key="req.id" :to="req.follower | userPage"> | ||||||
| 			</router-link> | 						<mk-user-name :user="req.follower"/> | ||||||
| 			<span> | 					</router-link> | ||||||
| 				<a @click="accept(req.follower)">{{ $t('accept') }}</a>|<a @click="reject(req.follower)">{{ $t('reject') }}</a> | 					<span> | ||||||
| 			</span> | 						<a @click="accept(req.follower)">{{ $t('accept') }}</a>|<a @click="reject(req.follower)">{{ $t('reject') }}</a> | ||||||
|  | 					</span> | ||||||
|  | 				</div> | ||||||
|  | 			</sequential-entrance> | ||||||
| 		</div> | 		</div> | ||||||
| 	</main> | 	</ui-container> | ||||||
| </mk-ui> | </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 Progress from '../../../common/scripts/loading'; | import Progress from '../../scripts/loading'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	i18n: i18n('mobile/views/pages/received-follow-requests.vue'), | 	i18n: i18n('common/views/pages/follow-requests.vue'), | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			fetching: true, | 			fetching: true, | ||||||
|  | @ -29,14 +32,10 @@ export default Vue.extend({ | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		document.title = this.$t('title'); |  | ||||||
| 
 |  | ||||||
| 		Progress.start(); | 		Progress.start(); | ||||||
| 
 |  | ||||||
| 		this.$root.api('following/requests/list').then(requests => { | 		this.$root.api('following/requests/list').then(requests => { | ||||||
| 			this.fetching = false; | 			this.fetching = false; | ||||||
| 			this.requests = requests; | 			this.requests = requests; | ||||||
| 
 |  | ||||||
| 			Progress.done(); | 			Progress.done(); | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
|  | @ -56,7 +55,7 @@ export default Vue.extend({ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="stylus" scoped> | <style lang="stylus" scoped> | ||||||
| main | .mcbzkkaw | ||||||
| 	> div | 	> div | ||||||
| 		display flex | 		display flex | ||||||
| 		padding 16px | 		padding 16px | ||||||
|  | @ -50,6 +50,11 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 	created() { | 	created() { | ||||||
| 		this.fetch(); | 		this.fetch(); | ||||||
|  | 
 | ||||||
|  | 		this.$emit('init', { | ||||||
|  | 			title: this.$t('@.pages'), | ||||||
|  | 			icon: faStickyNote | ||||||
|  | 		}); | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		async fetch() { | 		async fetch() { | ||||||
|  |  | ||||||
							
								
								
									
										180
									
								
								src/client/app/common/views/pages/user-group-editor.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								src/client/app/common/views/pages/user-group-editor.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,180 @@ | ||||||
|  | <template> | ||||||
|  | <div class="ivrbakop"> | ||||||
|  | 	<ui-container v-if="group"> | ||||||
|  | 		<template #header><fa :icon="faUsers"/> {{ group.name }}</template> | ||||||
|  | 
 | ||||||
|  | 		<section> | ||||||
|  | 			<ui-margin> | ||||||
|  | 				<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> | ||||||
|  | 				<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> | ||||||
|  | 			</ui-margin> | ||||||
|  | 		</section> | ||||||
|  | 	</ui-container> | ||||||
|  | 
 | ||||||
|  | 	<ui-container> | ||||||
|  | 		<template #header><fa :icon="faUsers"/> {{ $t('users') }}</template> | ||||||
|  | 
 | ||||||
|  | 		<section> | ||||||
|  | 			<ui-margin> | ||||||
|  | 				<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add-user') }}</ui-button> | ||||||
|  | 			</ui-margin> | ||||||
|  | 			<sequential-entrance animation="entranceFromTop" delay="25"> | ||||||
|  | 				<div class="kjlrfbes" v-for="user in users"> | ||||||
|  | 					<div> | ||||||
|  | 						<a :href="user | userPage"> | ||||||
|  | 							<mk-avatar class="avatar" :user="user" :disable-link="true"/> | ||||||
|  | 						</a> | ||||||
|  | 					</div> | ||||||
|  | 					<div> | ||||||
|  | 						<header> | ||||||
|  | 							<b><mk-user-name :user="user"/></b> | ||||||
|  | 							<span class="username">@{{ user | acct }}</span> | ||||||
|  | 						</header> | ||||||
|  | 						<div> | ||||||
|  | 							<a @click="remove(user)">{{ $t('remove-user') }}</a> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</sequential-entrance> | ||||||
|  | 		</section> | ||||||
|  | 	</ui-container> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import i18n from '../../../i18n'; | ||||||
|  | import { faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	i18n: i18n('common/views/components/user-group-editor.vue'), | ||||||
|  | 
 | ||||||
|  | 	props: { | ||||||
|  | 		groupId: { | ||||||
|  | 			required: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			group: null, | ||||||
|  | 			users: [], | ||||||
|  | 			faICursor, faTrashAlt, faUsers, faPlus | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	created() { | ||||||
|  | 		this.$root.api('users/groups/show', { | ||||||
|  | 			groupId: this.groupId | ||||||
|  | 		}).then(group => { | ||||||
|  | 			this.group = group; | ||||||
|  | 			this.fetchUsers(); | ||||||
|  | 			this.$emit('init', { | ||||||
|  | 				title: this.group.name, | ||||||
|  | 				icon: faUsers | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		fetchUsers() { | ||||||
|  | 			this.$root.api('users/show', { | ||||||
|  | 				userIds: this.group.userIds | ||||||
|  | 			}).then(users => { | ||||||
|  | 				this.users = users; | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		rename() { | ||||||
|  | 			this.$root.dialog({ | ||||||
|  | 				title: this.$t('rename'), | ||||||
|  | 				input: { | ||||||
|  | 					default: this.group.name | ||||||
|  | 				} | ||||||
|  | 			}).then(({ canceled, result: name }) => { | ||||||
|  | 				if (canceled) return; | ||||||
|  | 				this.$root.api('users/groups/update', { | ||||||
|  | 					groupId: this.group.id, | ||||||
|  | 					name: name | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		del() { | ||||||
|  | 			this.$root.dialog({ | ||||||
|  | 				type: 'warning', | ||||||
|  | 				text: this.$t('delete-are-you-sure').replace('$1', this.group.name), | ||||||
|  | 				showCancelButton: true | ||||||
|  | 			}).then(({ canceled }) => { | ||||||
|  | 				if (canceled) return; | ||||||
|  | 
 | ||||||
|  | 				this.$root.api('users/groups/delete', { | ||||||
|  | 					groupId: this.group.id | ||||||
|  | 				}).then(() => { | ||||||
|  | 					this.$root.dialog({ | ||||||
|  | 						type: 'success', | ||||||
|  | 						text: this.$t('deleted') | ||||||
|  | 					}); | ||||||
|  | 				}).catch(e => { | ||||||
|  | 					this.$root.dialog({ | ||||||
|  | 						type: 'error', | ||||||
|  | 						text: e | ||||||
|  | 					}); | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		remove(user: any) { | ||||||
|  | 			this.$root.api('users/groups/pull', { | ||||||
|  | 				groupId: this.group.id, | ||||||
|  | 				userId: user.id | ||||||
|  | 			}).then(() => { | ||||||
|  | 				this.fetchUsers(); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		async add() { | ||||||
|  | 			const { result: user } = await this.$root.dialog({ | ||||||
|  | 				user: { | ||||||
|  | 					local: true | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 			if (user == null) return; | ||||||
|  | 			this.$root.api('users/groups/push', { | ||||||
|  | 				groupId: this.group.id, | ||||||
|  | 				userId: user.id | ||||||
|  | 			}).then(() => { | ||||||
|  | 				this.fetchUsers(); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="stylus" scoped> | ||||||
|  | .ivrbakop | ||||||
|  | 	.kjlrfbes | ||||||
|  | 		display flex | ||||||
|  | 		padding 16px | ||||||
|  | 		border-top solid 1px var(--faceDivider) | ||||||
|  | 
 | ||||||
|  | 		> div:first-child | ||||||
|  | 			> a | ||||||
|  | 				> .avatar | ||||||
|  | 					width 64px | ||||||
|  | 					height 64px | ||||||
|  | 
 | ||||||
|  | 		> div:last-child | ||||||
|  | 			flex 1 | ||||||
|  | 			padding-left 16px | ||||||
|  | 
 | ||||||
|  | 			@media (max-width 500px) | ||||||
|  | 				font-size 14px | ||||||
|  | 
 | ||||||
|  | 			> header | ||||||
|  | 				> .username | ||||||
|  | 					margin-left 8px | ||||||
|  | 					opacity 0.7 | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
							
								
								
									
										63
									
								
								src/client/app/common/views/pages/user-groups.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/client/app/common/views/pages/user-groups.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | ||||||
|  | <template> | ||||||
|  | <ui-container> | ||||||
|  | 	<template #header><fa :icon="faUsers"/> {{ $t('user-groups') }}</template> | ||||||
|  | 	<ui-margin> | ||||||
|  | 		<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-group') }}</ui-button> | ||||||
|  | 	</ui-margin> | ||||||
|  | 	<div class="hwgkdrbl" v-for="group in groups" :key="group.id"> | ||||||
|  | 		<ui-hr/> | ||||||
|  | 		<ui-margin> | ||||||
|  | 			<router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link> | ||||||
|  | 		</ui-margin> | ||||||
|  | 	</div> | ||||||
|  | </ui-container> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import i18n from '../../../i18n'; | ||||||
|  | import { faUsers, faPlus } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	i18n: i18n('common/views/components/user-groups.vue'), | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			fetching: true, | ||||||
|  | 			groups: [], | ||||||
|  | 			faUsers, faPlus | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 	mounted() { | ||||||
|  | 		this.$root.api('users/groups/owned').then(groups => { | ||||||
|  | 			this.fetching = false; | ||||||
|  | 			this.groups = groups; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		this.$emit('init', { | ||||||
|  | 			title: this.$t('user-groups'), | ||||||
|  | 			icon: faUsers | ||||||
|  | 		}); | ||||||
|  | 	}, | ||||||
|  | 	methods: { | ||||||
|  | 		add() { | ||||||
|  | 			this.$root.dialog({ | ||||||
|  | 				title: this.$t('group-name'), | ||||||
|  | 				input: true | ||||||
|  | 			}).then(async ({ canceled, result: name }) => { | ||||||
|  | 				if (canceled) return; | ||||||
|  | 				const list = await this.$root.api('users/groups/create', { | ||||||
|  | 					name | ||||||
|  | 				}); | ||||||
|  | 
 | ||||||
|  | 				this.groups.push(list) | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="stylus" scoped> | ||||||
|  | .hwgkdrbl | ||||||
|  | 	display block | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
|  | @ -1,18 +1,23 @@ | ||||||
| <template> | <template> | ||||||
| <div class="cudqjmnl"> | <div class="cudqjmnl"> | ||||||
| 	<ui-card> | 	<ui-container v-if="list"> | ||||||
| 		<template #title><fa :icon="faList"/> {{ list.name }}</template> | 		<template #header><fa :icon="faListUl"/> {{ list.name }}</template> | ||||||
| 
 | 
 | ||||||
| 		<section> | 		<section class="fwvevrks"> | ||||||
| 			<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> | 			<ui-margin> | ||||||
| 			<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> | 				<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> | ||||||
|  | 				<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> | ||||||
|  | 			</ui-margin> | ||||||
| 		</section> | 		</section> | ||||||
| 	</ui-card> | 	</ui-container> | ||||||
| 
 | 
 | ||||||
| 	<ui-card> | 	<ui-container> | ||||||
| 		<template #title><fa :icon="faUsers"/> {{ $t('users') }}</template> | 		<template #header><fa :icon="faUsers"/> {{ $t('users') }}</template> | ||||||
| 
 | 
 | ||||||
| 		<section> | 		<section> | ||||||
|  | 			<ui-margin> | ||||||
|  | 				<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add-user') }}</ui-button> | ||||||
|  | 			</ui-margin> | ||||||
| 			<sequential-entrance animation="entranceFromTop" delay="25"> | 			<sequential-entrance animation="entranceFromTop" delay="25"> | ||||||
| 				<div class="phcqulfl" v-for="user in users"> | 				<div class="phcqulfl" v-for="user in users"> | ||||||
| 					<div> | 					<div> | ||||||
|  | @ -32,34 +37,44 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 			</sequential-entrance> | 			</sequential-entrance> | ||||||
| 		</section> | 		</section> | ||||||
| 	</ui-card> | 	</ui-container> | ||||||
| </div> | </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 { faList, faICursor, faUsers } from '@fortawesome/free-solid-svg-icons'; | import { faListUl, faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; | import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	i18n: i18n('common/views/components/user-list-editor.vue'), | 	i18n: i18n('common/views/components/user-list-editor.vue'), | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
| 		list: { | 		listId: { | ||||||
| 			required: true | 			required: true | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  | 			list: null, | ||||||
| 			users: [], | 			users: [], | ||||||
| 			faList, faICursor, faTrashAlt, faUsers | 			faListUl, faICursor, faTrashAlt, faUsers, faPlus | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	created() { | ||||||
| 		this.fetchUsers(); | 		this.$root.api('users/lists/show', { | ||||||
|  | 			listId: this.listId | ||||||
|  | 		}).then(list => { | ||||||
|  | 			this.list = list; | ||||||
|  | 			this.fetchUsers(); | ||||||
|  | 			this.$emit('init', { | ||||||
|  | 				title: this.list.name, | ||||||
|  | 				icon: faListUl | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
|  | @ -117,6 +132,21 @@ export default Vue.extend({ | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
| 				this.fetchUsers(); | 				this.fetchUsers(); | ||||||
| 			}); | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		async add() { | ||||||
|  | 			const { result: user } = await this.$root.dialog({ | ||||||
|  | 				user: { | ||||||
|  | 					local: true | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 			if (user == null) return; | ||||||
|  | 			this.$root.api('users/lists/push', { | ||||||
|  | 				listId: this.list.id, | ||||||
|  | 				userId: user.id | ||||||
|  | 			}).then(() => { | ||||||
|  | 				this.fetchUsers(); | ||||||
|  | 			}); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | @ -126,7 +156,7 @@ export default Vue.extend({ | ||||||
| .cudqjmnl | .cudqjmnl | ||||||
| 	.phcqulfl | 	.phcqulfl | ||||||
| 		display flex | 		display flex | ||||||
| 		padding 16px 0 | 		padding 16px | ||||||
| 		border-top solid 1px var(--faceDivider) | 		border-top solid 1px var(--faceDivider) | ||||||
| 
 | 
 | ||||||
| 		> div:first-child | 		> div:first-child | ||||||
							
								
								
									
										63
									
								
								src/client/app/common/views/pages/user-lists.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/client/app/common/views/pages/user-lists.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | ||||||
|  | <template> | ||||||
|  | <ui-container> | ||||||
|  | 	<template #header><fa :icon="faListUl"/> {{ $t('user-lists') }}</template> | ||||||
|  | 	<ui-margin> | ||||||
|  | 		<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-list') }}</ui-button> | ||||||
|  | 	</ui-margin> | ||||||
|  | 	<div class="cpqqyrst" v-for="list in lists" :key="list.id"> | ||||||
|  | 		<ui-hr/> | ||||||
|  | 		<ui-margin> | ||||||
|  | 			<router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link> | ||||||
|  | 		</ui-margin> | ||||||
|  | 	</div> | ||||||
|  | </ui-container> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import i18n from '../../../i18n'; | ||||||
|  | import { faListUl, faPlus } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	i18n: i18n('common/views/components/user-lists.vue'), | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			fetching: true, | ||||||
|  | 			lists: [], | ||||||
|  | 			faListUl, faPlus | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 	mounted() { | ||||||
|  | 		this.$root.api('users/lists/list').then(lists => { | ||||||
|  | 			this.fetching = false; | ||||||
|  | 			this.lists = lists; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		this.$emit('init', { | ||||||
|  | 			title: this.$t('user-lists'), | ||||||
|  | 			icon: faListUl | ||||||
|  | 		}); | ||||||
|  | 	}, | ||||||
|  | 	methods: { | ||||||
|  | 		add() { | ||||||
|  | 			this.$root.dialog({ | ||||||
|  | 				title: this.$t('list-name'), | ||||||
|  | 				input: true | ||||||
|  | 			}).then(async ({ canceled, result: name }) => { | ||||||
|  | 				if (canceled) return; | ||||||
|  | 				const list = await this.$root.api('users/lists/create', { | ||||||
|  | 					name | ||||||
|  | 				}); | ||||||
|  | 
 | ||||||
|  | 				this.lists.push(list) | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="stylus" scoped> | ||||||
|  | .cpqqyrst | ||||||
|  | 	display block | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
|  | @ -22,6 +22,7 @@ import MkShare from '../common/views/pages/share.vue'; | ||||||
| import MkFollow from '../common/views/pages/follow.vue'; | import MkFollow from '../common/views/pages/follow.vue'; | ||||||
| import MkNotFound from '../common/views/pages/not-found.vue'; | import MkNotFound from '../common/views/pages/not-found.vue'; | ||||||
| import MkSettings from './views/pages/settings.vue'; | import MkSettings from './views/pages/settings.vue'; | ||||||
|  | import DeckColumn from '../common/views/deck/deck.column-template.vue'; | ||||||
| 
 | 
 | ||||||
| import Ctx from './views/components/context-menu.vue'; | import Ctx from './views/components/context-menu.vue'; | ||||||
| import PostFormWindow from './views/components/post-form-window.vue'; | import PostFormWindow from './views/components/post-form-window.vue'; | ||||||
|  | @ -138,9 +139,14 @@ init(async (launch, os) => { | ||||||
| 					{ path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, | 					{ path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, | ||||||
| 					{ path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, | 					{ path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, | ||||||
| 					{ path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) }, | 					{ path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) }, | ||||||
| 					{ path: '/explore', name: 'explore', component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) }, | 					{ path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, | ||||||
| 					{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) }, | 					{ path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, | ||||||
| 					{ path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) } | 					{ path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) }, | ||||||
|  | 					{ path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, | ||||||
|  | 					{ path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, | ||||||
|  | 					{ path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) }, | ||||||
|  | 					{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, | ||||||
|  | 					{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) }, | ||||||
| 				]} | 				]} | ||||||
| 				: { path: '/', component: MkHome, children: [ | 				: { path: '/', component: MkHome, children: [ | ||||||
| 					{ path: '', name: 'index', component: MkHomeTimeline }, | 					{ path: '', name: 'index', component: MkHomeTimeline }, | ||||||
|  | @ -157,11 +163,17 @@ init(async (launch, os) => { | ||||||
| 					{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, | 					{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, | ||||||
| 					{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) }, | 					{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) }, | ||||||
| 					{ path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) }, | 					{ path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) }, | ||||||
|  | 					{ path: '/i/lists', component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }, | ||||||
|  | 					{ path: '/i/lists/:listId', props: true, component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default) }, | ||||||
|  | 					{ path: '/i/groups', component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }, | ||||||
|  | 					{ path: '/i/groups/:groupId', props: true, component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default) }, | ||||||
|  | 					{ path: '/i/follow-requests', component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }, | ||||||
| 				]}, | 				]}, | ||||||
| 			{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) }, | 			{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) }, | ||||||
| 			{ path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, | 			{ path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, | ||||||
| 			{ path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, | 			{ path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, | ||||||
| 			{ path: '/i/pages/edit/:pageId', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, | 			{ path: '/i/pages/edit/:pageId', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, | ||||||
|  | 			{ path: '/i/messaging/group/:group', component: MkMessagingRoom }, | ||||||
| 			{ path: '/i/messaging/:user', component: MkMessagingRoom }, | 			{ path: '/i/messaging/:user', component: MkMessagingRoom }, | ||||||
| 			{ path: '/i/drive', component: MkDrive }, | 			{ path: '/i/drive', component: MkDrive }, | ||||||
| 			{ path: '/i/drive/folder/:folder', component: MkDrive }, | 			{ path: '/i/drive/folder/:folder', component: MkDrive }, | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom"> | <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom"> | ||||||
| 	<template #header><fa icon="comments"/> {{ $t('@.messaging') }}: <mk-user-name :user="user"/></template> | 	<template #header><fa icon="comments"/> {{ $t('@.messaging') }}: <mk-user-name v-if="user" :user="user"/><span v-else>{{ group.name }}</span></template> | ||||||
| 	<x-messaging-room :user="user" :class="$style.content"/> | 	<x-messaging-room :user="user" :group="group" :class="$style.content"/> | ||||||
| </mk-window> | </mk-window> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -16,10 +16,14 @@ export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default) | 		XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default) | ||||||
| 	}, | 	}, | ||||||
| 	props: ['user'], | 	props: ['user', 'group'], | ||||||
| 	computed: { | 	computed: { | ||||||
| 		popout(): string { | 		popout(): string { | ||||||
| 			return `${url}/i/messaging/${getAcct(this.user)}`; | 			if (this.user) { | ||||||
|  | 				return `${url}/i/messaging/${getAcct(this.user)}`; | ||||||
|  | 			} else if (this.group) { | ||||||
|  | 				return `${url}/i/messaging/group/${this.group.id}`; | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <mk-window ref="window" width="500px" height="560px" @closed="destroyDom"> | <mk-window ref="window" width="500px" height="560px" @closed="destroyDom"> | ||||||
| 	<template #header :class="$style.header"><fa icon="comments"/>{{ $t('@.messaging') }}</template> | 	<template #header :class="$style.header"><fa icon="comments"/>{{ $t('@.messaging') }}</template> | ||||||
| 	<x-messaging :class="$style.content" @navigate="navigate"/> | 	<x-messaging :class="$style.content" @navigate="navigate" @navigateGroup="navigateGroup"/> | ||||||
| </mk-window> | </mk-window> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -20,6 +20,11 @@ export default Vue.extend({ | ||||||
| 			this.$root.new(MkMessagingRoomWindow, { | 			this.$root.new(MkMessagingRoomWindow, { | ||||||
| 				user: user | 				user: user | ||||||
| 			}); | 			}); | ||||||
|  | 		}, | ||||||
|  | 		navigateGroup(group) { | ||||||
|  | 			this.$root.new(MkMessagingRoomWindow, { | ||||||
|  | 				group: group | ||||||
|  | 			}); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,70 +0,0 @@ | ||||||
| <template> |  | ||||||
| <mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom"> |  | ||||||
| 	<template #header><fa :icon="['far', 'envelope']"/> {{ $t('title') }}</template> |  | ||||||
| 
 |  | ||||||
| 	<div class="slpqaxdoxhvglersgjukmvizkqbmbokc"> |  | ||||||
| 		<div v-for="req in requests"> |  | ||||||
| 			<router-link :key="req.id" :to="req.follower | userPage"> |  | ||||||
| 				<mk-user-name :user="req.follower"/> |  | ||||||
| 			</router-link> |  | ||||||
| 			<span> |  | ||||||
| 				<a @click="accept(req.follower)">{{ $t('accept') }}</a>|<a @click="reject(req.follower)">{{ $t('reject') }}</a> |  | ||||||
| 			</span> |  | ||||||
| 		</div> |  | ||||||
| 	</div> |  | ||||||
| </mk-window> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import i18n from '../../../i18n'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	i18n: i18n('desktop/views/components/received-follow-requests-window.vue'), |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			fetching: true, |  | ||||||
| 			requests: [] |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	mounted() { |  | ||||||
| 		this.$root.api('following/requests/list').then(requests => { |  | ||||||
| 			this.fetching = false; |  | ||||||
| 			this.requests = requests; |  | ||||||
| 		}); |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		accept(user) { |  | ||||||
| 			this.$root.api('following/requests/accept', { userId: user.id }).then(() => { |  | ||||||
| 				this.requests = this.requests.filter(r => r.follower.id != user.id); |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 		reject(user) { |  | ||||||
| 			this.$root.api('following/requests/reject', { userId: user.id }).then(() => { |  | ||||||
| 				this.requests = this.requests.filter(r => r.follower.id != user.id); |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 		close() { |  | ||||||
| 			(this as any).$refs.window.close(); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="stylus" scoped> |  | ||||||
| .slpqaxdoxhvglersgjukmvizkqbmbokc |  | ||||||
| 	padding 16px |  | ||||||
| 
 |  | ||||||
| 	> button |  | ||||||
| 		margin-bottom 16px |  | ||||||
| 
 |  | ||||||
| 	> div |  | ||||||
| 		display flex |  | ||||||
| 		padding 16px |  | ||||||
| 		border solid 1px var(--faceDivider) |  | ||||||
| 		border-radius 4px |  | ||||||
| 
 |  | ||||||
| 		> span |  | ||||||
| 			margin 0 0 0 auto |  | ||||||
| 
 |  | ||||||
| </style> |  | ||||||
|  | @ -28,12 +28,19 @@ | ||||||
| 						<i><fa icon="angle-right"/></i> | 						<i><fa icon="angle-right"/></i> | ||||||
| 					</router-link> | 					</router-link> | ||||||
| 				</li> | 				</li> | ||||||
| 				<li @click="list"> | 				<li> | ||||||
| 					<p> | 					<router-link to="/i/lists"> | ||||||
| 						<i><fa icon="list" fixed-width/></i> | 						<i><fa icon="list" fixed-width/></i> | ||||||
| 						<span>{{ $t('lists') }}</span> | 						<span>{{ $t('lists') }}</span> | ||||||
| 						<i><fa icon="angle-right"/></i> | 						<i><fa icon="angle-right"/></i> | ||||||
| 					</p> | 					</router-link> | ||||||
|  | 				</li> | ||||||
|  | 				<li> | ||||||
|  | 					<router-link to="/i/groups"> | ||||||
|  | 						<i><fa :icon="faUsers" fixed-width/></i> | ||||||
|  | 						<span>{{ $t('groups') }}</span> | ||||||
|  | 						<i><fa icon="angle-right"/></i> | ||||||
|  | 					</router-link> | ||||||
| 				</li> | 				</li> | ||||||
| 				<li> | 				<li> | ||||||
| 					<router-link to="/i/pages"> | 					<router-link to="/i/pages"> | ||||||
|  | @ -42,12 +49,12 @@ | ||||||
| 						<i><fa icon="angle-right"/></i> | 						<i><fa icon="angle-right"/></i> | ||||||
| 					</router-link> | 					</router-link> | ||||||
| 				</li> | 				</li> | ||||||
| 				<li @click="followRequests" v-if="($store.state.i.isLocked || $store.state.i.carefulBot)"> | 				<li v-if="($store.state.i.isLocked || $store.state.i.carefulBot)"> | ||||||
| 					<p> | 					<router-link to="/i/follow-requests"> | ||||||
| 						<i><fa :icon="['far', 'envelope']" fixed-width/></i> | 						<i><fa :icon="['far', 'envelope']" fixed-width/></i> | ||||||
| 						<span>{{ $t('follow-requests') }}<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span> | 						<span>{{ $t('follow-requests') }}<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span> | ||||||
| 						<i><fa icon="angle-right"/></i> | 						<i><fa icon="angle-right"/></i> | ||||||
| 					</p> | 					</router-link> | ||||||
| 				</li> | 				</li> | ||||||
| 			</ul> | 			</ul> | ||||||
| 			<ul> | 			<ul> | ||||||
|  | @ -96,12 +103,10 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import i18n from '../../../i18n'; | import i18n from '../../../i18n'; | ||||||
| import MkUserListsWindow from './user-lists-window.vue'; |  | ||||||
| import MkFollowRequestsWindow from './received-follow-requests-window.vue'; |  | ||||||
| // import MkSettingsWindow from './settings-window.vue'; | // import MkSettingsWindow from './settings-window.vue'; | ||||||
| import MkDriveWindow from './drive-window.vue'; | import MkDriveWindow from './drive-window.vue'; | ||||||
| import contains from '../../../common/scripts/contains'; | import contains from '../../../common/scripts/contains'; | ||||||
| import { faHome, faColumns } from '@fortawesome/free-solid-svg-icons'; | import { faHome, faColumns, faUsers } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons'; | import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
|  | @ -109,7 +114,7 @@ export default Vue.extend({ | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			isOpen: false, | 			isOpen: false, | ||||||
| 			faHome, faColumns, faMoon, faSun, faStickyNote | 			faHome, faColumns, faMoon, faSun, faStickyNote, faUsers | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	computed: { | 	computed: { | ||||||
|  | @ -147,14 +152,6 @@ export default Vue.extend({ | ||||||
| 			this.close(); | 			this.close(); | ||||||
| 			this.$root.new(MkDriveWindow); | 			this.$root.new(MkDriveWindow); | ||||||
| 		}, | 		}, | ||||||
| 		list() { |  | ||||||
| 			this.close(); |  | ||||||
| 			this.$root.new(MkUserListsWindow); |  | ||||||
| 		}, |  | ||||||
| 		followRequests() { |  | ||||||
| 			this.close(); |  | ||||||
| 			this.$root.new(MkFollowRequestsWindow); |  | ||||||
| 		}, |  | ||||||
| 		signout() { | 		signout() { | ||||||
| 			this.$root.signout(); | 			this.$root.signout(); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | @ -72,8 +72,6 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import i18n from '../../../i18n'; | import i18n from '../../../i18n'; | ||||||
| import MkUserListsWindow from './user-lists-window.vue'; |  | ||||||
| import MkFollowRequestsWindow from './received-follow-requests-window.vue'; |  | ||||||
| import MkSettingsWindow from './settings-window.vue'; | import MkSettingsWindow from './settings-window.vue'; | ||||||
| import MkDriveWindow from './drive-window.vue'; | import MkDriveWindow from './drive-window.vue'; | ||||||
| import MkMessagingWindow from './messaging-window.vue'; | import MkMessagingWindow from './messaging-window.vue'; | ||||||
|  |  | ||||||
|  | @ -1,24 +0,0 @@ | ||||||
| <template> |  | ||||||
| <mk-window ref="window" width="450px" height="500px" @closed="destroyDom"> |  | ||||||
| 	<template #header><fa icon="list"/> {{ list.name }}</template> |  | ||||||
| 
 |  | ||||||
| 	<x-editor :list="list"/> |  | ||||||
| </mk-window> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import XEditor from '../../../common/views/components/user-list-editor.vue'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	components: { |  | ||||||
| 		XEditor |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	props: { |  | ||||||
| 		list: { |  | ||||||
| 			required: true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  | @ -1,36 +0,0 @@ | ||||||
| <template> |  | ||||||
| <mk-window ref="window" width="450px" height="500px" @closed="destroyDom"> |  | ||||||
| 	<template #header><fa icon="list"/> {{ $t('title') }}</template> |  | ||||||
| 	<x-lists :class="$style.content" @choosen="choosen"/> |  | ||||||
| </mk-window> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import i18n from '../../../i18n'; |  | ||||||
| import MkUserListWindow from './user-list-window.vue'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	i18n: i18n('desktop/views/components/user-lists-window.vue'), |  | ||||||
| 	components: { |  | ||||||
| 		XLists: () => import('../../../common/views/components/user-lists.vue').then(m => m.default) |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		close() { |  | ||||||
| 			(this as any).$refs.window.close(); |  | ||||||
| 		}, |  | ||||||
| 		choosen(list) { |  | ||||||
| 			this.$root.new(MkUserListWindow, { |  | ||||||
| 				list |  | ||||||
| 			}); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="stylus" module> |  | ||||||
| .content |  | ||||||
| 	height 100% |  | ||||||
| 	overflow auto |  | ||||||
| 
 |  | ||||||
| </style> |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mk-messaging-room-page"> | <div class="mk-messaging-room-page"> | ||||||
| 	<x-messaging-room v-if="user" :user="user" :is-naked="true"/> | 	<x-messaging-room v-if="user || group" :user="user" :group="group" :is-naked="true"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -19,7 +19,8 @@ export default Vue.extend({ | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			fetching: true, | 			fetching: true, | ||||||
| 			user: null | 			user: null, | ||||||
|  | 			group: null | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	watch: { | 	watch: { | ||||||
|  | @ -47,14 +48,25 @@ export default Vue.extend({ | ||||||
| 			Progress.start(); | 			Progress.start(); | ||||||
| 			this.fetching = true; | 			this.fetching = true; | ||||||
| 
 | 
 | ||||||
| 			this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { | 			if (this.$route.params.user) { | ||||||
| 				this.user = user; | 				this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { | ||||||
| 				this.fetching = false; | 					this.user = user; | ||||||
|  | 					this.fetching = false; | ||||||
| 
 | 
 | ||||||
| 				document.title = this.$t('@.messaging') + ': ' + getUserName(this.user); | 					document.title = this.$t('@.messaging') + ': ' + getUserName(this.user); | ||||||
| 
 | 
 | ||||||
| 				Progress.done(); | 					Progress.done(); | ||||||
| 			}); | 				}); | ||||||
|  | 			} else { | ||||||
|  | 				this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => { | ||||||
|  | 					this.group = group; | ||||||
|  | 					this.fetching = false; | ||||||
|  | 
 | ||||||
|  | 					document.title = this.$t('@.messaging') + ': ' + this.group.name; | ||||||
|  | 
 | ||||||
|  | 					Progress.done(); | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 		<template #header><fa icon="comments"/>{{ $t('@.messaging') }}</template> | 		<template #header><fa icon="comments"/>{{ $t('@.messaging') }}</template> | ||||||
| 		<template #func><button @click="add"><fa icon="plus"/></button></template> | 		<template #func><button @click="add"><fa icon="plus"/></button></template> | ||||||
| 
 | 
 | ||||||
| 		<x-messaging ref="index" compact @navigate="navigate"/> | 		<x-messaging ref="index" compact @navigate="navigate" @navigateGroup="navigateGroup"/> | ||||||
| 	</ui-container> | 	</ui-container> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  | @ -31,6 +31,11 @@ export default define({ | ||||||
| 				user: user | 				user: user | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  | 		navigateGroup(group) { | ||||||
|  | 			this.$root.new(MkMessagingRoomWindow, { | ||||||
|  | 				group: group | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
| 		add() { | 		add() { | ||||||
| 			this.$root.new(MkMessagingWindow); | 			this.$root.new(MkMessagingWindow); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | @ -18,17 +18,16 @@ import MkDrive from './views/pages/drive.vue'; | ||||||
| import MkWidgets from './views/pages/widgets.vue'; | import MkWidgets from './views/pages/widgets.vue'; | ||||||
| import MkMessaging from './views/pages/messaging.vue'; | import MkMessaging from './views/pages/messaging.vue'; | ||||||
| import MkMessagingRoom from './views/pages/messaging-room.vue'; | import MkMessagingRoom from './views/pages/messaging-room.vue'; | ||||||
| import MkReceivedFollowRequests from './views/pages/received-follow-requests.vue'; |  | ||||||
| import MkNote from './views/pages/note.vue'; | import MkNote from './views/pages/note.vue'; | ||||||
| import MkSearch from './views/pages/search.vue'; | import MkSearch from './views/pages/search.vue'; | ||||||
| import MkFavorites from './views/pages/favorites.vue'; | import MkFavorites from './views/pages/favorites.vue'; | ||||||
| import MkUserLists from './views/pages/user-lists.vue'; | import UI from './views/pages/ui.vue'; | ||||||
| import MkUserList from './views/pages/user-list.vue'; |  | ||||||
| import MkReversi from './views/pages/games/reversi.vue'; | import MkReversi from './views/pages/games/reversi.vue'; | ||||||
| import MkTag from './views/pages/tag.vue'; | import MkTag from './views/pages/tag.vue'; | ||||||
| import MkShare from '../common/views/pages/share.vue'; | import MkShare from '../common/views/pages/share.vue'; | ||||||
| import MkFollow from '../common/views/pages/follow.vue'; | import MkFollow from '../common/views/pages/follow.vue'; | ||||||
| import MkNotFound from '../common/views/pages/not-found.vue'; | import MkNotFound from '../common/views/pages/not-found.vue'; | ||||||
|  | import DeckColumn from '../common/views/deck/deck.column-template.vue'; | ||||||
| 
 | 
 | ||||||
| import PostForm from './views/components/post-form-dialog.vue'; | import PostForm from './views/components/post-form-dialog.vue'; | ||||||
| import FileChooser from './views/components/drive-file-chooser.vue'; | import FileChooser from './views/components/drive-file-chooser.vue'; | ||||||
|  | @ -125,9 +124,14 @@ init((launch, os) => { | ||||||
| 					{ path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, | 					{ path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, | ||||||
| 					{ path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, | 					{ path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, | ||||||
| 					{ path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) }, | 					{ path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) }, | ||||||
| 					{ path: '/explore', name: 'explore', component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) }, | 					{ path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, | ||||||
| 					{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) }, | 					{ path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, | ||||||
| 					{ path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) } | 					{ path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) }, | ||||||
|  | 					{ path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, | ||||||
|  | 					{ path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, | ||||||
|  | 					{ path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) }, | ||||||
|  | 					{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, | ||||||
|  | 					{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) }, | ||||||
| 				]}] | 				]}] | ||||||
| 			: [ | 			: [ | ||||||
| 				{ path: '/', name: 'index', component: MkIndex }, | 				{ path: '/', name: 'index', component: MkIndex }, | ||||||
|  | @ -135,12 +139,15 @@ init((launch, os) => { | ||||||
| 			{ path: '/signup', name: 'signup', component: MkSignup }, | 			{ path: '/signup', name: 'signup', component: MkSignup }, | ||||||
| 			{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, | 			{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, | ||||||
| 			{ path: '/i/favorites', name: 'favorites', component: MkFavorites }, | 			{ path: '/i/favorites', name: 'favorites', component: MkFavorites }, | ||||||
| 			{ path: '/i/pages', name: 'pages', component: () => import('./views/pages/pages.vue').then(m => m.default) }, | 			{ path: '/i/pages', name: 'pages', component: UI, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, | ||||||
| 			{ path: '/i/lists', name: 'user-lists', component: MkUserLists }, | 			{ path: '/i/lists', name: 'user-lists', component: UI, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, | ||||||
| 			{ path: '/i/lists/:list', name: 'user-list', component: MkUserList }, | 			{ path: '/i/lists/:list', component: UI, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.list }) }, | ||||||
| 			{ path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests }, | 			{ path: '/i/groups', name: 'user-groups', component: UI, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, | ||||||
|  | 			{ path: '/i/groups/:group', component: UI, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.group }) }, | ||||||
|  | 			{ path: '/i/follow-requests', name: 'follow-requests', component: UI, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, | ||||||
| 			{ path: '/i/widgets', name: 'widgets', component: MkWidgets }, | 			{ path: '/i/widgets', name: 'widgets', component: MkWidgets }, | ||||||
| 			{ path: '/i/messaging', name: 'messaging', component: MkMessaging }, | 			{ path: '/i/messaging', name: 'messaging', component: MkMessaging }, | ||||||
|  | 			{ path: '/i/messaging/group/:group', component: MkMessagingRoom }, | ||||||
| 			{ path: '/i/messaging/:user', component: MkMessagingRoom }, | 			{ path: '/i/messaging/:user', component: MkMessagingRoom }, | ||||||
| 			{ path: '/i/drive', name: 'drive', component: MkDrive }, | 			{ path: '/i/drive', name: 'drive', component: MkDrive }, | ||||||
| 			{ path: '/i/drive/folder/:folder', component: MkDrive }, | 			{ path: '/i/drive/folder/:folder', component: MkDrive }, | ||||||
|  | @ -151,8 +158,8 @@ init((launch, os) => { | ||||||
| 			{ path: '/search', component: MkSearch }, | 			{ path: '/search', component: MkSearch }, | ||||||
| 			{ path: '/tags/:tag', component: MkTag }, | 			{ path: '/tags/:tag', component: MkTag }, | ||||||
| 			{ path: '/featured', name: 'featured', component: () => import('./views/pages/featured.vue').then(m => m.default) }, | 			{ path: '/featured', name: 'featured', component: () => import('./views/pages/featured.vue').then(m => m.default) }, | ||||||
| 			{ path: '/explore', name: 'explore', component: () => import('./views/pages/explore.vue').then(m => m.default) }, | 			{ path: '/explore', name: 'explore', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, | ||||||
| 			{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('./views/pages/explore.vue').then(m => m.default) }, | 			{ path: '/explore/tags/:tag', name: 'explore-tag', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, | ||||||
| 			{ path: '/share', component: MkShare }, | 			{ path: '/share', component: MkShare }, | ||||||
| 			{ path: '/games/reversi/:game?', name: 'reversi', component: MkReversi }, | 			{ path: '/games/reversi/:game?', name: 'reversi', component: MkReversi }, | ||||||
| 			{ path: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [ | 			{ path: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [ | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ | ||||||
| 						<li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home" fixed-width/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li> | 						<li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home" fixed-width/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li> | ||||||
| 						<li><p @click="showNotifications = true"><i><fa :icon="['far', 'bell']" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></p></li> | 						<li><p @click="showNotifications = true"><i><fa :icon="['far', 'bell']" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></p></li> | ||||||
| 						<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']" fixed-width/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> | 						<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']" fixed-width/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> | ||||||
| 						<li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> | 						<li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/follow-requests" :data-active="$route.name == 'follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> | ||||||
| 						<li><router-link to="/featured" :data-active="$route.name == 'featured'"><i><fa :icon="faNewspaper" fixed-width/></i>{{ $t('@.featured-notes') }}<i><fa icon="angle-right"/></i></router-link></li> | 						<li><router-link to="/featured" :data-active="$route.name == 'featured'"><i><fa :icon="faNewspaper" fixed-width/></i>{{ $t('@.featured-notes') }}<i><fa icon="angle-right"/></i></router-link></li> | ||||||
| 						<li><router-link to="/explore" :data-active="$route.name == 'explore' || $route.name == 'explore-tag'"><i><fa :icon="faHashtag" fixed-width/></i>{{ $t('@.explore') }}<i><fa icon="angle-right"/></i></router-link></li> | 						<li><router-link to="/explore" :data-active="$route.name == 'explore' || $route.name == 'explore-tag'"><i><fa :icon="faHashtag" fixed-width/></i>{{ $t('@.explore') }}<i><fa icon="angle-right"/></i></router-link></li> | ||||||
| 						<li><router-link to="/games/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad" fixed-width/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> | 						<li><router-link to="/games/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad" fixed-width/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> | ||||||
|  |  | ||||||
|  | @ -1,28 +0,0 @@ | ||||||
| <template> |  | ||||||
| <mk-ui> |  | ||||||
| 	<template #header><span style="margin-right:4px;"><fa :icon="faHashtag"/></span>{{ $t('@.explore') }}</template> |  | ||||||
| 
 |  | ||||||
| 	<main> |  | ||||||
| 		<x-explore v-bind="$attrs"/> |  | ||||||
| 	</main> |  | ||||||
| </mk-ui> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import i18n from '../../../i18n'; |  | ||||||
| import { faHashtag } from '@fortawesome/free-solid-svg-icons'; |  | ||||||
| import XExplore from '../../../common/views/pages/explore.vue'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	i18n: i18n(''), |  | ||||||
| 	components: { |  | ||||||
| 		XExplore |  | ||||||
| 	}, |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			faHashtag |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  | @ -2,9 +2,10 @@ | ||||||
| <mk-ui> | <mk-ui> | ||||||
| 	<template #header> | 	<template #header> | ||||||
| 		<template v-if="user"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span><mk-user-name :user="user"/></template> | 		<template v-if="user"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span><mk-user-name :user="user"/></template> | ||||||
|  | 		<template v-else-if="group"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ group.name }}</template> | ||||||
| 		<template v-else><mk-ellipsis/></template> | 		<template v-else><mk-ellipsis/></template> | ||||||
| 	</template> | 	</template> | ||||||
| 	<x-messaging-room v-if="!fetching" :user="user" :is-naked="true"/> | 	<x-messaging-room v-if="!fetching" :user="user" :group="group" :is-naked="true"/> | ||||||
| </mk-ui> | </mk-ui> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -22,6 +23,7 @@ export default Vue.extend({ | ||||||
| 		return { | 		return { | ||||||
| 			fetching: true, | 			fetching: true, | ||||||
| 			user: null, | 			user: null, | ||||||
|  | 			group: null, | ||||||
| 			unwatchDarkmode: null | 			unwatchDarkmode: null | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
|  | @ -48,12 +50,21 @@ export default Vue.extend({ | ||||||
| 	methods: { | 	methods: { | ||||||
| 		fetch() { | 		fetch() { | ||||||
| 			this.fetching = true; | 			this.fetching = true; | ||||||
| 			this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { | 			if (this.$route.params.user) { | ||||||
| 				this.user = user; | 				this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { | ||||||
| 				this.fetching = false; | 					this.user = user; | ||||||
|  | 					this.fetching = false; | ||||||
| 
 | 
 | ||||||
| 				document.title = `${this.$t('@.messaging')}: ${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`; | 					document.title = `${this.$t('@.messaging')}: ${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`; | ||||||
| 			}); | 				}); | ||||||
|  | 			} else { | ||||||
|  | 				this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => { | ||||||
|  | 					this.group = group; | ||||||
|  | 					this.fetching = false; | ||||||
|  | 
 | ||||||
|  | 					document.title = this.$t('@.messaging') + ': ' + this.group.name; | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <mk-ui> | <mk-ui> | ||||||
| 	<template #header><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ $t('@.messaging') }}</template> | 	<template #header><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ $t('@.messaging') }}</template> | ||||||
| 	<x-messaging @navigate="navigate" :header-top="48"/> | 	<x-messaging @navigate="navigate" @navigateGroup="navigateGroup" :header-top="48"/> | ||||||
| </mk-ui> | </mk-ui> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -21,6 +21,9 @@ export default Vue.extend({ | ||||||
| 	methods: { | 	methods: { | ||||||
| 		navigate(user) { | 		navigate(user) { | ||||||
| 			(this as any).$router.push(`/i/messaging/${getAcct(user)}`); | 			(this as any).$router.push(`/i/messaging/${getAcct(user)}`); | ||||||
|  | 		}, | ||||||
|  | 		navigateGroup(group) { | ||||||
|  | 			(this as any).$router.push(`/i/messaging/group/${group.id}`); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,29 +0,0 @@ | ||||||
| <template> |  | ||||||
| <mk-ui> |  | ||||||
| 	<template #header><span style="margin-right:4px;"><fa :icon="faStickyNote"/></span>{{ $t('@.pages') }}</template> |  | ||||||
| 
 |  | ||||||
| 	<main> |  | ||||||
| 		<x-pages v-bind="$attrs"/> |  | ||||||
| 	</main> |  | ||||||
| </mk-ui> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import i18n from '../../../i18n'; |  | ||||||
| import { faHashtag } from '@fortawesome/free-solid-svg-icons'; |  | ||||||
| import XPages from '../../../common/views/pages/pages.vue'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	i18n: i18n(''), |  | ||||||
| 	components: { |  | ||||||
| 		XPages |  | ||||||
| 	}, |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			faHashtag |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
							
								
								
									
										38
									
								
								src/client/app/mobile/views/pages/ui.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/client/app/mobile/views/pages/ui.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | ||||||
|  | <template> | ||||||
|  | <mk-ui> | ||||||
|  | 	<template #header><span style="margin-right:4px;" v-if="icon"><fa :icon="icon"/></span>{{ title }}</template> | ||||||
|  | 
 | ||||||
|  | 	<main> | ||||||
|  | 		<component :is="component" @init="init" v-bind="$attrs"/> | ||||||
|  | 	</main> | ||||||
|  | </mk-ui> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	props: { | ||||||
|  | 		component: { | ||||||
|  | 			required: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			title: null, | ||||||
|  | 			icon: null, | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	mounted() { | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		init(v) { | ||||||
|  | 			this.title = v.title; | ||||||
|  | 			this.icon = v.icon; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | @ -1,48 +0,0 @@ | ||||||
| <template> |  | ||||||
| <mk-ui> |  | ||||||
| 	<template #header v-if="!fetching"><fa icon="list"/>{{ list.name }}</template> |  | ||||||
| 
 |  | ||||||
| 	<main v-if="!fetching"> |  | ||||||
| 		<x-editor :list="list"/> |  | ||||||
| 	</main> |  | ||||||
| </mk-ui> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import Progress from '../../../common/scripts/loading'; |  | ||||||
| import XEditor from '../../../common/views/components/user-list-editor.vue'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	components: { |  | ||||||
| 		XEditor |  | ||||||
| 	}, |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			fetching: true, |  | ||||||
| 			list: null |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	watch: { |  | ||||||
| 		$route: 'fetch' |  | ||||||
| 	}, |  | ||||||
| 	created() { |  | ||||||
| 		this.fetch(); |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		fetch() { |  | ||||||
| 			Progress.start(); |  | ||||||
| 			this.fetching = true; |  | ||||||
| 
 |  | ||||||
| 			this.$root.api('users/lists/show', { |  | ||||||
| 				listId: this.$route.params.list |  | ||||||
| 			}).then(list => { |  | ||||||
| 				this.list = list; |  | ||||||
| 				this.fetching = false; |  | ||||||
| 
 |  | ||||||
| 				Progress.done(); |  | ||||||
| 			}); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  | @ -1,35 +0,0 @@ | ||||||
| <template> |  | ||||||
| <mk-ui> |  | ||||||
| 	<template #header><fa icon="list"/>{{ $t('title') }}</template> |  | ||||||
| 	<template #func><button @click="$refs.lists.add()"><fa icon="plus"/></button></template> |  | ||||||
| 
 |  | ||||||
| 	<x-lists ref="lists" @choosen="choosen"/> |  | ||||||
| </mk-ui> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import i18n from '../../../i18n'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	i18n: i18n('mobile/views/pages/user-lists.vue'), |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			fetching: true, |  | ||||||
| 			lists: [] |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	components: { |  | ||||||
| 		XLists: () => import('../../../common/views/components/user-lists.vue').then(m => m.default) |  | ||||||
| 	}, |  | ||||||
| 	mounted() { |  | ||||||
| 		document.title = this.$t('title'); |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		choosen(list) { |  | ||||||
| 			if (!list) return; |  | ||||||
| 			this.$router.push(`/i/lists/${list.id}`); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  | @ -24,6 +24,8 @@ import { SwSubscription } from '../models/entities/sw-subscription'; | ||||||
| import { Blocking } from '../models/entities/blocking'; | import { Blocking } from '../models/entities/blocking'; | ||||||
| import { UserList } from '../models/entities/user-list'; | 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 { UserGroupJoining } from '../models/entities/user-group-joining'; | ||||||
| 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'; | ||||||
|  | @ -106,6 +108,8 @@ export function initDb(justBorrow = false, sync = false, log = false) { | ||||||
| 			UserPublickey, | 			UserPublickey, | ||||||
| 			UserList, | 			UserList, | ||||||
| 			UserListJoining, | 			UserListJoining, | ||||||
|  | 			UserGroup, | ||||||
|  | 			UserGroupJoining, | ||||||
| 			UserNotePining, | 			UserNotePining, | ||||||
| 			Following, | 			Following, | ||||||
| 			FollowRequest, | 			FollowRequest, | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ | ||||||
| import { User } from './user'; | import { User } from './user'; | ||||||
| import { DriveFile } from './drive-file'; | import { DriveFile } from './drive-file'; | ||||||
| import { id } from '../id'; | import { id } from '../id'; | ||||||
|  | import { UserGroup } from './user-group'; | ||||||
| 
 | 
 | ||||||
| @Entity() | @Entity() | ||||||
| export class MessagingMessage { | export class MessagingMessage { | ||||||
|  | @ -29,10 +30,10 @@ export class MessagingMessage { | ||||||
| 
 | 
 | ||||||
| 	@Index() | 	@Index() | ||||||
| 	@Column({ | 	@Column({ | ||||||
| 		...id(), | 		...id(), nullable: true, | ||||||
| 		comment: 'The recipient user ID.' | 		comment: 'The recipient user ID.' | ||||||
| 	}) | 	}) | ||||||
| 	public recipientId: User['id']; | 	public recipientId: User['id'] | null; | ||||||
| 
 | 
 | ||||||
| 	@ManyToOne(type => User, { | 	@ManyToOne(type => User, { | ||||||
| 		onDelete: 'CASCADE' | 		onDelete: 'CASCADE' | ||||||
|  | @ -40,6 +41,19 @@ export class MessagingMessage { | ||||||
| 	@JoinColumn() | 	@JoinColumn() | ||||||
| 	public recipient: User | null; | 	public recipient: User | null; | ||||||
| 
 | 
 | ||||||
|  | 	@Index() | ||||||
|  | 	@Column({ | ||||||
|  | 		...id(), nullable: true, | ||||||
|  | 		comment: 'The recipient group ID.' | ||||||
|  | 	}) | ||||||
|  | 	public groupId: UserGroup['id'] | null; | ||||||
|  | 
 | ||||||
|  | 	@ManyToOne(type => UserGroup, { | ||||||
|  | 		onDelete: 'CASCADE' | ||||||
|  | 	}) | ||||||
|  | 	@JoinColumn() | ||||||
|  | 	public group: UserGroup | null; | ||||||
|  | 
 | ||||||
| 	@Column('varchar', { | 	@Column('varchar', { | ||||||
| 		length: 4096, nullable: true | 		length: 4096, nullable: true | ||||||
| 	}) | 	}) | ||||||
|  | @ -50,6 +64,12 @@ export class MessagingMessage { | ||||||
| 	}) | 	}) | ||||||
| 	public isRead: boolean; | 	public isRead: boolean; | ||||||
| 
 | 
 | ||||||
|  | 	@Column({ | ||||||
|  | 		...id(), | ||||||
|  | 		array: true, default: '{}' | ||||||
|  | 	}) | ||||||
|  | 	public reads: User['id'][]; | ||||||
|  | 
 | ||||||
| 	@Column({ | 	@Column({ | ||||||
| 		...id(), | 		...id(), | ||||||
| 		nullable: true, | 		nullable: true, | ||||||
|  |  | ||||||
							
								
								
									
										41
									
								
								src/models/entities/user-group-joining.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/models/entities/user-group-joining.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | ||||||
|  | import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; | ||||||
|  | import { User } from './user'; | ||||||
|  | import { UserGroup } from './user-group'; | ||||||
|  | import { id } from '../id'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class UserGroupJoining { | ||||||
|  | 	@PrimaryColumn(id()) | ||||||
|  | 	public id: string; | ||||||
|  | 
 | ||||||
|  | 	@Column('timestamp with time zone', { | ||||||
|  | 		comment: 'The created date of the UserGroupJoining.' | ||||||
|  | 	}) | ||||||
|  | 	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; | ||||||
|  | } | ||||||
							
								
								
									
										46
									
								
								src/models/entities/user-group.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/models/entities/user-group.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | ||||||
|  | import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; | ||||||
|  | import { User } from './user'; | ||||||
|  | import { id } from '../id'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class UserGroup { | ||||||
|  | 	@PrimaryColumn(id()) | ||||||
|  | 	public id: string; | ||||||
|  | 
 | ||||||
|  | 	@Index() | ||||||
|  | 	@Column('timestamp with time zone', { | ||||||
|  | 		comment: 'The created date of the UserGroup.' | ||||||
|  | 	}) | ||||||
|  | 	public createdAt: Date; | ||||||
|  | 
 | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 256, | ||||||
|  | 	}) | ||||||
|  | 	public name: string; | ||||||
|  | 
 | ||||||
|  | 	@Index() | ||||||
|  | 	@Column({ | ||||||
|  | 		...id(), | ||||||
|  | 		comment: 'The ID of owner.' | ||||||
|  | 	}) | ||||||
|  | 	public userId: User['id']; | ||||||
|  | 
 | ||||||
|  | 	@ManyToOne(type => User, { | ||||||
|  | 		onDelete: 'CASCADE' | ||||||
|  | 	}) | ||||||
|  | 	@JoinColumn() | ||||||
|  | 	public user: User | null; | ||||||
|  | 
 | ||||||
|  | 	@Column('boolean', { | ||||||
|  | 		default: false, | ||||||
|  | 	}) | ||||||
|  | 	public isPrivate: boolean; | ||||||
|  | 
 | ||||||
|  | 	constructor(data: Partial<UserGroup>) { | ||||||
|  | 		if (data == null) return; | ||||||
|  | 
 | ||||||
|  | 		for (const [k, v] of Object.entries(data)) { | ||||||
|  | 			(this as any)[k] = v; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -6,7 +6,6 @@ import { PollVote } from './entities/poll-vote'; | ||||||
| import { Meta } from './entities/meta'; | import { Meta } from './entities/meta'; | ||||||
| import { SwSubscription } from './entities/sw-subscription'; | import { SwSubscription } from './entities/sw-subscription'; | ||||||
| import { NoteWatching } from './entities/note-watching'; | import { NoteWatching } from './entities/note-watching'; | ||||||
| import { UserListJoining } from './entities/user-list-joining'; |  | ||||||
| import { NoteUnread } from './entities/note-unread'; | import { NoteUnread } from './entities/note-unread'; | ||||||
| import { RegistrationTicket } from './entities/registration-tickets'; | import { RegistrationTicket } from './entities/registration-tickets'; | ||||||
| import { UserRepository } from './repositories/user'; | import { UserRepository } from './repositories/user'; | ||||||
|  | @ -20,6 +19,9 @@ import { SigninRepository } from './repositories/signin'; | ||||||
| import { MessagingMessageRepository } from './repositories/messaging-message'; | import { MessagingMessageRepository } from './repositories/messaging-message'; | ||||||
| import { ReversiGameRepository } from './repositories/games/reversi/game'; | import { ReversiGameRepository } from './repositories/games/reversi/game'; | ||||||
| import { UserListRepository } from './repositories/user-list'; | 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 { 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'; | ||||||
|  | @ -52,6 +54,8 @@ export const UserKeypairs = getRepository(UserKeypair); | ||||||
| export const UserPublickeys = getRepository(UserPublickey); | export const UserPublickeys = getRepository(UserPublickey); | ||||||
| export const UserLists = getCustomRepository(UserListRepository); | export const UserLists = getCustomRepository(UserListRepository); | ||||||
| export const UserListJoinings = getRepository(UserListJoining); | export const UserListJoinings = getRepository(UserListJoining); | ||||||
|  | export const UserGroups = getCustomRepository(UserGroupRepository); | ||||||
|  | export const UserGroupJoinings = getRepository(UserGroupJoining); | ||||||
| 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); | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { EntityRepository, Repository } from 'typeorm'; | import { EntityRepository, Repository } from 'typeorm'; | ||||||
| import { MessagingMessage } from '../entities/messaging-message'; | import { MessagingMessage } from '../entities/messaging-message'; | ||||||
| import { Users, DriveFiles } from '..'; | import { Users, DriveFiles, UserGroups } from '..'; | ||||||
| import { ensure } from '../../prelude/ensure'; | import { ensure } from '../../prelude/ensure'; | ||||||
| import { types, bool, SchemaType } from '../../misc/schema'; | import { types, bool, SchemaType } from '../../misc/schema'; | ||||||
| 
 | 
 | ||||||
|  | @ -16,11 +16,13 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> { | ||||||
| 		src: MessagingMessage['id'] | MessagingMessage, | 		src: MessagingMessage['id'] | MessagingMessage, | ||||||
| 		me?: any, | 		me?: any, | ||||||
| 		options?: { | 		options?: { | ||||||
| 			populateRecipient: boolean | 			populateRecipient?: boolean, | ||||||
|  | 			populateGroup?: boolean, | ||||||
| 		} | 		} | ||||||
| 	): Promise<PackedMessagingMessage> { | 	): Promise<PackedMessagingMessage> { | ||||||
| 		const opts = options || { | 		const opts = options || { | ||||||
| 			populateRecipient: true | 			populateRecipient: true, | ||||||
|  | 			populateGroup: true, | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
| 		const message = typeof src === 'object' ? src : await this.findOne(src).then(ensure); | 		const message = typeof src === 'object' ? src : await this.findOne(src).then(ensure); | ||||||
|  | @ -32,10 +34,13 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> { | ||||||
| 			userId: message.userId, | 			userId: message.userId, | ||||||
| 			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: 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, | ||||||
|  | 			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, | ||||||
| 			isRead: message.isRead | 			isRead: message.isRead, | ||||||
|  | 			reads: message.reads, | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -83,17 +88,36 @@ export const packedMessagingMessageSchema = { | ||||||
| 		}, | 		}, | ||||||
| 		recipientId: { | 		recipientId: { | ||||||
| 			type: types.string, | 			type: types.string, | ||||||
| 			optional: bool.false, nullable: bool.false, | 			optional: bool.false, nullable: bool.true, | ||||||
| 			format: 'id', | 			format: 'id', | ||||||
| 		}, | 		}, | ||||||
| 		recipient: { | 		recipient: { | ||||||
| 			type: types.object, | 			type: types.object, | ||||||
| 			optional: bool.true, nullable: bool.false, | 			optional: bool.true, nullable: bool.true, | ||||||
| 			ref: 'User' | 			ref: 'User' | ||||||
| 		}, | 		}, | ||||||
|  | 		groupId: { | ||||||
|  | 			type: types.string, | ||||||
|  | 			optional: bool.false, nullable: bool.true, | ||||||
|  | 			format: 'id', | ||||||
|  | 		}, | ||||||
|  | 		group: { | ||||||
|  | 			type: types.object, | ||||||
|  | 			optional: bool.true, nullable: bool.true, | ||||||
|  | 			ref: 'UserGroup' | ||||||
|  | 		}, | ||||||
| 		isRead: { | 		isRead: { | ||||||
| 			type: types.boolean, | 			type: types.boolean, | ||||||
| 			optional: bool.true, nullable: bool.false, | 			optional: bool.true, nullable: bool.false, | ||||||
| 		}, | 		}, | ||||||
|  | 		reads: { | ||||||
|  | 			type: types.array, | ||||||
|  | 			optional: bool.true, nullable: bool.false, | ||||||
|  | 			items: { | ||||||
|  | 				type: types.string, | ||||||
|  | 				optional: bool.false, nullable: bool.false, | ||||||
|  | 				format: 'id' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
							
								
								
									
										61
									
								
								src/models/repositories/user-group.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/models/repositories/user-group.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | ||||||
|  | import { EntityRepository, Repository } from 'typeorm'; | ||||||
|  | import { UserGroup } from '../entities/user-group'; | ||||||
|  | import { ensure } from '../../prelude/ensure'; | ||||||
|  | import { UserGroupJoinings } from '..'; | ||||||
|  | import { bool, types, SchemaType } from '../../misc/schema'; | ||||||
|  | 
 | ||||||
|  | export type PackedUserGroup = SchemaType<typeof packedUserGroupSchema>; | ||||||
|  | 
 | ||||||
|  | @EntityRepository(UserGroup) | ||||||
|  | export class UserGroupRepository extends Repository<UserGroup> { | ||||||
|  | 	public async pack( | ||||||
|  | 		src: UserGroup['id'] | UserGroup, | ||||||
|  | 	): Promise<PackedUserGroup> { | ||||||
|  | 		const userGroup = typeof src === 'object' ? src : await this.findOne(src).then(ensure); | ||||||
|  | 
 | ||||||
|  | 		const users = await UserGroupJoinings.find({ | ||||||
|  | 			userGroupId: userGroup.id | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			id: userGroup.id, | ||||||
|  | 			createdAt: userGroup.createdAt.toISOString(), | ||||||
|  | 			name: userGroup.name, | ||||||
|  | 			userIds: users.map(x => x.userId) | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const packedUserGroupSchema = { | ||||||
|  | 	type: types.object, | ||||||
|  | 	optional: bool.false, nullable: bool.false, | ||||||
|  | 	properties: { | ||||||
|  | 		id: { | ||||||
|  | 			type: types.string, | ||||||
|  | 			optional: bool.false, nullable: bool.false, | ||||||
|  | 			format: 'id', | ||||||
|  | 			description: 'The unique identifier for this UserGroup.', | ||||||
|  | 			example: 'xxxxxxxxxx', | ||||||
|  | 		}, | ||||||
|  | 		createdAt: { | ||||||
|  | 			type: types.string, | ||||||
|  | 			optional: bool.false, nullable: bool.false, | ||||||
|  | 			format: 'date-time', | ||||||
|  | 			description: 'The date that the UserGroup was created.' | ||||||
|  | 		}, | ||||||
|  | 		name: { | ||||||
|  | 			type: types.string, | ||||||
|  | 			optional: bool.false, nullable: bool.false, | ||||||
|  | 			description: 'The name of the UserGroup.' | ||||||
|  | 		}, | ||||||
|  | 		userIds: { | ||||||
|  | 			type: types.array, | ||||||
|  | 			nullable: bool.false, optional: bool.true, | ||||||
|  | 			items: { | ||||||
|  | 				type: types.string, | ||||||
|  | 				nullable: bool.false, optional: bool.false, | ||||||
|  | 				format: 'id', | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | }; | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { EntityRepository, Repository, In } from 'typeorm'; | import { EntityRepository, Repository, In } from 'typeorm'; | ||||||
| import { User, ILocalUser, IRemoteUser } from '../entities/user'; | import { User, ILocalUser, IRemoteUser } from '../entities/user'; | ||||||
| import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles } from '..'; | import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserGroupJoinings } from '..'; | ||||||
| import { ensure } from '../../prelude/ensure'; | import { ensure } from '../../prelude/ensure'; | ||||||
| import config from '../../config'; | import config from '../../config'; | ||||||
| import { SchemaType, bool, types } from '../../misc/schema'; | import { SchemaType, bool, types } from '../../misc/schema'; | ||||||
|  | @ -54,6 +54,31 @@ export class UserRepository extends Repository<User> { | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	public async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> { | ||||||
|  | 		const joinings = await UserGroupJoinings.find({ userId: userId }); | ||||||
|  | 
 | ||||||
|  | 		const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message') | ||||||
|  | 			.where(`message.groupId = :groupId`, { groupId: j.userGroupId }) | ||||||
|  | 			.andWhere('message.userId != :userId', { userId: userId }) | ||||||
|  | 			.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) | ||||||
|  | 			.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
 | ||||||
|  | 			.getOne().then(x => x != null))); | ||||||
|  | 
 | ||||||
|  | 		const [withUser, withGroups] = await Promise.all([ | ||||||
|  | 			// TODO: ミュートを考慮
 | ||||||
|  | 			MessagingMessages.count({ | ||||||
|  | 				where: { | ||||||
|  | 					recipientId: userId, | ||||||
|  | 					isRead: false | ||||||
|  | 				}, | ||||||
|  | 				take: 1 | ||||||
|  | 			}).then(count => count > 0), | ||||||
|  | 			groupQs | ||||||
|  | 		]); | ||||||
|  | 
 | ||||||
|  | 		return withUser || withGroups.some(x => x); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	public async pack( | 	public async pack( | ||||||
| 		src: User['id'] | User, | 		src: User['id'] | User, | ||||||
| 		me?: User['id'] | User | null | undefined, | 		me?: User['id'] | User | null | undefined, | ||||||
|  | @ -151,13 +176,7 @@ export class UserRepository extends Repository<User> { | ||||||
| 				autoWatch: profile!.autoWatch, | 				autoWatch: profile!.autoWatch, | ||||||
| 				alwaysMarkNsfw: profile!.alwaysMarkNsfw, | 				alwaysMarkNsfw: profile!.alwaysMarkNsfw, | ||||||
| 				carefulBot: profile!.carefulBot, | 				carefulBot: profile!.carefulBot, | ||||||
| 				hasUnreadMessagingMessage: MessagingMessages.count({ | 				hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id), | ||||||
| 					where: { |  | ||||||
| 						recipientId: user.id, |  | ||||||
| 						isRead: false |  | ||||||
| 					}, |  | ||||||
| 					take: 1 |  | ||||||
| 				}).then(count => count > 0), |  | ||||||
| 				hasUnreadNotification: Notifications.count({ | 				hasUnreadNotification: Notifications.count({ | ||||||
| 					where: { | 					where: { | ||||||
| 						notifieeId: user.id, | 						notifieeId: user.id, | ||||||
|  |  | ||||||
|  | @ -1,21 +1,33 @@ | ||||||
| import { publishMainStream } from '../../../services/stream'; | import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream'; | ||||||
| import { publishMessagingStream } from '../../../services/stream'; | import { publishMessagingStream } from '../../../services/stream'; | ||||||
| import { publishMessagingIndexStream } from '../../../services/stream'; | import { publishMessagingIndexStream } from '../../../services/stream'; | ||||||
| import { User } from '../../../models/entities/user'; | import { User } from '../../../models/entities/user'; | ||||||
| import { MessagingMessage } from '../../../models/entities/messaging-message'; | import { MessagingMessage } from '../../../models/entities/messaging-message'; | ||||||
| import { MessagingMessages } from '../../../models'; | import { MessagingMessages, UserGroupJoinings, Users } from '../../../models'; | ||||||
| import { In } from 'typeorm'; | import { In } from 'typeorm'; | ||||||
|  | import { IdentifiableError } from '../../../misc/identifiable-error'; | ||||||
|  | import { UserGroup } from '../../../models/entities/user-group'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Mark messages as read |  * Mark messages as read | ||||||
|  */ |  */ | ||||||
| export default async ( | export async function readUserMessagingMessage( | ||||||
| 	userId: User['id'], | 	userId: User['id'], | ||||||
| 	otherpartyId: User['id'], | 	otherpartyId: User['id'], | ||||||
| 	messageIds: MessagingMessage['id'][] | 	messageIds: MessagingMessage['id'][] | ||||||
| ) => { | ) { | ||||||
| 	if (messageIds.length === 0) return; | 	if (messageIds.length === 0) return; | ||||||
| 
 | 
 | ||||||
|  | 	const messages = await MessagingMessages.find({ | ||||||
|  | 		id: In(messageIds) | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	for (const message of messages) { | ||||||
|  | 		if (message.recipientId !== userId) { | ||||||
|  | 			throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).'); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// Update documents
 | 	// Update documents
 | ||||||
| 	await MessagingMessages.update({ | 	await MessagingMessages.update({ | ||||||
| 		id: In(messageIds), | 		id: In(messageIds), | ||||||
|  | @ -30,14 +42,62 @@ export default async ( | ||||||
| 	publishMessagingStream(otherpartyId, userId, 'read', messageIds); | 	publishMessagingStream(otherpartyId, userId, 'read', messageIds); | ||||||
| 	publishMessagingIndexStream(userId, 'read', messageIds); | 	publishMessagingIndexStream(userId, 'read', messageIds); | ||||||
| 
 | 
 | ||||||
| 	// Calc count of my unread messages
 | 	if (!Users.getHasUnreadMessagingMessage(userId)) { | ||||||
| 	const count = await MessagingMessages.count({ |  | ||||||
| 		recipientId: userId, |  | ||||||
| 		isRead: false |  | ||||||
| 	}); |  | ||||||
| 
 |  | ||||||
| 	if (count == 0) { |  | ||||||
| 		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 | 		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 | ||||||
| 		publishMainStream(userId, 'readAllMessagingMessages'); | 		publishMainStream(userId, 'readAllMessagingMessages'); | ||||||
| 	} | 	} | ||||||
| }; | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Mark messages as read | ||||||
|  |  */ | ||||||
|  | export async function readGroupMessagingMessage( | ||||||
|  | 	userId: User['id'], | ||||||
|  | 	groupId: UserGroup['id'], | ||||||
|  | 	messageIds: MessagingMessage['id'][] | ||||||
|  | ) { | ||||||
|  | 	if (messageIds.length === 0) return; | ||||||
|  | 
 | ||||||
|  | 	// check joined
 | ||||||
|  | 	const joining = await UserGroupJoinings.findOne({ | ||||||
|  | 		userId: userId, | ||||||
|  | 		userGroupId: groupId | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (joining == null) { | ||||||
|  | 		throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).'); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const messages = await MessagingMessages.find({ | ||||||
|  | 		id: In(messageIds) | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	const reads = []; | ||||||
|  | 
 | ||||||
|  | 	for (const message of messages) { | ||||||
|  | 		if (message.userId === userId) continue; | ||||||
|  | 		if (message.reads.includes(userId)) continue; | ||||||
|  | 
 | ||||||
|  | 		// Update document
 | ||||||
|  | 		await MessagingMessages.createQueryBuilder().update() | ||||||
|  | 			.set({ | ||||||
|  | 				reads: (() => `array_append("reads", '${joining.userId}')`) as any | ||||||
|  | 			}) | ||||||
|  | 			.where('id = :id', { id: message.id }) | ||||||
|  | 			.execute(); | ||||||
|  | 
 | ||||||
|  | 		reads.push(message.id); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Publish event
 | ||||||
|  | 	publishGroupMessagingStream(groupId, 'read', { | ||||||
|  | 		ids: reads, | ||||||
|  | 		userId: userId | ||||||
|  | 	}); | ||||||
|  | 	publishMessagingIndexStream(userId, 'read', reads); | ||||||
|  | 
 | ||||||
|  | 	if (!Users.getHasUnreadMessagingMessage(userId)) { | ||||||
|  | 		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 | ||||||
|  | 		publishMainStream(userId, 'readAllMessagingMessages'); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import define from '../../define'; | import define from '../../define'; | ||||||
| import { MessagingMessage } from '../../../../models/entities/messaging-message'; | import { MessagingMessage } from '../../../../models/entities/messaging-message'; | ||||||
| import { MessagingMessages, Mutings } from '../../../../models'; | import { MessagingMessages, Mutings, UserGroupJoinings } from '../../../../models'; | ||||||
| import { Brackets } from 'typeorm'; | import { Brackets } from 'typeorm'; | ||||||
| import { types, bool } from '../../../../misc/schema'; | import { types, bool } from '../../../../misc/schema'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	desc: { | 	desc: { | ||||||
| 		'ja-JP': 'Messagingの履歴を取得します。', | 		'ja-JP': 'トークの履歴を取得します。', | ||||||
| 		'en-US': 'Show messaging history.' | 		'en-US': 'Show messaging history.' | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -21,6 +21,11 @@ export const meta = { | ||||||
| 		limit: { | 		limit: { | ||||||
| 			validator: $.optional.num.range(1, 100), | 			validator: $.optional.num.range(1, 100), | ||||||
| 			default: 10 | 			default: 10 | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		group: { | ||||||
|  | 			validator: $.optional.bool, | ||||||
|  | 			default: false | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -40,26 +45,46 @@ export default define(meta, async (ps, user) => { | ||||||
| 		muterId: user.id, | 		muterId: user.id, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
|  | 	const groups = ps.group ? await UserGroupJoinings.find({ | ||||||
|  | 		userId: user.id, | ||||||
|  | 	}).then(xs => xs.map(x => x.userGroupId)) : []; | ||||||
|  | 
 | ||||||
|  | 	if (ps.group && groups.length === 0) { | ||||||
|  | 		return []; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	const history: MessagingMessage[] = []; | 	const history: MessagingMessage[] = []; | ||||||
| 
 | 
 | ||||||
| 	for (let i = 0; i < ps.limit!; i++) { | 	for (let i = 0; i < ps.limit!; i++) { | ||||||
| 		const found = history.map(m => (m.userId === user.id) ? m.recipientId : m.userId); | 		const found = ps.group | ||||||
|  | 			? history.map(m => m.groupId!) | ||||||
|  | 			: history.map(m => (m.userId === user.id) ? m.recipientId! : m.userId!); | ||||||
| 
 | 
 | ||||||
| 		const query = MessagingMessages.createQueryBuilder('message') | 		const query = MessagingMessages.createQueryBuilder('message') | ||||||
| 			.where(new Brackets(qb => { qb |  | ||||||
| 				.where(`message.userId = :userId`, { userId: user.id }) |  | ||||||
| 				.orWhere(`message.recipientId = :userId`, { userId: user.id }); |  | ||||||
| 			})) |  | ||||||
| 			.orderBy('message.createdAt', 'DESC'); | 			.orderBy('message.createdAt', 'DESC'); | ||||||
| 
 | 
 | ||||||
| 		if (found.length > 0) { | 		if (ps.group) { | ||||||
| 			query.andWhere(`message.userId NOT IN (:...found)`, { found: found }); | 			query.where(`message.groupId IN (:...groups)`, { groups: groups }); | ||||||
| 			query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found }); |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		if (mute.length > 0) { | 			if (found.length > 0) { | ||||||
| 			query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); | 				query.andWhere(`message.groupId NOT IN (:...found)`, { found: found }); | ||||||
| 			query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); | 			} | ||||||
|  | 		} else { | ||||||
|  | 			query.where(new Brackets(qb => { qb | ||||||
|  | 				.where(`message.userId = :userId`, { userId: user.id }) | ||||||
|  | 				.orWhere(`message.recipientId = :userId`, { userId: user.id }); | ||||||
|  | 			})); | ||||||
|  | 			query.andWhere(`message.groupId IS NULL`); | ||||||
|  | 
 | ||||||
|  | 			if (found.length > 0) { | ||||||
|  | 				query.andWhere(`message.userId NOT IN (:...found)`, { found: found }); | ||||||
|  | 				query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found }); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if (mute.length > 0) { | ||||||
|  | 				query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); | ||||||
|  | 				query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		const message = await query.getOne(); | 		const message = await query.getOne(); | ||||||
|  |  | ||||||
|  | @ -1,16 +1,17 @@ | ||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import { ID } from '../../../../misc/cafy-id'; | import { ID } from '../../../../misc/cafy-id'; | ||||||
| import read from '../../common/read-messaging-message'; |  | ||||||
| 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 { MessagingMessages } from '../../../../models'; | import { MessagingMessages, UserGroups, UserGroupJoinings } from '../../../../models'; | ||||||
| import { makePaginationQuery } from '../../common/make-pagination-query'; | import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||||
| import { types, bool } from '../../../../misc/schema'; | import { types, bool } from '../../../../misc/schema'; | ||||||
|  | import { Brackets } from 'typeorm'; | ||||||
|  | import { readUserMessagingMessage, readGroupMessagingMessage } from '../../common/read-messaging-message'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	desc: { | 	desc: { | ||||||
| 		'ja-JP': '指定したユーザーとのMessagingのメッセージ一覧を取得します。', | 		'ja-JP': 'トークメッセージ一覧を取得します。', | ||||||
| 		'en-US': 'Get messages of messaging.' | 		'en-US': 'Get messages of messaging.' | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -22,13 +23,21 @@ export const meta = { | ||||||
| 
 | 
 | ||||||
| 	params: { | 	params: { | ||||||
| 		userId: { | 		userId: { | ||||||
| 			validator: $.type(ID), | 			validator: $.optional.type(ID), | ||||||
| 			desc: { | 			desc: { | ||||||
| 				'ja-JP': '対象のユーザーのID', | 				'ja-JP': '対象のユーザーのID', | ||||||
| 				'en-US': 'Target user ID' | 				'en-US': 'Target user ID' | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		groupId: { | ||||||
|  | 			validator: $.optional.type(ID), | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': '対象のグループのID', | ||||||
|  | 				'en-US': 'Target group ID' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		limit: { | 		limit: { | ||||||
| 			validator: $.optional.num.range(1, 100), | 			validator: $.optional.num.range(1, 100), | ||||||
| 			default: 10 | 			default: 10 | ||||||
|  | @ -64,27 +73,85 @@ export const meta = { | ||||||
| 			code: 'NO_SUCH_USER', | 			code: 'NO_SUCH_USER', | ||||||
| 			id: '11795c64-40ea-4198-b06e-3c873ed9039d' | 			id: '11795c64-40ea-4198-b06e-3c873ed9039d' | ||||||
| 		}, | 		}, | ||||||
|  | 
 | ||||||
|  | 		noSuchGroup: { | ||||||
|  | 			message: 'No such group.', | ||||||
|  | 			code: 'NO_SUCH_GROUP', | ||||||
|  | 			id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		groupAccessDenied: { | ||||||
|  | 			message: 'You can not read messages of groups that you have not joined.', | ||||||
|  | 			code: 'GROUP_ACCESS_DENIED', | ||||||
|  | 			id: 'a053a8dd-a491-4718-8f87-50775aad9284' | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps, user) => { | export default define(meta, async (ps, user) => { | ||||||
| 	// Fetch recipient
 | 	if (ps.userId != null) { | ||||||
| 	const recipient = await getUser(ps.userId).catch(e => { | 		// Fetch recipient (user)
 | ||||||
| 		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); | 		const recipient = await getUser(ps.userId).catch(e => { | ||||||
| 		throw e; | 			if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); | ||||||
| 	}); | 			throw e; | ||||||
|  | 		}); | ||||||
| 
 | 
 | ||||||
| 	const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) | 		const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) | ||||||
| 		.andWhere(`(message.userId = :meId AND message.recipientId = :recipientId) OR (message.userId = :recipientId AND message.recipientId = :meId)`, { meId: user.id, recipientId: recipient.id }); | 			.andWhere(new Brackets(qb => { qb | ||||||
|  | 				.where(new Brackets(qb => { qb | ||||||
|  | 					.where('message.userId = :meId') | ||||||
|  | 					.andWhere('message.recipientId = :recipientId'); | ||||||
|  | 				})) | ||||||
|  | 				.orWhere(new Brackets(qb => { qb | ||||||
|  | 					.where('message.userId = :recipientId') | ||||||
|  | 					.andWhere('message.recipientId = :meId'); | ||||||
|  | 				})); | ||||||
|  | 			})) | ||||||
|  | 			.setParameter('meId', user.id) | ||||||
|  | 			.setParameter('recipientId', recipient.id); | ||||||
| 
 | 
 | ||||||
| 	const messages = await query.getMany(); | 		const messages = await query.take(ps.limit!).getMany(); | ||||||
| 
 | 
 | ||||||
| 	// Mark all as read
 | 		// Mark all as read
 | ||||||
| 	if (ps.markAsRead) { | 		if (ps.markAsRead) { | ||||||
| 		read(user.id, recipient.id, messages.map(x => x.id)); | 			readUserMessagingMessage(user.id, recipient.id, messages.map(x => x.id)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { | ||||||
|  | 			populateRecipient: false | ||||||
|  | 		}))); | ||||||
|  | 	} else if (ps.groupId != null) { | ||||||
|  | 		// Fetch recipient (group)
 | ||||||
|  | 		const recipientGroup = await UserGroups.findOne(ps.groupId); | ||||||
|  | 
 | ||||||
|  | 		if (recipientGroup == null) { | ||||||
|  | 			throw new ApiError(meta.errors.noSuchGroup); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// check joined
 | ||||||
|  | 		const joining = await UserGroupJoinings.findOne({ | ||||||
|  | 			userId: user.id, | ||||||
|  | 			userGroupId: recipientGroup.id | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		if (joining == null) { | ||||||
|  | 			throw new ApiError(meta.errors.groupAccessDenied); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) | ||||||
|  | 			.andWhere(`message.groupId = :groupId`, { groupId: recipientGroup.id }); | ||||||
|  | 
 | ||||||
|  | 		const messages = await query.take(ps.limit!).getMany(); | ||||||
|  | 
 | ||||||
|  | 		// Mark all as read
 | ||||||
|  | 		if (ps.markAsRead) { | ||||||
|  | 			readGroupMessagingMessage(user.id, recipientGroup.id, messages.map(x => x.id)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { | ||||||
|  | 			populateGroup: false | ||||||
|  | 		}))); | ||||||
|  | 	} else { | ||||||
|  | 		throw new Error(); | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { |  | ||||||
| 		populateRecipient: false |  | ||||||
| 	}))); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,19 +1,22 @@ | ||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import { ID } from '../../../../../misc/cafy-id'; | import { ID } from '../../../../../misc/cafy-id'; | ||||||
| import { publishMainStream } from '../../../../../services/stream'; | import { publishMainStream, publishGroupMessagingStream } from '../../../../../services/stream'; | ||||||
| import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../services/stream'; | import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../services/stream'; | ||||||
| import pushSw from '../../../../../services/push-notification'; | import pushSw from '../../../../../services/push-notification'; | ||||||
| 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 { MessagingMessages, DriveFiles, Mutings } from '../../../../../models'; | import { MessagingMessages, DriveFiles, Mutings, UserGroups, UserGroupJoinings } from '../../../../../models'; | ||||||
| import { MessagingMessage } from '../../../../../models/entities/messaging-message'; | import { MessagingMessage } from '../../../../../models/entities/messaging-message'; | ||||||
| import { genId } from '../../../../../misc/gen-id'; | import { genId } from '../../../../../misc/gen-id'; | ||||||
| import { types, bool } from '../../../../../misc/schema'; | import { types, bool } from '../../../../../misc/schema'; | ||||||
|  | import { User } from '../../../../../models/entities/user'; | ||||||
|  | import { UserGroup } from '../../../../../models/entities/user-group'; | ||||||
|  | import { Not } from 'typeorm'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	desc: { | 	desc: { | ||||||
| 		'ja-JP': '指定したユーザーへMessagingのメッセージを送信します。', | 		'ja-JP': 'トークメッセージを送信します。', | ||||||
| 		'en-US': 'Create a message of messaging.' | 		'en-US': 'Create a message of messaging.' | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -25,13 +28,21 @@ export const meta = { | ||||||
| 
 | 
 | ||||||
| 	params: { | 	params: { | ||||||
| 		userId: { | 		userId: { | ||||||
| 			validator: $.type(ID), | 			validator: $.optional.type(ID), | ||||||
| 			desc: { | 			desc: { | ||||||
| 				'ja-JP': '対象のユーザーのID', | 				'ja-JP': '対象のユーザーのID', | ||||||
| 				'en-US': 'Target user ID' | 				'en-US': 'Target user ID' | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		groupId: { | ||||||
|  | 			validator: $.optional.type(ID), | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': '対象のグループのID', | ||||||
|  | 				'en-US': 'Target group ID' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		text: { | 		text: { | ||||||
| 			validator: $.optional.str.pipe(MessagingMessages.isValidText) | 			validator: $.optional.str.pipe(MessagingMessages.isValidText) | ||||||
| 		}, | 		}, | ||||||
|  | @ -60,6 +71,18 @@ export const meta = { | ||||||
| 			id: '11795c64-40ea-4198-b06e-3c873ed9039d' | 			id: '11795c64-40ea-4198-b06e-3c873ed9039d' | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		noSuchGroup: { | ||||||
|  | 			message: 'No such group.', | ||||||
|  | 			code: 'NO_SUCH_GROUP', | ||||||
|  | 			id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		groupAccessDenied: { | ||||||
|  | 			message: 'You can not send messages to groups that you have not joined.', | ||||||
|  | 			code: 'GROUP_ACCESS_DENIED', | ||||||
|  | 			id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		noSuchFile: { | 		noSuchFile: { | ||||||
| 			message: 'No such file.', | 			message: 'No such file.', | ||||||
| 			code: 'NO_SUCH_FILE', | 			code: 'NO_SUCH_FILE', | ||||||
|  | @ -75,16 +98,38 @@ export const meta = { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps, user) => { | export default define(meta, async (ps, user) => { | ||||||
| 	// Myself
 | 	let recipientUser: User | undefined; | ||||||
| 	if (ps.userId === user.id) { | 	let recipientGroup: UserGroup | undefined; | ||||||
| 		throw new ApiError(meta.errors.recipientIsYourself); |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	// Fetch recipient
 | 	if (ps.userId != null) { | ||||||
| 	const recipient = await getUser(ps.userId).catch(e => { | 		// Myself
 | ||||||
| 		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); | 		if (ps.userId === user.id) { | ||||||
| 		throw e; | 			throw new ApiError(meta.errors.recipientIsYourself); | ||||||
| 	}); | 		} | ||||||
|  | 
 | ||||||
|  | 		// Fetch recipient (user)
 | ||||||
|  | 		recipientUser = await getUser(ps.userId).catch(e => { | ||||||
|  | 			if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); | ||||||
|  | 			throw e; | ||||||
|  | 		}); | ||||||
|  | 	} else if (ps.groupId != null) { | ||||||
|  | 		// Fetch recipient (group)
 | ||||||
|  | 		recipientGroup = await UserGroups.findOne(ps.groupId); | ||||||
|  | 
 | ||||||
|  | 		if (recipientGroup == null) { | ||||||
|  | 			throw new ApiError(meta.errors.noSuchGroup); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// check joined
 | ||||||
|  | 		const joining = await UserGroupJoinings.findOne({ | ||||||
|  | 			userId: user.id, | ||||||
|  | 			userGroupId: recipientGroup.id | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		if (joining == null) { | ||||||
|  | 			throw new ApiError(meta.errors.groupAccessDenied); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	let file = null; | 	let file = null; | ||||||
| 	if (ps.fileId != null) { | 	if (ps.fileId != null) { | ||||||
|  | @ -107,32 +152,49 @@ export default define(meta, async (ps, user) => { | ||||||
| 		id: genId(), | 		id: genId(), | ||||||
| 		createdAt: new Date(), | 		createdAt: new Date(), | ||||||
| 		fileId: file ? file.id : null, | 		fileId: file ? file.id : null, | ||||||
| 		recipientId: recipient.id, | 		recipientId: recipientUser ? recipientUser.id : null, | ||||||
|  | 		groupId: recipientGroup ? recipientGroup.id : null, | ||||||
| 		text: ps.text ? ps.text.trim() : null, | 		text: ps.text ? ps.text.trim() : null, | ||||||
| 		userId: user.id, | 		userId: user.id, | ||||||
| 		isRead: false | 		isRead: false, | ||||||
|  | 		reads: [] as any[] | ||||||
| 	} as MessagingMessage); | 	} as MessagingMessage); | ||||||
| 
 | 
 | ||||||
| 	const messageObj = await MessagingMessages.pack(message); | 	const messageObj = await MessagingMessages.pack(message); | ||||||
| 
 | 
 | ||||||
| 	// 自分のストリーム
 | 	if (recipientUser) { | ||||||
| 	publishMessagingStream(message.userId, message.recipientId, 'message', messageObj); | 		// 自分のストリーム
 | ||||||
| 	publishMessagingIndexStream(message.userId, 'message', messageObj); | 		publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj); | ||||||
| 	publishMainStream(message.userId, 'messagingMessage', messageObj); | 		publishMessagingIndexStream(message.userId, 'message', messageObj); | ||||||
|  | 		publishMainStream(message.userId, 'messagingMessage', messageObj); | ||||||
| 
 | 
 | ||||||
| 	// 相手のストリーム
 | 		// 相手のストリーム
 | ||||||
| 	publishMessagingStream(message.recipientId, message.userId, 'message', messageObj); | 		publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj); | ||||||
| 	publishMessagingIndexStream(message.recipientId, 'message', messageObj); | 		publishMessagingIndexStream(recipientUser.id, 'message', messageObj); | ||||||
| 	publishMainStream(message.recipientId, 'messagingMessage', messageObj); | 		publishMainStream(recipientUser.id, 'messagingMessage', messageObj); | ||||||
|  | 	} else if (recipientGroup) { | ||||||
|  | 		// グループのストリーム
 | ||||||
|  | 		publishGroupMessagingStream(recipientGroup.id, 'message', messageObj); | ||||||
|  | 
 | ||||||
|  | 		// メンバーのストリーム
 | ||||||
|  | 		const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id }); | ||||||
|  | 		for (const joining of joinings) { | ||||||
|  | 			publishMessagingIndexStream(joining.userId, 'message', messageObj); | ||||||
|  | 			publishMainStream(joining.userId, 'messagingMessage', messageObj); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	// 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
 | 	// 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
 | ||||||
| 	setTimeout(async () => { | 	setTimeout(async () => { | ||||||
| 		const freshMessage = await MessagingMessages.findOne({ id: message.id }); | 		const freshMessage = await MessagingMessages.findOne(message.id); | ||||||
| 		if (freshMessage == null) return; // メッセージが削除されている場合もある
 | 		if (freshMessage == null) return; // メッセージが削除されている場合もある
 | ||||||
| 		if (!freshMessage.isRead) { | 
 | ||||||
|  | 		if (recipientUser) { | ||||||
|  | 			if (freshMessage.isRead) return; // 既読
 | ||||||
|  | 
 | ||||||
| 			//#region ただしミュートされているなら発行しない
 | 			//#region ただしミュートされているなら発行しない
 | ||||||
| 			const mute = await Mutings.find({ | 			const mute = await Mutings.find({ | ||||||
| 				muterId: recipient.id, | 				muterId: recipientUser.id, | ||||||
| 			}); | 			}); | ||||||
| 			const mutedUserIds = mute.map(m => m.muteeId.toString()); | 			const mutedUserIds = mute.map(m => m.muteeId.toString()); | ||||||
| 			if (mutedUserIds.indexOf(user.id) != -1) { | 			if (mutedUserIds.indexOf(user.id) != -1) { | ||||||
|  | @ -140,8 +202,15 @@ export default define(meta, async (ps, user) => { | ||||||
| 			} | 			} | ||||||
| 			//#endregion
 | 			//#endregion
 | ||||||
| 
 | 
 | ||||||
| 			publishMainStream(message.recipientId, 'unreadMessagingMessage', messageObj); | 			publishMainStream(recipientUser.id, 'unreadMessagingMessage', messageObj); | ||||||
| 			pushSw(message.recipientId, 'unreadMessagingMessage', messageObj); | 			pushSw(recipientUser.id, 'unreadMessagingMessage', messageObj); | ||||||
|  | 		} else if (recipientGroup) { | ||||||
|  | 			const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id, userId: Not(user.id) }); | ||||||
|  | 			for (const joining of joinings) { | ||||||
|  | 				if (freshMessage.reads.includes(joining.userId)) return; // 既読
 | ||||||
|  | 				publishMainStream(joining.userId, 'unreadMessagingMessage', messageObj); | ||||||
|  | 				pushSw(joining.userId, 'unreadMessagingMessage', messageObj); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	}, 2000); | 	}, 2000); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import { ID } from '../../../../../misc/cafy-id'; | import { ID } from '../../../../../misc/cafy-id'; | ||||||
| import define from '../../../define'; | import define from '../../../define'; | ||||||
| import { publishMessagingStream } from '../../../../../services/stream'; | import { publishMessagingStream, publishGroupMessagingStream } from '../../../../../services/stream'; | ||||||
| import * as ms from 'ms'; | import * as ms from 'ms'; | ||||||
| import { ApiError } from '../../../error'; | import { ApiError } from '../../../error'; | ||||||
| import { MessagingMessages } from '../../../../../models'; | import { MessagingMessages } from '../../../../../models'; | ||||||
|  | @ -10,7 +10,7 @@ export const meta = { | ||||||
| 	stability: 'stable', | 	stability: 'stable', | ||||||
| 
 | 
 | ||||||
| 	desc: { | 	desc: { | ||||||
| 		'ja-JP': '指定したメッセージを削除します。', | 		'ja-JP': '指定したトークメッセージを削除します。', | ||||||
| 		'en-US': 'Delete a message.' | 		'en-US': 'Delete a message.' | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -57,6 +57,10 @@ export default define(meta, async (ps, user) => { | ||||||
| 
 | 
 | ||||||
| 	await MessagingMessages.delete(message.id); | 	await MessagingMessages.delete(message.id); | ||||||
| 
 | 
 | ||||||
| 	publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); | 	if (message.recipientId) { | ||||||
| 	publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); | 		publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); | ||||||
|  | 		publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); | ||||||
|  | 	} else if (message.groupId) { | ||||||
|  | 		publishGroupMessagingStream(message.groupId, 'deleted', message.id); | ||||||
|  | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import { ID } from '../../../../../misc/cafy-id'; | import { ID } from '../../../../../misc/cafy-id'; | ||||||
| import read from '../../../common/read-messaging-message'; |  | ||||||
| import define from '../../../define'; | import define from '../../../define'; | ||||||
| import { ApiError } from '../../../error'; | import { ApiError } from '../../../error'; | ||||||
| import { MessagingMessages } from '../../../../../models'; | import { MessagingMessages } from '../../../../../models'; | ||||||
|  | import { readUserMessagingMessage, readGroupMessagingMessage } from '../../../common/read-messaging-message'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	desc: { | 	desc: { | ||||||
| 		'ja-JP': '指定した自分宛てのメッセージを既読にします。', | 		'ja-JP': '指定した自分宛てのトークメッセージを既読にします。', | ||||||
| 		'en-US': 'Mark as read a message of messaging.' | 		'en-US': 'Mark as read a message of messaging.' | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -39,12 +39,21 @@ export const meta = { | ||||||
| export default define(meta, async (ps, user) => { | export default define(meta, async (ps, user) => { | ||||||
| 	const message = await MessagingMessages.findOne({ | 	const message = await MessagingMessages.findOne({ | ||||||
| 		id: ps.messageId, | 		id: ps.messageId, | ||||||
| 		recipientId: user.id |  | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	if (message == null) { | 	if (message == null) { | ||||||
| 		throw new ApiError(meta.errors.noSuchMessage); | 		throw new ApiError(meta.errors.noSuchMessage); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	read(user.id, message.userId, [message.id]); | 	if (message.recipientId) { | ||||||
|  | 		await readUserMessagingMessage(user.id, message.recipientId, [message.id]).catch(e => { | ||||||
|  | 			if (e.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage); | ||||||
|  | 			throw e; | ||||||
|  | 		}); | ||||||
|  | 	} else if (message.groupId) { | ||||||
|  | 		await readGroupMessagingMessage(user.id, message.groupId, [message.id]).catch(e => { | ||||||
|  | 			if (e.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage); | ||||||
|  | 			throw e; | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
							
								
								
									
										51
									
								
								src/server/api/endpoints/users/groups/create.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/server/api/endpoints/users/groups/create.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import define from '../../../define'; | ||||||
|  | import { UserGroups, UserGroupJoinings } from '../../../../../models'; | ||||||
|  | import { genId } from '../../../../../misc/gen-id'; | ||||||
|  | import { UserGroup } from '../../../../../models/entities/user-group'; | ||||||
|  | import { types, bool } from '../../../../../misc/schema'; | ||||||
|  | import { UserGroupJoining } from '../../../../../models/entities/user-group-joining'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': 'ユーザーグループを作成します。', | ||||||
|  | 		'en-US': 'Create a user group.' | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	tags: ['groups'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 
 | ||||||
|  | 	kind: 'write:user-groups', | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		name: { | ||||||
|  | 			validator: $.str.range(1, 100) | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	res: { | ||||||
|  | 		type: types.object, | ||||||
|  | 		optional: bool.false, nullable: bool.false, | ||||||
|  | 		ref: 'UserGroup', | ||||||
|  | 	}, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, user) => { | ||||||
|  | 	const userGroup = await UserGroups.save({ | ||||||
|  | 		id: genId(), | ||||||
|  | 		createdAt: new Date(), | ||||||
|  | 		userId: user.id, | ||||||
|  | 		name: ps.name, | ||||||
|  | 	} as UserGroup); | ||||||
|  | 
 | ||||||
|  | 	// Push the owner
 | ||||||
|  | 	await UserGroupJoinings.save({ | ||||||
|  | 		id: genId(), | ||||||
|  | 		createdAt: new Date(), | ||||||
|  | 		userId: user.id, | ||||||
|  | 		userGroupId: userGroup.id | ||||||
|  | 	} as UserGroupJoining); | ||||||
|  | 
 | ||||||
|  | 	return await UserGroups.pack(userGroup); | ||||||
|  | }); | ||||||
							
								
								
									
										49
									
								
								src/server/api/endpoints/users/groups/delete.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/server/api/endpoints/users/groups/delete.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import { ID } from '../../../../../misc/cafy-id'; | ||||||
|  | import define from '../../../define'; | ||||||
|  | import { ApiError } from '../../../error'; | ||||||
|  | import { UserGroups } from '../../../../../models'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '指定したユーザーグループを削除します。', | ||||||
|  | 		'en-US': 'Delete a user group' | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	tags: ['groups'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 
 | ||||||
|  | 	kind: 'write:user-groups', | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		groupId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': '対象となるユーザーグループのID', | ||||||
|  | 				'en-US': 'ID of target user group' | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 		noSuchGroup: { | ||||||
|  | 			message: 'No such group.', | ||||||
|  | 			code: 'NO_SUCH_GROUP', | ||||||
|  | 			id: '63dbd64c-cd77-413f-8e08-61781e210b38' | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, user) => { | ||||||
|  | 	const userGroup = await UserGroups.findOne({ | ||||||
|  | 		id: ps.groupId, | ||||||
|  | 		userId: user.id | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (userGroup == null) { | ||||||
|  | 		throw new ApiError(meta.errors.noSuchGroup); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	await UserGroups.delete(userGroup.id); | ||||||
|  | }); | ||||||
							
								
								
									
										33
									
								
								src/server/api/endpoints/users/groups/joined.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/server/api/endpoints/users/groups/joined.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | import define from '../../../define'; | ||||||
|  | import { UserGroups, UserGroupJoinings } from '../../../../../models'; | ||||||
|  | import { types, bool } from '../../../../../misc/schema'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '自分の所属するユーザーグループ一覧を取得します。' | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	tags: ['groups', 'account'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 
 | ||||||
|  | 	kind: 'read:user-groups', | ||||||
|  | 
 | ||||||
|  | 	res: { | ||||||
|  | 		type: types.array, | ||||||
|  | 		optional: bool.false, nullable: bool.false, | ||||||
|  | 		items: { | ||||||
|  | 			type: types.object, | ||||||
|  | 			optional: bool.false, nullable: bool.false, | ||||||
|  | 			ref: 'UserGroup', | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, me) => { | ||||||
|  | 	const joinings = await UserGroupJoinings.find({ | ||||||
|  | 		userId: me.id, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId))); | ||||||
|  | }); | ||||||
							
								
								
									
										33
									
								
								src/server/api/endpoints/users/groups/owned.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/server/api/endpoints/users/groups/owned.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | import define from '../../../define'; | ||||||
|  | import { UserGroups } from '../../../../../models'; | ||||||
|  | import { types, bool } from '../../../../../misc/schema'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '自分の作成したユーザーグループ一覧を取得します。' | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	tags: ['groups', 'account'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 
 | ||||||
|  | 	kind: 'read:user-groups', | ||||||
|  | 
 | ||||||
|  | 	res: { | ||||||
|  | 		type: types.array, | ||||||
|  | 		optional: bool.false, nullable: bool.false, | ||||||
|  | 		items: { | ||||||
|  | 			type: types.object, | ||||||
|  | 			optional: bool.false, nullable: bool.false, | ||||||
|  | 			ref: 'UserGroup', | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, me) => { | ||||||
|  | 	const userGroups = await UserGroups.find({ | ||||||
|  | 		userId: me.id, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	return await Promise.all(userGroups.map(x => UserGroups.pack(x))); | ||||||
|  | }); | ||||||
							
								
								
									
										68
									
								
								src/server/api/endpoints/users/groups/pull.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/server/api/endpoints/users/groups/pull.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,68 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import { ID } from '../../../../../misc/cafy-id'; | ||||||
|  | import define from '../../../define'; | ||||||
|  | import { ApiError } from '../../../error'; | ||||||
|  | import { getUser } from '../../../common/getters'; | ||||||
|  | import { UserGroups, UserGroupJoinings } from '../../../../../models'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '指定したユーザーグループから指定したユーザーを削除します。', | ||||||
|  | 		'en-US': 'Remove a user to a user group.' | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	tags: ['groups', 'users'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 
 | ||||||
|  | 	kind: 'write:user-groups', | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		groupId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		userId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': '対象のユーザーのID', | ||||||
|  | 				'en-US': 'Target user ID' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 		noSuchGroup: { | ||||||
|  | 			message: 'No such group.', | ||||||
|  | 			code: 'NO_SUCH_GROUP', | ||||||
|  | 			id: '4662487c-05b1-4b78-86e5-fd46998aba74' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		noSuchUser: { | ||||||
|  | 			message: 'No such user.', | ||||||
|  | 			code: 'NO_SUCH_USER', | ||||||
|  | 			id: '0b5cc374-3681-41da-861e-8bc1146f7a55' | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, me) => { | ||||||
|  | 	// Fetch the group
 | ||||||
|  | 	const userGroup = await UserGroups.findOne({ | ||||||
|  | 		id: ps.groupId, | ||||||
|  | 		userId: me.id, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (userGroup == null) { | ||||||
|  | 		throw new ApiError(meta.errors.noSuchGroup); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Fetch the user
 | ||||||
|  | 	const user = await getUser(ps.userId).catch(e => { | ||||||
|  | 		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); | ||||||
|  | 		throw e; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	// Pull the user
 | ||||||
|  | 	await UserGroupJoinings.delete({ userId: user.id }); | ||||||
|  | }); | ||||||
							
								
								
									
										90
									
								
								src/server/api/endpoints/users/groups/push.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/server/api/endpoints/users/groups/push.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import { ID } from '../../../../../misc/cafy-id'; | ||||||
|  | import define from '../../../define'; | ||||||
|  | import { ApiError } from '../../../error'; | ||||||
|  | import { getUser } from '../../../common/getters'; | ||||||
|  | import { UserGroups, UserGroupJoinings } from '../../../../../models'; | ||||||
|  | import { genId } from '../../../../../misc/gen-id'; | ||||||
|  | import { UserGroupJoining } from '../../../../../models/entities/user-group-joining'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '指定したユーザーグループに指定したユーザーを追加します。', | ||||||
|  | 		'en-US': 'Add a user to a user group.' | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	tags: ['groups', 'users'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 
 | ||||||
|  | 	kind: 'write:user-groups', | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		groupId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		userId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': '対象のユーザーのID', | ||||||
|  | 				'en-US': 'Target user ID' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 		noSuchGroup: { | ||||||
|  | 			message: 'No such group.', | ||||||
|  | 			code: 'NO_SUCH_GROUP', | ||||||
|  | 			id: '583f8bc0-8eee-4b78-9299-1e14fc91e409' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		noSuchUser: { | ||||||
|  | 			message: 'No such user.', | ||||||
|  | 			code: 'NO_SUCH_USER', | ||||||
|  | 			id: 'da52de61-002c-475b-90e1-ba64f9cf13a8' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		alreadyAdded: { | ||||||
|  | 			message: 'That user has already been added to that group.', | ||||||
|  | 			code: 'ALREADY_ADDED', | ||||||
|  | 			id: '7e35c6a0-39b2-4488-aea6-6ee20bd5da2c' | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, me) => { | ||||||
|  | 	// Fetch the group
 | ||||||
|  | 	const userGroup = await UserGroups.findOne({ | ||||||
|  | 		id: ps.groupId, | ||||||
|  | 		userId: me.id, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (userGroup == null) { | ||||||
|  | 		throw new ApiError(meta.errors.noSuchGroup); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Fetch the user
 | ||||||
|  | 	const user = await getUser(ps.userId).catch(e => { | ||||||
|  | 		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); | ||||||
|  | 		throw e; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	const exist = await UserGroupJoinings.findOne({ | ||||||
|  | 		userGroupId: userGroup.id, | ||||||
|  | 		userId: user.id | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (exist) { | ||||||
|  | 		throw new ApiError(meta.errors.alreadyAdded); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Push the user
 | ||||||
|  | 	await UserGroupJoinings.save({ | ||||||
|  | 		id: genId(), | ||||||
|  | 		createdAt: new Date(), | ||||||
|  | 		userId: user.id, | ||||||
|  | 		userGroupId: userGroup.id | ||||||
|  | 	} as UserGroupJoining); | ||||||
|  | }); | ||||||
							
								
								
									
										53
									
								
								src/server/api/endpoints/users/groups/show.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/server/api/endpoints/users/groups/show.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 { UserGroups } from '../../../../../models'; | ||||||
|  | import { types, bool } from '../../../../../misc/schema'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '指定したユーザーグループの情報を取得します。', | ||||||
|  | 		'en-US': 'Show a user group.' | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	tags: ['groups', 'account'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 
 | ||||||
|  | 	kind: 'read:user-groups', | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		groupId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	res: { | ||||||
|  | 		type: types.object, | ||||||
|  | 		optional: bool.false, nullable: bool.false, | ||||||
|  | 		ref: 'UserGroup', | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 		noSuchGroup: { | ||||||
|  | 			message: 'No such group.', | ||||||
|  | 			code: 'NO_SUCH_GROUP', | ||||||
|  | 			id: 'ea04751e-9b7e-487b-a509-330fb6bd6b9b' | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, me) => { | ||||||
|  | 	// Fetch the group
 | ||||||
|  | 	const userGroup = await UserGroups.findOne({ | ||||||
|  | 		id: ps.groupId, | ||||||
|  | 		userId: me.id, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (userGroup == null) { | ||||||
|  | 		throw new ApiError(meta.errors.noSuchGroup); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return await UserGroups.pack(userGroup); | ||||||
|  | }); | ||||||
|  | @ -80,5 +80,5 @@ export default define(meta, async (ps, me) => { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Push the user
 | 	// Push the user
 | ||||||
| 	pushUserToUserList(user, userList); | 	await pushUserToUserList(user, userList); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -23,4 +23,6 @@ export const kinds = [ | ||||||
| 	'write:pages', | 	'write:pages', | ||||||
| 	'write:page-likes', | 	'write:page-likes', | ||||||
| 	'read:page-likes', | 	'read:page-likes', | ||||||
|  | 	'read:user-groups', | ||||||
|  | 	'write:user-groups', | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ import { packedBlockingSchema } from '../../../models/repositories/blocking'; | ||||||
| import { packedNoteReactionSchema } from '../../../models/repositories/note-reaction'; | import { packedNoteReactionSchema } from '../../../models/repositories/note-reaction'; | ||||||
| import { packedHashtagSchema } from '../../../models/repositories/hashtag'; | import { packedHashtagSchema } from '../../../models/repositories/hashtag'; | ||||||
| import { packedPageSchema } from '../../../models/repositories/page'; | import { packedPageSchema } from '../../../models/repositories/page'; | ||||||
|  | import { packedUserGroupSchema } from '../../../models/repositories/user-group'; | ||||||
| 
 | 
 | ||||||
| export function convertSchemaToOpenApiSchema(schema: Schema) { | export function convertSchemaToOpenApiSchema(schema: Schema) { | ||||||
| 	const res: any = schema; | 	const res: any = schema; | ||||||
|  | @ -66,6 +67,7 @@ export const schemas = { | ||||||
| 
 | 
 | ||||||
| 	User: convertSchemaToOpenApiSchema(packedUserSchema), | 	User: convertSchemaToOpenApiSchema(packedUserSchema), | ||||||
| 	UserList: convertSchemaToOpenApiSchema(packedUserListSchema), | 	UserList: convertSchemaToOpenApiSchema(packedUserListSchema), | ||||||
|  | 	UserGroup: convertSchemaToOpenApiSchema(packedUserGroupSchema), | ||||||
| 	App: convertSchemaToOpenApiSchema(packedAppSchema), | 	App: convertSchemaToOpenApiSchema(packedAppSchema), | ||||||
| 	MessagingMessage: convertSchemaToOpenApiSchema(packedMessagingMessageSchema), | 	MessagingMessage: convertSchemaToOpenApiSchema(packedMessagingMessageSchema), | ||||||
| 	Note: convertSchemaToOpenApiSchema(packedNoteSchema), | 	Note: convertSchemaToOpenApiSchema(packedNoteSchema), | ||||||
|  |  | ||||||
|  | @ -1,20 +1,39 @@ | ||||||
| import autobind from 'autobind-decorator'; | import autobind from 'autobind-decorator'; | ||||||
| import read from '../../common/read-messaging-message'; | import { readUserMessagingMessage, readGroupMessagingMessage } from '../../common/read-messaging-message'; | ||||||
| import Channel from '../channel'; | import Channel from '../channel'; | ||||||
|  | import { UserGroupJoinings } from '../../../../models'; | ||||||
| 
 | 
 | ||||||
| export default class extends Channel { | export default class extends Channel { | ||||||
| 	public readonly chName = 'messaging'; | 	public readonly chName = 'messaging'; | ||||||
| 	public static shouldShare = false; | 	public static shouldShare = false; | ||||||
| 	public static requireCredential = true; | 	public static requireCredential = true; | ||||||
| 
 | 
 | ||||||
| 	private otherpartyId: string; | 	private otherpartyId: string | null; | ||||||
|  | 	private groupId: string | null; | ||||||
| 
 | 
 | ||||||
| 	@autobind | 	@autobind | ||||||
| 	public async init(params: any) { | 	public async init(params: any) { | ||||||
| 		this.otherpartyId = params.otherparty as string; | 		this.otherpartyId = params.otherparty as string; | ||||||
|  | 		this.groupId = params.group as string; | ||||||
|  | 
 | ||||||
|  | 		// Check joining
 | ||||||
|  | 		if (this.groupId) { | ||||||
|  | 			const joining = await UserGroupJoinings.findOne({ | ||||||
|  | 				userId: this.user!.id, | ||||||
|  | 				userGroupId: this.groupId | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			if (joining == null) { | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		const subCh = this.otherpartyId | ||||||
|  | 			? `messagingStream:${this.user!.id}-${this.otherpartyId}` | ||||||
|  | 			: `messagingStream:${this.groupId}`; | ||||||
| 
 | 
 | ||||||
| 		// Subscribe messaging stream
 | 		// Subscribe messaging stream
 | ||||||
| 		this.subscriber.on(`messagingStream:${this.user!.id}-${this.otherpartyId}`, data => { | 		this.subscriber.on(subCh, data => { | ||||||
| 			this.send(data); | 			this.send(data); | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
|  | @ -23,7 +42,11 @@ export default class extends Channel { | ||||||
| 	public onMessage(type: string, body: any) { | 	public onMessage(type: string, body: any) { | ||||||
| 		switch (type) { | 		switch (type) { | ||||||
| 			case 'read': | 			case 'read': | ||||||
| 				read(this.user!.id, this.otherpartyId, [body.id]); | 				if (this.otherpartyId) { | ||||||
|  | 					readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]); | ||||||
|  | 				} else if (this.groupId) { | ||||||
|  | 					readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]); | ||||||
|  | 				} | ||||||
| 				break; | 				break; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import { User } from '../models/entities/user'; | ||||||
| import { Note } from '../models/entities/note'; | import { Note } from '../models/entities/note'; | ||||||
| import { UserList } from '../models/entities/user-list'; | import { UserList } from '../models/entities/user-list'; | ||||||
| import { ReversiGame } from '../models/entities/games/reversi/game'; | import { ReversiGame } from '../models/entities/games/reversi/game'; | ||||||
|  | import { UserGroup } from '../models/entities/user-group'; | ||||||
| 
 | 
 | ||||||
| class Publisher { | class Publisher { | ||||||
| 	private publish = (channel: string, type: string | null, value?: any): void => { | 	private publish = (channel: string, type: string | null, value?: any): void => { | ||||||
|  | @ -39,6 +40,10 @@ class Publisher { | ||||||
| 		this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); | 		this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	public publishGroupMessagingStream = (groupId: UserGroup['id'], type: string, value?: any): void => { | ||||||
|  | 		this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => { | 	public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => { | ||||||
| 		this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); | 		this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); | ||||||
| 	} | 	} | ||||||
|  | @ -74,6 +79,7 @@ export const publishNoteStream = publisher.publishNoteStream; | ||||||
| export const publishNotesStream = publisher.publishNotesStream; | export const publishNotesStream = publisher.publishNotesStream; | ||||||
| export const publishUserListStream = publisher.publishUserListStream; | export const publishUserListStream = publisher.publishUserListStream; | ||||||
| export const publishMessagingStream = publisher.publishMessagingStream; | export const publishMessagingStream = publisher.publishMessagingStream; | ||||||
|  | export const publishGroupMessagingStream = publisher.publishGroupMessagingStream; | ||||||
| export const publishMessagingIndexStream = publisher.publishMessagingIndexStream; | export const publishMessagingIndexStream = publisher.publishMessagingIndexStream; | ||||||
| export const publishReversiStream = publisher.publishReversiStream; | export const publishReversiStream = publisher.publishReversiStream; | ||||||
| export const publishReversiGameStream = publisher.publishReversiGameStream; | export const publishReversiGameStream = publisher.publishReversiGameStream; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue