summaryrefslogtreecommitdiff
path: root/src/client/app/common
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/app/common')
-rw-r--r--src/client/app/common/views/components/dialog.vue1
-rw-r--r--src/client/app/common/views/components/index.ts4
-rw-r--r--src/client/app/common/views/components/messaging-room.form.vue16
-rw-r--r--src/client/app/common/views/components/messaging-room.message.vue10
-rw-r--r--src/client/app/common/views/components/messaging-room.vue56
-rw-r--r--src/client/app/common/views/components/messaging.vue141
-rw-r--r--src/client/app/common/views/components/ui/hr.vue15
-rw-r--r--src/client/app/common/views/components/ui/margin.vue16
-rw-r--r--src/client/app/common/views/components/user-lists.vue95
-rw-r--r--src/client/app/common/views/components/user-menu.vue2
-rw-r--r--src/client/app/common/views/deck/deck.column-template.vue (renamed from src/client/app/common/views/deck/deck.explore-column.vue)29
-rw-r--r--src/client/app/common/views/pages/explore.vue4
-rw-r--r--src/client/app/common/views/pages/follow-requests.vue68
-rw-r--r--src/client/app/common/views/pages/pages.vue5
-rw-r--r--src/client/app/common/views/pages/user-group-editor.vue180
-rw-r--r--src/client/app/common/views/pages/user-groups.vue63
-rw-r--r--src/client/app/common/views/pages/user-list-editor.vue (renamed from src/client/app/common/views/components/user-list-editor.vue)60
-rw-r--r--src/client/app/common/views/pages/user-lists.vue63
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>