parent
							
								
									61f54f8f74
								
							
						
					
					
						commit
						c7cc3dcdfd
					
				
					 65 changed files with 1797 additions and 638 deletions
				
			
		|  | @ -265,6 +265,7 @@ common: | |||
|   my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" | ||||
|   hide-password: "パスワードを隠す" | ||||
|   show-password: "パスワードを表示する" | ||||
|   enter-username: "ユーザー名を入力してください" | ||||
| 
 | ||||
|   do-not-use-in-production: "これは開発ビルドです。本番環境で使用しないでください。" | ||||
|   user-suspended: "このユーザーは凍結されています。" | ||||
|  | @ -480,20 +481,24 @@ common/views/components/messaging.vue: | |||
|   search-user: "ユーザーを探す" | ||||
|   you: "あなた" | ||||
|   no-history: "履歴はありません" | ||||
|   user: "ユーザー" | ||||
|   group: "グループ" | ||||
|   start-with-user: "ユーザーとトークを開始" | ||||
|   start-with-group: "グループとトークを開始" | ||||
| 
 | ||||
| common/views/components/messaging-room.vue: | ||||
|   empty: "このユーザーと話したことはありません" | ||||
|   not-talked-user: "このユーザーとの会話はありません" | ||||
|   not-talked-group: "このグループでの会話はありません" | ||||
|   no-history: "これより過去の履歴はありません" | ||||
|   resize-form: "ドラッグしてフォームの広さを調整" | ||||
|   new-message: "新しいメッセージがあります" | ||||
|   only-one-file-attached: "メッセージに添付できるのはひとつのファイルのみです" | ||||
|   only-one-file-attached: "メッセージに添付できるファイルはひとつです" | ||||
| 
 | ||||
| common/views/components/messaging-room.form.vue: | ||||
|   input-message-here: "ここにメッセージを入力" | ||||
|   send: "送信" | ||||
|   attach-from-local: "PCからファイルを添付する" | ||||
|   attach-from-drive: "ドライブからファイルを添付する" | ||||
|   only-one-file-attached: "メッセージに添付できるのはひとつのファイルのみです" | ||||
|   only-one-file-attached: "メッセージに添付できるファイルはひとつです" | ||||
| 
 | ||||
| common/views/components/messaging-room.message.vue: | ||||
|   is-read: "既読" | ||||
|  | @ -750,11 +755,27 @@ common/views/components/user-list-editor.vue: | |||
|   remove-user: "このリストから削除" | ||||
|   delete-are-you-sure: "リスト「$1」を削除しますか?" | ||||
|   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: | ||||
|   user-lists: "リスト" | ||||
|   create-list: "リストを作成" | ||||
|   list-name: "リスト名" | ||||
| 
 | ||||
| common/views/components/user-groups.vue: | ||||
|   user-groups: "グループ" | ||||
|   create-group: "グループを作成" | ||||
|   group-name: "グループ名" | ||||
| 
 | ||||
| common/views/widgets/broadcast.vue: | ||||
|   fetching: "確認中" | ||||
|   no-broadcasts: "お知らせはありません" | ||||
|  | @ -827,6 +848,11 @@ common/views/pages/follow.vue: | |||
|   follow-processing: "フォロー処理中" | ||||
|   follow-request: "フォロー申請" | ||||
| 
 | ||||
| common/views/pages/follow-requests.vue: | ||||
|   received-follow-requests: "フォロー申請" | ||||
|   accept: "承認" | ||||
|   reject: "拒否" | ||||
| 
 | ||||
| desktop: | ||||
|   banner-crop-title: "バナーとして表示する部分を選択" | ||||
|   banner: "バナー" | ||||
|  | @ -1139,6 +1165,7 @@ desktop/views/components/ui.header.vue: | |||
| desktop/views/components/ui.header.account.vue: | ||||
|   profile: "プロフィール" | ||||
|   lists: "リスト" | ||||
|   groups: "グループ" | ||||
|   follow-requests: "フォロー申請" | ||||
|   admin: "管理" | ||||
| 
 | ||||
|  | @ -1154,14 +1181,6 @@ desktop/views/components/ui.header.post.vue: | |||
| desktop/views/components/ui.header.search.vue: | ||||
|   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: | ||||
|   notes: "投稿" | ||||
|   following: "フォロー" | ||||
|  | @ -1749,11 +1768,6 @@ mobile/views/pages/widgets/activity.vue: | |||
| mobile/views/pages/share.vue: | ||||
|   share-with: "{name}で共有" | ||||
| 
 | ||||
| mobile/views/pages/received-follow-requests.vue: | ||||
|   title: "フォロー申請" | ||||
|   accept: "承認" | ||||
|   reject: "拒否" | ||||
| 
 | ||||
| mobile/views/pages/note.vue: | ||||
|   title: "投稿" | ||||
|   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'"/> | ||||
| 			</div> | ||||
| 			<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> | ||||
| 			<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> | ||||
|  |  | |||
|  | @ -44,6 +44,8 @@ import uiSwitch from './ui/switch.vue'; | |||
| import uiRadio from './ui/radio.vue'; | ||||
| import uiSelect from './ui/select.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 formRadio from './ui/form/radio.vue'; | ||||
| 
 | ||||
