diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2019-05-18 20:36:33 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2019-05-18 20:36:33 +0900 |
| commit | c7cc3dcdfd2c0962a39e7186852a17dbd09b6a5b (patch) | |
| tree | c2e1671787c00daa8963c879dba6fbdab6f02d66 /src/client/app/common | |
| parent | Fix bug (diff) | |
| download | sharkey-c7cc3dcdfd2c0962a39e7186852a17dbd09b6a5b.tar.gz sharkey-c7cc3dcdfd2c0962a39e7186852a17dbd09b6a5b.tar.bz2 sharkey-c7cc3dcdfd2c0962a39e7186852a17dbd09b6a5b.zip | |
ユーザーグループ
Resolve #3218
Diffstat (limited to 'src/client/app/common')
18 files changed, 658 insertions, 170 deletions
diff --git a/src/client/app/common/views/components/dialog.vue b/src/client/app/common/views/components/dialog.vue index f22e0174b3..9f38031d62 100644 --- a/src/client/app/common/views/components/dialog.vue +++ b/src/client/app/common/views/components/dialog.vue @@ -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> diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index f4d40f9b1a..174fa36c00 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -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); diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue index ee6c312bce..1dfb0589e4 100644 --- a/src/client/app/common/views/components/messaging-room.form.vue +++ b/src/client/app/common/views/components/messaging-room.form.vue @@ -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 => { diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue index 908533e0cc..aff89c2573 100644 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -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: { diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue index a8980e068f..658dc93f64 100644 --- a/src/client/app/common/views/components/messaging-room.vue +++ b/src/client/app/common/views/components/messaging-room.vue @@ -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); + } } } }, diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue index f884a599d7..01d7a5a798 100644 --- a/src/client/app/common/views/components/messaging.vue +++ b/src/client/app/common/views/components/messaging.vue @@ -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 diff --git a/src/client/app/common/views/components/ui/hr.vue b/src/client/app/common/views/components/ui/hr.vue new file mode 100644 index 0000000000..38572cfcc3 --- /dev/null +++ b/src/client/app/common/views/components/ui/hr.vue @@ -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> diff --git a/src/client/app/common/views/components/ui/margin.vue b/src/client/app/common/views/components/ui/margin.vue new file mode 100644 index 0000000000..508116f070 --- /dev/null +++ b/src/client/app/common/views/components/ui/margin.vue @@ -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> diff --git a/src/client/app/common/views/components/user-lists.vue b/src/client/app/common/views/components/user-lists.vue deleted file mode 100644 index 699251b313..0000000000 --- a/src/client/app/common/views/components/user-lists.vue +++ /dev/null @@ -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> diff --git a/src/client/app/common/views/components/user-menu.vue b/src/client/app/common/views/components/user-menu.vue index 7cbffa9f9a..532dcf35c2 100644 --- a/src/client/app/common/views/components/user-menu.vue +++ b/src/client/app/common/views/components/user-menu.vue @@ -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'], diff --git a/src/client/app/common/views/deck/deck.explore-column.vue b/src/client/app/common/views/deck/deck.column-template.vue index 53db677b37..09583de4b2 100644 --- a/src/client/app/common/views/deck/deck.explore-column.vue +++ b/src/client/app/common/views/deck/deck.column-template.vue @@ -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> diff --git a/src/client/app/common/views/pages/explore.vue b/src/client/app/common/views/pages/explore.vue index d0e98035f8..bf0d7ab574 100644 --- a/src/client/app/common/views/pages/explore.vue +++ b/src/client/app/common/views/pages/explore.vue @@ -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, diff --git a/src/client/app/common/views/pages/follow-requests.vue b/src/client/app/common/views/pages/follow-requests.vue new file mode 100644 index 0000000000..860efefd93 --- /dev/null +++ b/src/client/app/common/views/pages/follow-requests.vue @@ -0,0 +1,68 @@ +<template> +<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> + </ui-container> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../i18n'; +import Progress from '../../scripts/loading'; + +export default Vue.extend({ + i18n: i18n('common/views/pages/follow-requests.vue'), + data() { + return { + fetching: true, + requests: [] + }; + }, + mounted() { + Progress.start(); + this.$root.api('following/requests/list').then(requests => { + this.fetching = false; + this.requests = requests; + Progress.done(); + }); + }, + 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); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mcbzkkaw + > div + display flex + padding 16px + border solid 1px var(--faceDivider) + border-radius 4px + + > span + margin 0 0 0 auto + +</style> diff --git a/src/client/app/common/views/pages/pages.vue b/src/client/app/common/views/pages/pages.vue index 751ea72374..d658728a19 100644 --- a/src/client/app/common/views/pages/pages.vue +++ b/src/client/app/common/views/pages/pages.vue @@ -50,6 +50,11 @@ export default Vue.extend({ }, created() { this.fetch(); + + this.$emit('init', { + title: this.$t('@.pages'), + icon: faStickyNote + }); }, methods: { async fetch() { diff --git a/src/client/app/common/views/pages/user-group-editor.vue b/src/client/app/common/views/pages/user-group-editor.vue new file mode 100644 index 0000000000..c658d0c6ff --- /dev/null +++ b/src/client/app/common/views/pages/user-group-editor.vue @@ -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> diff --git a/src/client/app/common/views/pages/user-groups.vue b/src/client/app/common/views/pages/user-groups.vue new file mode 100644 index 0000000000..336772799b --- /dev/null +++ b/src/client/app/common/views/pages/user-groups.vue @@ -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> diff --git a/src/client/app/common/views/components/user-list-editor.vue b/src/client/app/common/views/pages/user-list-editor.vue index 86024c4da3..6b2fd75f85 100644 --- a/src/client/app/common/views/components/user-list-editor.vue +++ b/src/client/app/common/views/pages/user-list-editor.vue @@ -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 diff --git a/src/client/app/common/views/pages/user-lists.vue b/src/client/app/common/views/pages/user-lists.vue new file mode 100644 index 0000000000..4c09eca6ce --- /dev/null +++ b/src/client/app/common/views/pages/user-lists.vue @@ -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> |