|  | @ -91,5 +93,7 @@ Vue.component('ui-switch', uiSwitch); | |||
| Vue.component('ui-radio', uiRadio); | ||||
| Vue.component('ui-select', uiSelect); | ||||
| Vue.component('ui-info', uiInfo); | ||||
| Vue.component('ui-margin', uiMargin); | ||||
| Vue.component('ui-hr', uiHr); | ||||
| Vue.component('form-button', formButton); | ||||
| Vue.component('form-radio', formRadio); | ||||
|  |  | |||
|  | @ -33,7 +33,16 @@ import * as autosize from 'autosize'; | |||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/messaging-room.form.vue'), | ||||
| 	props: ['user'], | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			requird: false, | ||||
| 		}, | ||||
| 		group: { | ||||
| 			type: Object, | ||||
| 			requird: false, | ||||
| 		}, | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			text: null, | ||||
|  | @ -43,7 +52,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 	computed: { | ||||
| 		draftId(): string { | ||||
| 			return this.user.id; | ||||
| 			return this.user ? 'user:' + this.user.id : 'group:' + this.group.id; | ||||
| 		}, | ||||
| 		canSend(): boolean { | ||||
| 			return (this.text != null && this.text != '') || this.file != null; | ||||
|  | @ -159,7 +168,8 @@ export default Vue.extend({ | |||
| 		send() { | ||||
| 			this.sending = true; | ||||
| 			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, | ||||
| 				fileId: this.file ? this.file.id : undefined | ||||
| 			}).then(message => { | ||||
|  |  | |||
|  | @ -23,7 +23,12 @@ | |||
| 		<div></div> | ||||
| 		<mk-url-preview v-for="url in urls" :url="url" :key="url"/> | ||||
| 		<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"/> | ||||
| 			<template v-if="message.is_edited"><fa icon="pencil-alt"/></template> | ||||
| 		</footer> | ||||
|  | @ -42,6 +47,9 @@ export default Vue.extend({ | |||
| 	props: { | ||||
| 		message: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 		isGroup: { | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| 	computed: { | ||||
|  |  | |||
|  | @ -4,14 +4,14 @@ | |||
| 	@drop.prevent.stop="onDrop" | ||||
| > | ||||
| 	<div class="body"> | ||||
| 		<p class="init" v-if="init"><fa icon="spinner .spin"/>{{ $t('@.loading') }}</p> | ||||
| 		<p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ $t('empty') }}</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"/>{{ 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> | ||||
| 		<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') }} | ||||
| 		</button> | ||||
| 		<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"> | ||||
| 				<span>{{ _messages[i + 1]._datetext }}</span> | ||||
| 			</p> | ||||
|  | @ -23,7 +23,7 @@ | |||
| 				<button @click="onIndicatorClick"><i><fa :icon="faArrowCircleDown"/></i>{{ $t('new-message') }}</button> | ||||
| 			</div> | ||||
| 		</transition> | ||||
| 		<x-form :user="user" ref="form"/> | ||||
| 		<x-form :user="user" :group="group" ref="form"/> | ||||
| 	</footer> | ||||
| </div> | ||||
| </template> | ||||
|  | @ -34,17 +34,30 @@ import i18n from '../../../i18n'; | |||
| import XMessage from './messaging-room.message.vue'; | ||||
| import XForm from './messaging-room.form.vue'; | ||||
| import { url } from '../../../config'; | ||||
| import { faArrowCircleDown } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faFlag } from '@fortawesome/free-regular-svg-icons'; | ||||
| import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/messaging-room.vue'), | ||||
| 
 | ||||
| 	components: { | ||||
| 		XMessage, | ||||
| 		XForm | ||||
| 	}, | ||||
| 
 | ||||
| 	props: ['user', 'isNaked'], | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			requird: false, | ||||
| 		}, | ||||
| 		group: { | ||||
| 			type: Object, | ||||
| 			requird: false, | ||||
| 		}, | ||||
| 		isNaked: { | ||||
| 			type: Boolean, | ||||
| 			requird: false, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
|  | @ -76,7 +89,10 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	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('read', this.onRead); | ||||
|  | @ -147,7 +163,8 @@ export default Vue.extend({ | |||
| 				const max = this.existMoreMessages ? 20 : 10; | ||||
| 
 | ||||
| 				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, | ||||
| 					untilId: this.existMoreMessages ? this.messages[0].id : undefined | ||||
| 				}).then(messages => { | ||||
|  | @ -199,12 +216,21 @@ export default Vue.extend({ | |||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onRead(ids) { | ||||
| 			if (!Array.isArray(ids)) ids = [ids]; | ||||
| 			for (const id of ids) { | ||||
| 				if (this.messages.some(x => x.id == id)) { | ||||
| 					const exist = this.messages.map(x => x.id).indexOf(id); | ||||
| 					this.messages[exist].isRead = true; | ||||
| 		onRead(x) { | ||||
| 			if (this.user) { | ||||
| 				if (!Array.isArray(x)) x = [x]; | ||||
| 				for (const id of x) { | ||||
| 					if (this.messages.some(x => x.id == id)) { | ||||
| 						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 class="history" v-if="messages.length > 0"> | ||||
| 		<template> | ||||
| 			<a v-for="message in messages" | ||||
| 				class="user" | ||||
| 				:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" | ||||
| 				:data-is-me="isMe(message)" | ||||
| 				:data-is-read="message.isRead" | ||||
| 				@click.prevent="navigate(isMe(message) ? message.recipient : message.user)" | ||||
| 				:key="message.id" | ||||
| 			> | ||||
| 				<div> | ||||
| 					<mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/> | ||||
| 					<header> | ||||
| 						<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> | ||||
| 						<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 class="title">{{ $t('user') }}</div> | ||||
| 		<a v-for="message in messages" | ||||
| 			class="user" | ||||
| 			:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" | ||||
| 			:data-is-me="isMe(message)" | ||||
| 			:data-is-read="message.isRead" | ||||
| 			@click.prevent="navigate(isMe(message) ? message.recipient : message.user)" | ||||
| 			:key="message.id" | ||||
| 		> | ||||
| 			<div> | ||||
| 				<mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/> | ||||
| 				<header> | ||||
| 					<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> | ||||
| 					<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> | ||||
| 			</a> | ||||
| 		</template> | ||||
| 			</div> | ||||
| 		</a> | ||||
| 	</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> | ||||
| 	<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> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faUser, faUsers } from '@fortawesome/free-solid-svg-icons'; | ||||
| import i18n from '../../../i18n'; | ||||
| import getAcct from '../../../../../misc/acct/render'; | ||||
| 
 | ||||
|  | @ -71,9 +97,11 @@ export default Vue.extend({ | |||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			messages: [], | ||||
| 			groupMessages: [], | ||||
| 			q: null, | ||||
| 			result: [], | ||||
| 			connection: null | ||||
| 			connection: null, | ||||
| 			faUser, faUsers | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
|  | @ -82,9 +110,12 @@ export default Vue.extend({ | |||
| 		this.connection.on('message', this.onMessage); | ||||
| 		this.connection.on('read', this.onRead); | ||||
| 
 | ||||
| 		this.$root.api('messaging/history').then(messages => { | ||||
| 			this.messages = messages; | ||||
| 			this.fetching = false; | ||||
| 		this.$root.api('messaging/history', { group: false }).then(messages => { | ||||
| 			this.$root.api('messaging/history', { group: true }).then(groupMessages => { | ||||
| 				this.messages = messages; | ||||
| 				this.groupMessages = groupMessages; | ||||
| 				this.fetching = false; | ||||
| 			}); | ||||
| 		}); | ||||
| 	}, | ||||
| 	beforeDestroy() { | ||||
|  | @ -96,16 +127,27 @@ export default Vue.extend({ | |||
| 			return message.userId == this.$store.state.i.id; | ||||
| 		}, | ||||
| 		onMessage(message) { | ||||
| 			this.messages = this.messages.filter(m => !( | ||||
| 				(m.recipientId == message.recipientId && m.userId == message.userId) || | ||||
| 				(m.recipientId == message.userId && m.userId == message.recipientId))); | ||||
| 			if (message.recipientId) { | ||||
| 				this.messages = this.messages.filter(m => !( | ||||
| 					(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) { | ||||
| 			for (const id of ids) { | ||||
| 				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() { | ||||
|  | @ -125,6 +167,9 @@ export default Vue.extend({ | |||
| 		navigate(user) { | ||||
| 			this.$emit('navigate', user); | ||||
| 		}, | ||||
| 		navigateGroup(group) { | ||||
| 			this.$emit('navigateGroup', group); | ||||
| 		}, | ||||
| 		onSearchKeydown(e) { | ||||
| 			switch (e.which) { | ||||
| 				case 9: // [TAB] | ||||
|  | @ -161,6 +206,30 @@ export default Vue.extend({ | |||
| 					(list.childNodes[i].nextElementSibling || list.childNodes[0]).focus(); | ||||
| 					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 | ||||
| 
 | ||||
| 		> .history | ||||
| 			> .title | ||||
| 				padding 8px | ||||
| 
 | ||||
| 			> a | ||||
| 				&:last-child | ||||
| 					border-bottom none | ||||
|  | @ -311,6 +383,13 @@ export default Vue.extend({ | |||
| 						color rgba(#000, 0.3) | ||||
| 
 | ||||
| 	> .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 | ||||
| 			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'), | ||||
| 			action: this.pushList | ||||
| 		}] as any; | ||||
| 		 | ||||
| 
 | ||||
| 		if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) { | ||||
| 			menu = menu.concat([null, { | ||||
| 				icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'], | ||||
|  |  | |||
|  | @ -1,34 +1,45 @@ | |||
| <template> | ||||
| <x-column> | ||||
| 	<template #header> | ||||
| 		<fa :icon="faHashtag"/>{{ $t('@.explore') }} | ||||
| 		<fa :icon="icon"/>{{ title }} | ||||
| 	</template> | ||||
| 
 | ||||
| 	<div> | ||||
| 		<x-explore v-bind="$attrs"/> | ||||
| 		<component :is="component" @init="init" v-bind="$attrs"/> | ||||
| 	</div> | ||||
| </x-column> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| 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({ | ||||
| 	i18n: i18n(), | ||||
| 
 | ||||
| 	components: { | ||||
| 		XColumn, | ||||
| 		XExplore, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		component: { | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			faHashtag | ||||
| 			title: null, | ||||
| 			icon: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		init(v) { | ||||
| 			this.title = v.title; | ||||
| 			this.icon = v.icon; | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | @ -116,6 +116,10 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.$emit('init', { | ||||
| 			title: this.$t('@.explore'), | ||||
| 			icon: faHashtag | ||||
| 		}); | ||||
| 		this.$root.api('hashtags/list', { | ||||
| 			sort: '+attachedLocalUsers', | ||||
| 			attachedToLocalUserOnly: true, | ||||
|  |  | |||
|  | @ -1,27 +1,30 @@ | |||
| <template> | ||||
| <mk-ui> | ||||
| 	<template #header><fa :icon="['far', 'envelope']"/>{{ $t('title') }}</template> | ||||
| 
 | ||||
| 	<main> | ||||
| 		<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> | ||||
| 	<ui-container :body-togglable="true"> | ||||
| 		<template #header>{{ $t('received-follow-requests') }}</template> | ||||
| 		<div v-if="!fetching"> | ||||
| 			<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="mcbzkkaw"> | ||||
| 				<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> | ||||
| 			</sequential-entrance> | ||||
| 		</div> | ||||
| 	</main> | ||||
| </mk-ui> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import Progress from '../../../common/scripts/loading'; | ||||
| import Progress from '../../scripts/loading'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('mobile/views/pages/received-follow-requests.vue'), | ||||
| 	i18n: i18n('common/views/pages/follow-requests.vue'), | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
|  | @ -29,14 +32,10 @@ export default Vue.extend({ | |||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		document.title = this.$t('title'); | ||||
| 
 | ||||
| 		Progress.start(); | ||||
| 
 | ||||
| 		this.$root.api('following/requests/list').then(requests => { | ||||
| 			this.fetching = false; | ||||
| 			this.requests = requests; | ||||
| 
 | ||||
| 			Progress.done(); | ||||
| 		}); | ||||
| 	}, | ||||
|  | @ -56,7 +55,7 @@ export default Vue.extend({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| main | ||||
| .mcbzkkaw | ||||
| 	> div | ||||
| 		display flex | ||||
| 		padding 16px | ||||
|  | @ -50,6 +50,11 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 
 | ||||
| 		this.$emit('init', { | ||||
| 			title: this.$t('@.pages'), | ||||
| 			icon: faStickyNote | ||||
| 		}); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		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> | ||||
| <div class="cudqjmnl"> | ||||
| 	<ui-card> | ||||
| 		<template #title><fa :icon="faList"/> {{ list.name }}</template> | ||||
| 	<ui-container v-if="list"> | ||||
| 		<template #header><fa :icon="faListUl"/> {{ list.name }}</template> | ||||
| 
 | ||||
| 		<section> | ||||
| 			<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> | ||||
| 			<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> | ||||
| 		<section class="fwvevrks"> | ||||
| 			<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-card> | ||||
| 	</ui-container> | ||||
| 
 | ||||
| 	<ui-card> | ||||
| 		<template #title><fa :icon="faUsers"/> {{ $t('users') }}</template> | ||||
| 	<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="phcqulfl" v-for="user in users"> | ||||
| 					<div> | ||||
|  | @ -32,34 +37,44 @@ | |||
| 				</div> | ||||
| 			</sequential-entrance> | ||||
| 		</section> | ||||
| 	</ui-card> | ||||
| 	</ui-container> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| 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'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/user-list-editor.vue'), | ||||
| 
 | ||||
| 	props: { | ||||
| 		list: { | ||||
| 		listId: { | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			list: null, | ||||
| 			users: [], | ||||
| 			faList, faICursor, faTrashAlt, faUsers | ||||
| 			faListUl, faICursor, faTrashAlt, faUsers, faPlus | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.fetchUsers(); | ||||
| 	created() { | ||||
| 		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: { | ||||
|  | @ -117,6 +132,21 @@ export default Vue.extend({ | |||
| 			}).then(() => { | ||||
| 				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 | ||||
| 	.phcqulfl | ||||
| 		display flex | ||||
| 		padding 16px 0 | ||||
| 		padding 16px | ||||
| 		border-top solid 1px var(--faceDivider) | ||||
| 
 | ||||
| 		> 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 MkNotFound from '../common/views/pages/not-found.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 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: '/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: '/explore', name: 'explore', component: () => import('../common/views/deck/deck.explore-column.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: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-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', 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/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: '', 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: '/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/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/: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/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/drive', component: MkDrive }, | ||||
| 			{ path: '/i/drive/folder/:folder', component: MkDrive }, | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <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> | ||||
| 	<x-messaging-room :user="user" :class="$style.content"/> | ||||
| 	<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" :group="group" :class="$style.content"/> | ||||
| </mk-window> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -16,10 +16,14 @@ export default Vue.extend({ | |||
| 	components: { | ||||
| 		XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default) | ||||
| 	}, | ||||
| 	props: ['user'], | ||||
| 	props: ['user', 'group'], | ||||
| 	computed: { | ||||
| 		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> | ||||
| <mk-window ref="window" width="500px" height="560px" @closed="destroyDom"> | ||||
| 	<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> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -20,6 +20,11 @@ export default Vue.extend({ | |||
| 			this.$root.new(MkMessagingRoomWindow, { | ||||
| 				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> | ||||
| 					</router-link> | ||||
| 				</li> | ||||
| 				<li @click="list"> | ||||
| 					<p> | ||||
| 				<li> | ||||
| 					<router-link to="/i/lists"> | ||||
| 						<i><fa icon="list" fixed-width/></i> | ||||
| 						<span>{{ $t('lists') }}</span> | ||||
| 						<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> | ||||
| 					<router-link to="/i/pages"> | ||||
|  | @ -42,12 +49,12 @@ | |||
| 						<i><fa icon="angle-right"/></i> | ||||
| 					</router-link> | ||||
| 				</li> | ||||
| 				<li @click="followRequests" v-if="($store.state.i.isLocked || $store.state.i.carefulBot)"> | ||||
| 					<p> | ||||
| 				<li v-if="($store.state.i.isLocked || $store.state.i.carefulBot)"> | ||||
| 					<router-link to="/i/follow-requests"> | ||||
| 						<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> | ||||
| 						<i><fa icon="angle-right"/></i> | ||||
| 					</p> | ||||
| 					</router-link> | ||||
| 				</li> | ||||
| 			</ul> | ||||
| 			<ul> | ||||
|  | @ -96,12 +103,10 @@ | |||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| 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 MkDriveWindow from './drive-window.vue'; | ||||
| 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'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
|  | @ -109,7 +114,7 @@ export default Vue.extend({ | |||
| 	data() { | ||||
| 		return { | ||||
| 			isOpen: false, | ||||
| 			faHome, faColumns, faMoon, faSun, faStickyNote | ||||
| 			faHome, faColumns, faMoon, faSun, faStickyNote, faUsers | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
|  | @ -147,14 +152,6 @@ export default Vue.extend({ | |||
| 			this.close(); | ||||
| 			this.$root.new(MkDriveWindow); | ||||
| 		}, | ||||
| 		list() { | ||||
| 			this.close(); | ||||
| 			this.$root.new(MkUserListsWindow); | ||||
| 		}, | ||||
| 		followRequests() { | ||||
| 			this.close(); | ||||
| 			this.$root.new(MkFollowRequestsWindow); | ||||
| 		}, | ||||
| 		signout() { | ||||
| 			this.$root.signout(); | ||||
| 		}, | ||||
|  |  | |||
|  | @ -72,8 +72,6 @@ | |||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| 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 MkDriveWindow from './drive-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> | ||||
| <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> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -19,7 +19,8 @@ export default Vue.extend({ | |||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			user: null | ||||
| 			user: null, | ||||
| 			group: null | ||||
| 		}; | ||||
| 	}, | ||||
| 	watch: { | ||||
|  | @ -47,14 +48,25 @@ export default Vue.extend({ | |||
| 			Progress.start(); | ||||
| 			this.fetching = true; | ||||
| 
 | ||||
| 			this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { | ||||
| 				this.user = user; | ||||
| 				this.fetching = false; | ||||
| 			if (this.$route.params.user) { | ||||
| 				this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { | ||||
| 					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 #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> | ||||
| </div> | ||||
| </template> | ||||
|  | @ -31,6 +31,11 @@ export default define({ | |||
| 				user: user | ||||
| 			}); | ||||
| 		}, | ||||
| 		navigateGroup(group) { | ||||
| 			this.$root.new(MkMessagingRoomWindow, { | ||||
| 				group: group | ||||
| 			}); | ||||
| 		}, | ||||
| 		add() { | ||||
| 			this.$root.new(MkMessagingWindow); | ||||
| 		}, | ||||
|  |  | |||
|  | @ -18,17 +18,16 @@ import MkDrive from './views/pages/drive.vue'; | |||
| import MkWidgets from './views/pages/widgets.vue'; | ||||
| import MkMessaging from './views/pages/messaging.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 MkSearch from './views/pages/search.vue'; | ||||
| import MkFavorites from './views/pages/favorites.vue'; | ||||
| import MkUserLists from './views/pages/user-lists.vue'; | ||||
| import MkUserList from './views/pages/user-list.vue'; | ||||
| import UI from './views/pages/ui.vue'; | ||||
| import MkReversi from './views/pages/games/reversi.vue'; | ||||
| import MkTag from './views/pages/tag.vue'; | ||||
| import MkShare from '../common/views/pages/share.vue'; | ||||
| import MkFollow from '../common/views/pages/follow.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 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: '/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: '/explore', name: 'explore', component: () => import('../common/views/deck/deck.explore-column.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: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-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', 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/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 }, | ||||
|  | @ -135,12 +139,15 @@ init((launch, os) => { | |||
| 			{ path: '/signup', name: 'signup', component: MkSignup }, | ||||
| 			{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, | ||||
| 			{ path: '/i/favorites', name: 'favorites', component: MkFavorites }, | ||||
| 			{ path: '/i/pages', name: 'pages', component: () => import('./views/pages/pages.vue').then(m => m.default) }, | ||||
| 			{ path: '/i/lists', name: 'user-lists', component: MkUserLists }, | ||||
| 			{ path: '/i/lists/:list', name: 'user-list', component: MkUserList }, | ||||
| 			{ path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests }, | ||||
| 			{ 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: UI, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, | ||||
| 			{ 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/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/messaging', name: 'messaging', component: MkMessaging }, | ||||
| 			{ path: '/i/messaging/group/:group', component: MkMessagingRoom }, | ||||
| 			{ path: '/i/messaging/:user', component: MkMessagingRoom }, | ||||
| 			{ path: '/i/drive', name: 'drive', component: MkDrive }, | ||||
| 			{ path: '/i/drive/folder/:folder', component: MkDrive }, | ||||
|  | @ -151,8 +158,8 @@ init((launch, os) => { | |||
| 			{ path: '/search', component: MkSearch }, | ||||
| 			{ path: '/tags/:tag', component: MkTag }, | ||||
| 			{ 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/tags/:tag', name: 'explore-tag', props: true, 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', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, | ||||
| 			{ path: '/share', component: MkShare }, | ||||
| 			{ path: '/games/reversi/:game?', name: 'reversi', component: MkReversi }, | ||||
| 			{ 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><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 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="/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> | ||||
|  |  | |||
|  | @ -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> | ||||
| 	<template #header> | ||||
| 		<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> | ||||
| 	<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> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -22,6 +23,7 @@ export default Vue.extend({ | |||
| 		return { | ||||
| 			fetching: true, | ||||
| 			user: null, | ||||
| 			group: null, | ||||
| 			unwatchDarkmode: null | ||||
| 		}; | ||||
| 	}, | ||||
|  | @ -48,12 +50,21 @@ export default Vue.extend({ | |||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.fetching = true; | ||||
| 			this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { | ||||
| 				this.user = user; | ||||
| 				this.fetching = false; | ||||
| 			if (this.$route.params.user) { | ||||
| 				this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { | ||||
| 					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> | ||||
| <mk-ui> | ||||
| 	<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> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -21,6 +21,9 @@ export default Vue.extend({ | |||
| 	methods: { | ||||
| 		navigate(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 { UserList } from '../models/entities/user-list'; | ||||
| import { UserListJoining } from '../models/entities/user-list-joining'; | ||||
| import { UserGroup } from '../models/entities/user-group'; | ||||
| import { UserGroupJoining } from '../models/entities/user-group-joining'; | ||||
| import { Hashtag } from '../models/entities/hashtag'; | ||||
| import { NoteFavorite } from '../models/entities/note-favorite'; | ||||
| import { AbuseUserReport } from '../models/entities/abuse-user-report'; | ||||
|  | @ -106,6 +108,8 @@ export function initDb(justBorrow = false, sync = false, log = false) { | |||
| 			UserPublickey, | ||||
| 			UserList, | ||||
| 			UserListJoining, | ||||
| 			UserGroup, | ||||
| 			UserGroupJoining, | ||||
| 			UserNotePining, | ||||
| 			Following, | ||||
| 			FollowRequest, | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ | |||
| import { User } from './user'; | ||||
| import { DriveFile } from './drive-file'; | ||||
| import { id } from '../id'; | ||||
| import { UserGroup } from './user-group'; | ||||
| 
 | ||||
| @Entity() | ||||
| export class MessagingMessage { | ||||
|  | @ -29,10 +30,10 @@ export class MessagingMessage { | |||
| 
 | ||||
| 	@Index() | ||||
| 	@Column({ | ||||
| 		...id(), | ||||
| 		...id(), nullable: true, | ||||
| 		comment: 'The recipient user ID.' | ||||
| 	}) | ||||
| 	public recipientId: User['id']; | ||||
| 	public recipientId: User['id'] | null; | ||||
| 
 | ||||
| 	@ManyToOne(type => User, { | ||||
| 		onDelete: 'CASCADE' | ||||
|  | @ -40,6 +41,19 @@ export class MessagingMessage { | |||
| 	@JoinColumn() | ||||
| 	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', { | ||||
| 		length: 4096, nullable: true | ||||
| 	}) | ||||
|  | @ -50,6 +64,12 @@ export class MessagingMessage { | |||
| 	}) | ||||
| 	public isRead: boolean; | ||||
| 
 | ||||
| 	@Column({ | ||||
| 		...id(), | ||||
| 		array: true, default: '{}' | ||||
| 	}) | ||||
| 	public reads: User['id'][]; | ||||
| 
 | ||||
| 	@Column({ | ||||
| 		...id(), | ||||
| 		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 { SwSubscription } from './entities/sw-subscription'; | ||||
| import { NoteWatching } from './entities/note-watching'; | ||||
| import { UserListJoining } from './entities/user-list-joining'; | ||||
| import { NoteUnread } from './entities/note-unread'; | ||||
| import { RegistrationTicket } from './entities/registration-tickets'; | ||||
| import { UserRepository } from './repositories/user'; | ||||
|  | @ -20,6 +19,9 @@ import { SigninRepository } from './repositories/signin'; | |||
| import { MessagingMessageRepository } from './repositories/messaging-message'; | ||||
| import { ReversiGameRepository } from './repositories/games/reversi/game'; | ||||
| 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 { MutingRepository } from './repositories/muting'; | ||||
| import { BlockingRepository } from './repositories/blocking'; | ||||
|  | @ -52,6 +54,8 @@ export const UserKeypairs = getRepository(UserKeypair); | |||
| export const UserPublickeys = getRepository(UserPublickey); | ||||
| export const UserLists = getCustomRepository(UserListRepository); | ||||
| export const UserListJoinings = getRepository(UserListJoining); | ||||
| export const UserGroups = getCustomRepository(UserGroupRepository); | ||||
| export const UserGroupJoinings = getRepository(UserGroupJoining); | ||||
| export const UserNotePinings = getRepository(UserNotePining); | ||||
| export const Followings = getCustomRepository(FollowingRepository); | ||||
| export const FollowRequests = getCustomRepository(FollowRequestRepository); | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { EntityRepository, Repository } from 'typeorm'; | ||||
| import { MessagingMessage } from '../entities/messaging-message'; | ||||
| import { Users, DriveFiles } from '..'; | ||||
| import { Users, DriveFiles, UserGroups } from '..'; | ||||
| import { ensure } from '../../prelude/ensure'; | ||||
| import { types, bool, SchemaType } from '../../misc/schema'; | ||||
| 
 | ||||
|  | @ -16,11 +16,13 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> { | |||
| 		src: MessagingMessage['id'] | MessagingMessage, | ||||
| 		me?: any, | ||||
| 		options?: { | ||||
| 			populateRecipient: boolean | ||||
| 			populateRecipient?: boolean, | ||||
| 			populateGroup?: boolean, | ||||
| 		} | ||||
| 	): Promise<PackedMessagingMessage> { | ||||
| 		const opts = options || { | ||||
| 			populateRecipient: true | ||||
| 			populateRecipient: true, | ||||
| 			populateGroup: true, | ||||
| 		}; | ||||
| 
 | ||||
| 		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, | ||||
| 			user: await Users.pack(message.user || message.userId, me), | ||||
| 			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, | ||||
| 			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: { | ||||
| 			type: types.string, | ||||
| 			optional: bool.false, nullable: bool.false, | ||||
| 			optional: bool.false, nullable: bool.true, | ||||
| 			format: 'id', | ||||
| 		}, | ||||
| 		recipient: { | ||||
| 			type: types.object, | ||||
| 			optional: bool.true, nullable: bool.false, | ||||
| 			optional: bool.true, nullable: bool.true, | ||||
| 			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: { | ||||
| 			type: types.boolean, | ||||
| 			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 { 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 config from '../../config'; | ||||
| 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( | ||||
| 		src: User['id'] | User, | ||||
| 		me?: User['id'] | User | null | undefined, | ||||
|  | @ -151,13 +176,7 @@ export class UserRepository extends Repository<User> { | |||
| 				autoWatch: profile!.autoWatch, | ||||
| 				alwaysMarkNsfw: profile!.alwaysMarkNsfw, | ||||
| 				carefulBot: profile!.carefulBot, | ||||
| 				hasUnreadMessagingMessage: MessagingMessages.count({ | ||||
| 					where: { | ||||
| 						recipientId: user.id, | ||||
| 						isRead: false | ||||
| 					}, | ||||
| 					take: 1 | ||||
| 				}).then(count => count > 0), | ||||
| 				hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id), | ||||
| 				hasUnreadNotification: Notifications.count({ | ||||
| 					where: { | ||||
| 						notifieeId: user.id, | ||||
|  |  | |||
|  | @ -1,21 +1,33 @@ | |||
| import { publishMainStream } from '../../../services/stream'; | ||||
| import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream'; | ||||
| import { publishMessagingStream } from '../../../services/stream'; | ||||
| import { publishMessagingIndexStream } from '../../../services/stream'; | ||||
| import { User } from '../../../models/entities/user'; | ||||
| import { MessagingMessage } from '../../../models/entities/messaging-message'; | ||||
| import { MessagingMessages } from '../../../models'; | ||||
| import { MessagingMessages, UserGroupJoinings, Users } from '../../../models'; | ||||
| import { In } from 'typeorm'; | ||||
| import { IdentifiableError } from '../../../misc/identifiable-error'; | ||||
| import { UserGroup } from '../../../models/entities/user-group'; | ||||
| 
 | ||||
| /** | ||||
|  * Mark messages as read | ||||
|  */ | ||||
| export default async ( | ||||
| export async function readUserMessagingMessage( | ||||
| 	userId: User['id'], | ||||
| 	otherpartyId: User['id'], | ||||
| 	messageIds: MessagingMessage['id'][] | ||||
| ) => { | ||||
| ) { | ||||
| 	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
 | ||||
| 	await MessagingMessages.update({ | ||||
| 		id: In(messageIds), | ||||
|  | @ -30,14 +42,62 @@ export default async ( | |||
| 	publishMessagingStream(otherpartyId, userId, 'read', messageIds); | ||||
| 	publishMessagingIndexStream(userId, 'read', messageIds); | ||||
| 
 | ||||
| 	// Calc count of my unread messages
 | ||||
| 	const count = await MessagingMessages.count({ | ||||
| 		recipientId: userId, | ||||
| 		isRead: false | ||||
| 	}); | ||||
| 
 | ||||
| 	if (count == 0) { | ||||
| 	if (!Users.getHasUnreadMessagingMessage(userId)) { | ||||
| 		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 | ||||
| 		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 define from '../../define'; | ||||
| import { MessagingMessage } from '../../../../models/entities/messaging-message'; | ||||
| import { MessagingMessages, Mutings } from '../../../../models'; | ||||
| import { MessagingMessages, Mutings, UserGroupJoinings } from '../../../../models'; | ||||
| import { Brackets } from 'typeorm'; | ||||
| import { types, bool } from '../../../../misc/schema'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	desc: { | ||||
| 		'ja-JP': 'Messagingの履歴を取得します。', | ||||
| 		'ja-JP': 'トークの履歴を取得します。', | ||||
| 		'en-US': 'Show messaging history.' | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -21,6 +21,11 @@ export const meta = { | |||
| 		limit: { | ||||
| 			validator: $.optional.num.range(1, 100), | ||||
| 			default: 10 | ||||
| 		}, | ||||
| 
 | ||||
| 		group: { | ||||
| 			validator: $.optional.bool, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -40,26 +45,46 @@ export default define(meta, async (ps, user) => { | |||
| 		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[] = []; | ||||
| 
 | ||||
| 	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') | ||||
| 			.where(new Brackets(qb => { qb | ||||
| 				.where(`message.userId = :userId`, { userId: user.id }) | ||||
| 				.orWhere(`message.recipientId = :userId`, { userId: user.id }); | ||||
| 			})) | ||||
| 			.orderBy('message.createdAt', 'DESC'); | ||||
| 
 | ||||
| 		if (found.length > 0) { | ||||
| 			query.andWhere(`message.userId NOT IN (:...found)`, { found: found }); | ||||
| 			query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found }); | ||||
| 		} | ||||
| 		if (ps.group) { | ||||
| 			query.where(`message.groupId IN (:...groups)`, { groups: groups }); | ||||
| 
 | ||||
| 		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) }); | ||||
| 			if (found.length > 0) { | ||||
| 				query.andWhere(`message.groupId NOT IN (:...found)`, { found: found }); | ||||
| 			} | ||||
| 		} 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(); | ||||
|  |  | |||
|  | @ -1,16 +1,17 @@ | |||
| import $ from 'cafy'; | ||||
| import { ID } from '../../../../misc/cafy-id'; | ||||
| import read from '../../common/read-messaging-message'; | ||||
| import define from '../../define'; | ||||
| import { ApiError } from '../../error'; | ||||
| import { getUser } from '../../common/getters'; | ||||
| import { MessagingMessages } from '../../../../models'; | ||||
| import { MessagingMessages, UserGroups, UserGroupJoinings } from '../../../../models'; | ||||
| import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||
| import { types, bool } from '../../../../misc/schema'; | ||||
| import { Brackets } from 'typeorm'; | ||||
| import { readUserMessagingMessage, readGroupMessagingMessage } from '../../common/read-messaging-message'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	desc: { | ||||
| 		'ja-JP': '指定したユーザーとのMessagingのメッセージ一覧を取得します。', | ||||
| 		'ja-JP': 'トークメッセージ一覧を取得します。', | ||||
| 		'en-US': 'Get messages of messaging.' | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -22,13 +23,21 @@ export const meta = { | |||
| 
 | ||||
| 	params: { | ||||
| 		userId: { | ||||
| 			validator: $.type(ID), | ||||
| 			validator: $.optional.type(ID), | ||||
| 			desc: { | ||||
| 				'ja-JP': '対象のユーザーのID', | ||||
| 				'en-US': 'Target user ID' | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		groupId: { | ||||
| 			validator: $.optional.type(ID), | ||||
| 			desc: { | ||||
| 				'ja-JP': '対象のグループのID', | ||||
| 				'en-US': 'Target group ID' | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		limit: { | ||||
| 			validator: $.optional.num.range(1, 100), | ||||
| 			default: 10 | ||||
|  | @ -64,27 +73,85 @@ export const meta = { | |||
| 			code: 'NO_SUCH_USER', | ||||
| 			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) => { | ||||
| 	// Fetch recipient
 | ||||
| 	const recipient = await getUser(ps.userId).catch(e => { | ||||
| 		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); | ||||
| 		throw e; | ||||
| 	}); | ||||
| 	if (ps.userId != null) { | ||||
| 		// Fetch recipient (user)
 | ||||
| 		const recipient = await getUser(ps.userId).catch(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) | ||||
| 		.andWhere(`(message.userId = :meId AND message.recipientId = :recipientId) OR (message.userId = :recipientId AND message.recipientId = :meId)`, { meId: user.id, recipientId: recipient.id }); | ||||
| 		const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) | ||||
| 			.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
 | ||||
| 	if (ps.markAsRead) { | ||||
| 		read(user.id, recipient.id, messages.map(x => x.id)); | ||||
| 		// Mark all as read
 | ||||
| 		if (ps.markAsRead) { | ||||
| 			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 { ID } from '../../../../../misc/cafy-id'; | ||||
| import { publishMainStream } from '../../../../../services/stream'; | ||||
| import { publishMainStream, publishGroupMessagingStream } from '../../../../../services/stream'; | ||||
| import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../services/stream'; | ||||
| import pushSw from '../../../../../services/push-notification'; | ||||
| import define from '../../../define'; | ||||
| import { ApiError } from '../../../error'; | ||||
| 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 { genId } from '../../../../../misc/gen-id'; | ||||
| 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 = { | ||||
| 	desc: { | ||||
| 		'ja-JP': '指定したユーザーへMessagingのメッセージを送信します。', | ||||
| 		'ja-JP': 'トークメッセージを送信します。', | ||||
| 		'en-US': 'Create a message of messaging.' | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -25,13 +28,21 @@ export const meta = { | |||
| 
 | ||||
| 	params: { | ||||
| 		userId: { | ||||
| 			validator: $.type(ID), | ||||
| 			validator: $.optional.type(ID), | ||||
| 			desc: { | ||||
| 				'ja-JP': '対象のユーザーのID', | ||||
| 				'en-US': 'Target user ID' | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		groupId: { | ||||
| 			validator: $.optional.type(ID), | ||||
| 			desc: { | ||||
| 				'ja-JP': '対象のグループのID', | ||||
| 				'en-US': 'Target group ID' | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		text: { | ||||
| 			validator: $.optional.str.pipe(MessagingMessages.isValidText) | ||||
| 		}, | ||||
|  | @ -60,6 +71,18 @@ export const meta = { | |||
| 			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: { | ||||
| 			message: 'No such file.', | ||||
| 			code: 'NO_SUCH_FILE', | ||||
|  | @ -75,16 +98,38 @@ export const meta = { | |||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps, user) => { | ||||
| 	// Myself
 | ||||
| 	if (ps.userId === user.id) { | ||||
| 		throw new ApiError(meta.errors.recipientIsYourself); | ||||
| 	} | ||||
| 	let recipientUser: User | undefined; | ||||
| 	let recipientGroup: UserGroup | undefined; | ||||
| 
 | ||||
| 	// Fetch recipient
 | ||||
| 	const recipient = await getUser(ps.userId).catch(e => { | ||||
| 		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); | ||||
| 		throw e; | ||||
| 	}); | ||||
| 	if (ps.userId != null) { | ||||
| 		// Myself
 | ||||
| 		if (ps.userId === user.id) { | ||||
| 			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; | ||||
| 	if (ps.fileId != null) { | ||||
|  | @ -107,32 +152,49 @@ export default define(meta, async (ps, user) => { | |||
| 		id: genId(), | ||||
| 		createdAt: new Date(), | ||||
| 		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, | ||||
| 		userId: user.id, | ||||
| 		isRead: false | ||||
| 		isRead: false, | ||||
| 		reads: [] as any[] | ||||
| 	} as MessagingMessage); | ||||
| 
 | ||||
| 	const messageObj = await MessagingMessages.pack(message); | ||||
| 
 | ||||
| 	// 自分のストリーム
 | ||||
| 	publishMessagingStream(message.userId, message.recipientId, 'message', messageObj); | ||||
| 	publishMessagingIndexStream(message.userId, 'message', messageObj); | ||||
| 	publishMainStream(message.userId, 'messagingMessage', messageObj); | ||||
| 	if (recipientUser) { | ||||
| 		// 自分のストリーム
 | ||||
| 		publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj); | ||||
| 		publishMessagingIndexStream(message.userId, 'message', messageObj); | ||||
| 		publishMainStream(message.userId, 'messagingMessage', messageObj); | ||||
| 
 | ||||
| 	// 相手のストリーム
 | ||||
| 	publishMessagingStream(message.recipientId, message.userId, 'message', messageObj); | ||||
| 	publishMessagingIndexStream(message.recipientId, 'message', messageObj); | ||||
| 	publishMainStream(message.recipientId, 'messagingMessage', messageObj); | ||||
| 		// 相手のストリーム
 | ||||
| 		publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj); | ||||
| 		publishMessagingIndexStream(recipientUser.id, 'message', 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秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
 | ||||
| 	setTimeout(async () => { | ||||
| 		const freshMessage = await MessagingMessages.findOne({ id: message.id }); | ||||
| 		const freshMessage = await MessagingMessages.findOne(message.id); | ||||
| 		if (freshMessage == null) return; // メッセージが削除されている場合もある
 | ||||
| 		if (!freshMessage.isRead) { | ||||
| 
 | ||||
| 		if (recipientUser) { | ||||
| 			if (freshMessage.isRead) return; // 既読
 | ||||
| 
 | ||||
| 			//#region ただしミュートされているなら発行しない
 | ||||
| 			const mute = await Mutings.find({ | ||||
| 				muterId: recipient.id, | ||||
| 				muterId: recipientUser.id, | ||||
| 			}); | ||||
| 			const mutedUserIds = mute.map(m => m.muteeId.toString()); | ||||
| 			if (mutedUserIds.indexOf(user.id) != -1) { | ||||
|  | @ -140,8 +202,15 @@ export default define(meta, async (ps, user) => { | |||
| 			} | ||||
| 			//#endregion
 | ||||
| 
 | ||||
| 			publishMainStream(message.recipientId, 'unreadMessagingMessage', messageObj); | ||||
| 			pushSw(message.recipientId, 'unreadMessagingMessage', messageObj); | ||||
| 			publishMainStream(recipientUser.id, '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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import $ from 'cafy'; | ||||
| import { ID } from '../../../../../misc/cafy-id'; | ||||
| import define from '../../../define'; | ||||
| import { publishMessagingStream } from '../../../../../services/stream'; | ||||
| import { publishMessagingStream, publishGroupMessagingStream } from '../../../../../services/stream'; | ||||
| import * as ms from 'ms'; | ||||
| import { ApiError } from '../../../error'; | ||||
| import { MessagingMessages } from '../../../../../models'; | ||||
|  | @ -10,7 +10,7 @@ export const meta = { | |||
| 	stability: 'stable', | ||||
| 
 | ||||
| 	desc: { | ||||
| 		'ja-JP': '指定したメッセージを削除します。', | ||||
| 		'ja-JP': '指定したトークメッセージを削除します。', | ||||
| 		'en-US': 'Delete a message.' | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -57,6 +57,10 @@ export default define(meta, async (ps, user) => { | |||
| 
 | ||||
| 	await MessagingMessages.delete(message.id); | ||||
| 
 | ||||
| 	publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); | ||||
| 	publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); | ||||
| 	if (message.recipientId) { | ||||
| 		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 { ID } from '../../../../../misc/cafy-id'; | ||||
| import read from '../../../common/read-messaging-message'; | ||||
| import define from '../../../define'; | ||||
| import { ApiError } from '../../../error'; | ||||
| import { MessagingMessages } from '../../../../../models'; | ||||
| import { readUserMessagingMessage, readGroupMessagingMessage } from '../../../common/read-messaging-message'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	desc: { | ||||
| 		'ja-JP': '指定した自分宛てのメッセージを既読にします。', | ||||
| 		'ja-JP': '指定した自分宛てのトークメッセージを既読にします。', | ||||
| 		'en-US': 'Mark as read a message of messaging.' | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -39,12 +39,21 @@ export const meta = { | |||
| export default define(meta, async (ps, user) => { | ||||
| 	const message = await MessagingMessages.findOne({ | ||||
| 		id: ps.messageId, | ||||
| 		recipientId: user.id | ||||
| 	}); | ||||
| 
 | ||||
| 	if (message == null) { | ||||
| 		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
 | ||||
| 	pushUserToUserList(user, userList); | ||||
| 	await pushUserToUserList(user, userList); | ||||
| }); | ||||
|  |  | |||
|  | @ -23,4 +23,6 @@ export const kinds = [ | |||
| 	'write:pages', | ||||
| 	'write: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 { packedHashtagSchema } from '../../../models/repositories/hashtag'; | ||||
| import { packedPageSchema } from '../../../models/repositories/page'; | ||||
| import { packedUserGroupSchema } from '../../../models/repositories/user-group'; | ||||
| 
 | ||||
| export function convertSchemaToOpenApiSchema(schema: Schema) { | ||||
| 	const res: any = schema; | ||||
|  | @ -66,6 +67,7 @@ export const schemas = { | |||
| 
 | ||||
| 	User: convertSchemaToOpenApiSchema(packedUserSchema), | ||||
| 	UserList: convertSchemaToOpenApiSchema(packedUserListSchema), | ||||
| 	UserGroup: convertSchemaToOpenApiSchema(packedUserGroupSchema), | ||||
| 	App: convertSchemaToOpenApiSchema(packedAppSchema), | ||||
| 	MessagingMessage: convertSchemaToOpenApiSchema(packedMessagingMessageSchema), | ||||
| 	Note: convertSchemaToOpenApiSchema(packedNoteSchema), | ||||
|  |  | |||
|  | @ -1,20 +1,39 @@ | |||
| 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 { UserGroupJoinings } from '../../../../models'; | ||||
| 
 | ||||
| export default class extends Channel { | ||||
| 	public readonly chName = 'messaging'; | ||||
| 	public static shouldShare = false; | ||||
| 	public static requireCredential = true; | ||||
| 
 | ||||
| 	private otherpartyId: string; | ||||
| 	private otherpartyId: string | null; | ||||
| 	private groupId: string | null; | ||||
| 
 | ||||
| 	@autobind | ||||
| 	public async init(params: any) { | ||||
| 		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
 | ||||
| 		this.subscriber.on(`messagingStream:${this.user!.id}-${this.otherpartyId}`, data => { | ||||
| 		this.subscriber.on(subCh, data => { | ||||
| 			this.send(data); | ||||
| 		}); | ||||
| 	} | ||||
|  | @ -23,7 +42,11 @@ export default class extends Channel { | |||
| 	public onMessage(type: string, body: any) { | ||||
| 		switch (type) { | ||||
| 			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; | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import { User } from '../models/entities/user'; | |||
| import { Note } from '../models/entities/note'; | ||||
| import { UserList } from '../models/entities/user-list'; | ||||
| import { ReversiGame } from '../models/entities/games/reversi/game'; | ||||
| import { UserGroup } from '../models/entities/user-group'; | ||||
| 
 | ||||
| class Publisher { | ||||
| 	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); | ||||
| 	} | ||||
| 
 | ||||
| 	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 => { | ||||
| 		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 publishUserListStream = publisher.publishUserListStream; | ||||
| export const publishMessagingStream = publisher.publishMessagingStream; | ||||
| export const publishGroupMessagingStream = publisher.publishGroupMessagingStream; | ||||
| export const publishMessagingIndexStream = publisher.publishMessagingIndexStream; | ||||
| export const publishReversiStream = publisher.publishReversiStream; | ||||
| export const publishReversiGameStream = publisher.publishReversiGameStream; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue