summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2019-02-16 10:58:44 +0900
committersyuilo <syuilotan@yahoo.co.jp>2019-02-16 10:58:44 +0900
commit88dc4c83cbde7960c280ae359569cfbaa120ae69 (patch)
treeb116f963124b934ac68a399aa7979d031342d467 /src
parentImprove user-list component (diff)
downloadsharkey-88dc4c83cbde7960c280ae359569cfbaa120ae69.tar.gz
sharkey-88dc4c83cbde7960c280ae359569cfbaa120ae69.tar.bz2
sharkey-88dc4c83cbde7960c280ae359569cfbaa120ae69.zip
Improve UI
Diffstat (limited to 'src')
-rw-r--r--src/client/app/common/views/components/user-list.vue3
-rw-r--r--src/client/app/common/views/pages/followers.vue30
-rw-r--r--src/client/app/common/views/pages/following.vue27
-rw-r--r--src/client/app/desktop/script.ts16
-rw-r--r--src/client/app/desktop/views/deck/deck.user-column.home.vue244
-rw-r--r--src/client/app/desktop/views/deck/deck.user-column.vue234
-rw-r--r--src/client/app/desktop/views/home/user/index.vue (renamed from src/client/app/desktop/views/home/user/user.vue)39
-rw-r--r--src/client/app/desktop/views/home/user/user.header.vue2
-rw-r--r--src/client/app/desktop/views/home/user/user.home.vue63
-rw-r--r--src/client/app/desktop/views/pages/user-following-or-followers.vue120
-rw-r--r--src/client/app/mobile/script.ts10
-rw-r--r--src/client/app/mobile/views/components/index.ts2
-rw-r--r--src/client/app/mobile/views/components/users-list.vue135
-rw-r--r--src/client/app/mobile/views/pages/followers.vue70
-rw-r--r--src/client/app/mobile/views/pages/following.vue69
-rw-r--r--src/client/app/mobile/views/pages/user/index.vue (renamed from src/client/app/mobile/views/pages/user.vue)37
-rw-r--r--src/server/api/endpoints/users/followers.ts23
-rw-r--r--src/server/api/endpoints/users/following.ts23
18 files changed, 449 insertions, 698 deletions
diff --git a/src/client/app/common/views/components/user-list.vue b/src/client/app/common/views/components/user-list.vue
index 9fcb80f8ad..5d851002d2 100644
--- a/src/client/app/common/views/components/user-list.vue
+++ b/src/client/app/common/views/components/user-list.vue
@@ -12,7 +12,7 @@
<router-link class="name" :to="user | userPage" v-user-preview="user.id"><mk-user-name :user="user"/></router-link>
<p class="username">@{{ user | acct }}</p>
</div>
- <div class="description" v-if="user.description">
+ <div class="description" v-if="user.description" :title="user.description">
<mfm :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :should-break="false"/>
</div>
</div>
@@ -137,5 +137,6 @@ export default Vue.extend({
overflow hidden
text-overflow ellipsis
opacity 0.7
+ font-size 14px
</style>
diff --git a/src/client/app/common/views/pages/followers.vue b/src/client/app/common/views/pages/followers.vue
new file mode 100644
index 0000000000..94d9c9b13c
--- /dev/null
+++ b/src/client/app/common/views/pages/followers.vue
@@ -0,0 +1,30 @@
+<template>
+<div>
+ <mk-user-list :make-promise="makePromise">{{ $t('@.followers') }}</mk-user-list>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import parseAcct from '../../../../../misc/acct/parse';
+import i18n from '../../../i18n';
+
+export default Vue.extend({
+ i18n: i18n(''),
+
+ data() {
+ return {
+ makePromise: cursor => this.$root.api('users/followers', {
+ ...parseAcct(this.$route.params.user),
+ limit: 30,
+ cursor: cursor ? cursor : undefined
+ }).then(x => {
+ return {
+ users: x.users,
+ cursor: x.next
+ };
+ }),
+ };
+ },
+});
+</script>
diff --git a/src/client/app/common/views/pages/following.vue b/src/client/app/common/views/pages/following.vue
new file mode 100644
index 0000000000..39739fa3da
--- /dev/null
+++ b/src/client/app/common/views/pages/following.vue
@@ -0,0 +1,27 @@
+<template>
+<div>
+ <mk-user-list :make-promise="makePromise">{{ $t('@.following') }}</mk-user-list>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import parseAcct from '../../../../../misc/acct/parse';
+
+export default Vue.extend({
+ data() {
+ return {
+ makePromise: cursor => this.$root.api('users/following', {
+ ...parseAcct(this.$route.params.user),
+ limit: 30,
+ cursor: cursor ? cursor : undefined
+ }).then(x => {
+ return {
+ users: x.users,
+ cursor: x.next
+ };
+ }),
+ };
+ },
+});
+</script>
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index 1ec7de0cc5..4c5b29d1f4 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -130,7 +130,11 @@ init(async (launch, os) => {
routes: [
os.store.getters.isSignedIn && os.store.state.device.deckMode
? { path: '/', name: 'index', component: MkDeck, children: [
- { path: '/@:user', name: 'user', component: () => import('./views/deck/deck.user-column.vue').then(m => m.default) },
+ { path: '/@:user', name: 'user', component: () => import('./views/deck/deck.user-column.vue').then(m => m.default), children: [
+ { path: '', name: 'user', component: () => import('./views/deck/deck.user-column.home.vue').then(m => m.default) },
+ { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
+ { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
+ ]},
{ path: '/notes/:note', name: 'note', component: () => import('./views/deck/deck.note-column.vue').then(m => m.default) },
{ path: '/search', component: () => import('./views/deck/deck.search-column.vue').then(m => m.default) },
{ path: '/tags/:tag', name: 'tag', component: () => import('./views/deck/deck.hashtag-column.vue').then(m => m.default) },
@@ -140,13 +144,17 @@ init(async (launch, os) => {
]}
: { path: '/', component: MkHome, children: [
{ path: '', name: 'index', component: MkHomeTimeline },
- { path: '/@:user', name: 'user', component: () => import('./views/home/user/user.vue').then(m => m.default) },
+ { path: '/@:user', component: () => import('./views/home/user/index.vue').then(m => m.default), children: [
+ { path: '', name: 'user', component: () => import('./views/home/user/user.home.vue').then(m => m.default) },
+ { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
+ { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
+ ]},
{ path: '/notes/:note', name: 'note', component: () => import('./views/home/note.vue').then(m => m.default) },
{ path: '/search', component: () => import('./views/home/search.vue').then(m => m.default) },
{ path: '/tags/:tag', name: 'tag', component: () => import('./views/home/tag.vue').then(m => m.default) },
{ path: '/featured', component: () => import('./views/home/featured.vue').then(m => m.default) },
{ path: '/explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
- { path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) }
+ { path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) },
]},
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', component: MkDrive },
@@ -155,8 +163,6 @@ init(async (launch, os) => {
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/share', component: MkShare },
{ path: '/games/reversi/:game?', component: MkReversi },
- { path: '/@:user/following', name: 'userFollowing', component: MkUserFollowingOrFollowers },
- { path: '/@:user/followers', name: 'userFollowers', component: MkUserFollowingOrFollowers },
{ path: '/authorize-follow', component: MkFollow },
{ path: '/deck', redirect: '/' },
{ path: '*', component: MkNotFound }
diff --git a/src/client/app/desktop/views/deck/deck.user-column.home.vue b/src/client/app/desktop/views/deck/deck.user-column.home.vue
new file mode 100644
index 0000000000..966c5bdb1b
--- /dev/null
+++ b/src/client/app/desktop/views/deck/deck.user-column.home.vue
@@ -0,0 +1,244 @@
+<template>
+<div>
+ <ui-container v-if="user.pinnedNotes && user.pinnedNotes.length > 0" :body-togglable="true">
+ <span slot="header"><fa icon="thumbtack"/> {{ $t('pinned-notes') }}</span>
+ <div>
+ <x-note v-for="n in user.pinnedNotes" :key="n.id" :note="n" :mini="true"/>
+ </div>
+ </ui-container>
+ <ui-container v-if="images.length > 0" :body-togglable="true">
+ <span slot="header"><fa :icon="['far', 'images']"/> {{ $t('images') }}</span>
+ <div class="sainvnaq">
+ <router-link v-for="image in images"
+ :style="`background-image: url(${image.thumbnailUrl})`"
+ :key="`${image.id}:${image._note.id}`"
+ :to="image._note | notePage"
+ :title="`${image.name}\n${(new Date(image.createdAt)).toLocaleString()}`"
+ ></router-link>
+ </div>
+ </ui-container>
+ <ui-container :body-togglable="true">
+ <span slot="header"><fa :icon="['far', 'chart-bar']"/> {{ $t('activity') }}</span>
+ <div>
+ <div ref="chart"></div>
+ </div>
+ </ui-container>
+ <ui-container>
+ <span slot="header"><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</span>
+ <div>
+ <x-notes ref="timeline" :more="existMore ? fetchMoreNotes : null"/>
+ </div>
+ </ui-container>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../../../i18n';
+import parseAcct from '../../../../../misc/acct/parse';
+import XNotes from './deck.notes.vue';
+import XNote from '../components/note.vue';
+import { concat } from '../../../../../prelude/array';
+import ApexCharts from 'apexcharts';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ i18n: i18n('deck/deck.user-column.vue'),
+ components: {
+ XNotes,
+ XNote
+ },
+
+ props: {
+ user: {
+ type: Object,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ existMore: false,
+ moreFetching: false,
+ withFiles: false,
+ images: [],
+ };
+ },
+
+ created() {
+ this.fetch();
+ },
+
+ methods: {
+ fetch() {
+ this.$nextTick(() => {
+ (this.$refs.timeline as any).init(() => this.initTl());
+ });
+
+ const image = [
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif'
+ ];
+
+ this.$root.api('users/notes', {
+ userId: this.user.id,
+ fileType: image,
+ excludeNsfw: !this.$store.state.device.alwaysShowNsfw,
+ limit: 9,
+ untilDate: new Date().getTime() + 1000 * 86400 * 365
+ }).then(notes => {
+ for (const note of notes) {
+ for (const file of note.files) {
+ file._note = note;
+ }
+ }
+ const files = concat(notes.map((n: any): any[] => n.files));
+ this.images = files.filter(f => image.includes(f.type)).slice(0, 9);
+ });
+
+ this.$root.api('charts/user/notes', {
+ userId: this.user.id,
+ span: 'day',
+ limit: 21
+ }).then(stats => {
+ const normal = [];
+ const reply = [];
+ const renote = [];
+
+ const now = new Date();
+ const y = now.getFullYear();
+ const m = now.getMonth();
+ const d = now.getDate();
+
+ for (let i = 0; i < 21; i++) {
+ const x = new Date(y, m, d - i);
+ normal.push([
+ x,
+ stats.diffs.normal[i]
+ ]);
+ reply.push([
+ x,
+ stats.diffs.reply[i]
+ ]);
+ renote.push([
+ x,
+ stats.diffs.renote[i]
+ ]);
+ }
+
+ const chart = new ApexCharts(this.$refs.chart, {
+ chart: {
+ type: 'bar',
+ stacked: true,
+ height: 100,
+ sparkline: {
+ enabled: true
+ },
+ },
+ plotOptions: {
+ bar: {
+ columnWidth: '90%'
+ }
+ },
+ grid: {
+ clipMarkers: false,
+ padding: {
+ top: 16,
+ right: 16,
+ bottom: 16,
+ left: 16
+ }
+ },
+ tooltip: {
+ shared: true,
+ intersect: false
+ },
+ series: [{
+ name: 'Normal',
+ data: normal
+ }, {
+ name: 'Reply',
+ data: reply
+ }, {
+ name: 'Renote',
+ data: renote
+ }],
+ xaxis: {
+ type: 'datetime',
+ crosshairs: {
+ width: 1,
+ opacity: 1
+ }
+ }
+ });
+
+ chart.render();
+ });
+ },
+
+ initTl() {
+ return new Promise((res, rej) => {
+ this.$root.api('users/notes', {
+ userId: this.user.id,
+ limit: fetchLimit + 1,
+ untilDate: new Date().getTime() + 1000 * 86400 * 365,
+ withFiles: this.withFiles,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ }, rej);
+ });
+ },
+
+ fetchMoreNotes() {
+ this.moreFetching = true;
+
+ const promise = this.$root.api('users/notes', {
+ userId: this.user.id,
+ limit: fetchLimit + 1,
+ untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime(),
+ withFiles: this.withFiles,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ });
+
+ promise.then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ for (const n of notes) (this.$refs.timeline as any).append(n);
+ this.moreFetching = false;
+ });
+
+ return promise;
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.sainvnaq
+ display grid
+ grid-template-columns 1fr 1fr 1fr
+ gap 8px
+ padding 16px
+
+ > *
+ height 70px
+ background-position center center
+ background-size cover
+ background-clip content-box
+ border-radius 4px
+
+</style>
diff --git a/src/client/app/desktop/views/deck/deck.user-column.vue b/src/client/app/desktop/views/deck/deck.user-column.vue
index 16a7aa5b35..d6618c5716 100644
--- a/src/client/app/desktop/views/deck/deck.user-column.vue
+++ b/src/client/app/desktop/views/deck/deck.user-column.vue
@@ -39,8 +39,10 @@
</div>
<div class="counts">
<div>
- <b>{{ user.notesCount | number }}</b>
- <span>{{ $t('posts') }}</span>
+ <router-link :to="user | userPage()">
+ <b>{{ user.notesCount | number }}</b>
+ <span>{{ $t('posts') }}</span>
+ </router-link>
</div>
<div>
<router-link :to="user | userPage('following')">
@@ -56,35 +58,7 @@
</div>
</div>
</div>
- <ui-container v-if="user.pinnedNotes && user.pinnedNotes.length > 0" :body-togglable="true">
- <span slot="header"><fa icon="thumbtack"/> {{ $t('pinned-notes') }}</span>
- <div>
- <x-note v-for="n in user.pinnedNotes" :key="n.id" :note="n" :mini="true"/>
- </div>
- </ui-container>
- <ui-container v-if="images.length > 0" :body-togglable="true">
- <span slot="header"><fa :icon="['far', 'images']"/> {{ $t('images') }}</span>
- <div class="sainvnaq">
- <router-link v-for="image in images"
- :style="`background-image: url(${image.thumbnailUrl})`"
- :key="`${image.id}:${image._note.id}`"
- :to="image._note | notePage"
- :title="`${image.name}\n${(new Date(image.createdAt)).toLocaleString()}`"
- ></router-link>
- </div>
- </ui-container>
- <ui-container :body-togglable="true">
- <span slot="header"><fa :icon="['far', 'chart-bar']"/> {{ $t('activity') }}</span>
- <div>
- <div ref="chart"></div>
- </div>
- </ui-container>
- <ui-container>
- <span slot="header"><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</span>
- <div>
- <x-notes ref="timeline" :more="existMore ? fetchMoreNotes : null"/>
- </div>
- </ui-container>
+ <router-view :user="user"></router-view>
</div>
</x-column>
</template>
@@ -94,30 +68,18 @@ import Vue from 'vue';
import i18n from '../../../i18n';
import parseAcct from '../../../../../misc/acct/parse';
import XColumn from './deck.column.vue';
-import XNotes from './deck.notes.vue';
-import XNote from '../components/note.vue';
import XUserMenu from '../../../common/views/components/user-menu.vue';
-import { concat } from '../../../../../prelude/array';
-import ApexCharts from 'apexcharts';
-
-const fetchLimit = 10;
export default Vue.extend({
i18n: i18n('deck/deck.user-column.vue'),
components: {
XColumn,
- XNotes,
- XNote
},
data() {
return {
user: null,
fetching: true,
- existMore: false,
- moreFetching: false,
- withFiles: false,
- images: [],
};
},
@@ -146,160 +108,9 @@ export default Vue.extend({
this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
this.user = user;
this.fetching = false;
-
- this.$nextTick(() => {
- (this.$refs.timeline as any).init(() => this.initTl());
- });
-
- const image = [
- 'image/jpeg',
- 'image/png',
- 'image/gif'
- ];
-
- this.$root.api('users/notes', {
- userId: this.user.id,
- fileType: image,
- excludeNsfw: !this.$store.state.device.alwaysShowNsfw,
- limit: 9,
- untilDate: new Date().getTime() + 1000 * 86400 * 365
- }).then(notes => {
- for (const note of notes) {
- for (const file of note.files) {
- file._note = note;
- }
- }
- const files = concat(notes.map((n: any): any[] => n.files));
- this.images = files.filter(f => image.includes(f.type)).slice(0, 9);
- });
-
- this.$root.api('charts/user/notes', {
- userId: this.user.id,
- span: 'day',
- limit: 21
- }).then(stats => {
- const normal = [];
- const reply = [];
- const renote = [];
-
- const now = new Date();
- const y = now.getFullYear();
- const m = now.getMonth();
- const d = now.getDate();
-
- for (let i = 0; i < 21; i++) {
- const x = new Date(y, m, d - i);
- normal.push([
- x,
- stats.diffs.normal[i]
- ]);
- reply.push([
- x,
- stats.diffs.reply[i]
- ]);
- renote.push([
- x,
- stats.diffs.renote[i]
- ]);
- }
-
- const chart = new ApexCharts(this.$refs.chart, {
- chart: {
- type: 'bar',
- stacked: true,
- height: 100,
- sparkline: {
- enabled: true
- },
- },
- plotOptions: {
- bar: {
- columnWidth: '90%'
- }
- },
- grid: {
- clipMarkers: false,
- padding: {
- top: 16,
- right: 16,
- bottom: 16,
- left: 16
- }
- },
- tooltip: {
- shared: true,
- intersect: false
- },
- series: [{
- name: 'Normal',
- data: normal
- }, {
- name: 'Reply',
- data: reply
- }, {
- name: 'Renote',
- data: renote
- }],
- xaxis: {
- type: 'datetime',
- crosshairs: {
- width: 1,
- opacity: 1
- }
- }
- });
-
- chart.render();
- });
});
},
- initTl() {
- return new Promise((res, rej) => {
- this.$root.api('users/notes', {
- userId: this.user.id,
- limit: fetchLimit + 1,
- untilDate: new Date().getTime() + 1000 * 86400 * 365,
- withFiles: this.withFiles,
- includeMyRenotes: this.$store.state.settings.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
- }).then(notes => {
- if (notes.length == fetchLimit + 1) {
- notes.pop();
- this.existMore = true;
- }
- res(notes);
- }, rej);
- });
- },
-
- fetchMoreNotes() {
- this.moreFetching = true;
-
- const promise = this.$root.api('users/notes', {
- userId: this.user.id,
- limit: fetchLimit + 1,
- untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime(),
- withFiles: this.withFiles,
- includeMyRenotes: this.$store.state.settings.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
- });
-
- promise.then(notes => {
- if (notes.length == fetchLimit + 1) {
- notes.pop();
- } else {
- this.existMore = false;
- }
- for (const n of notes) (this.$refs.timeline as any).append(n);
- this.moreFetching = false;
- });
-
- return promise;
- },
-
menu() {
this.$root.new(XUserMenu, {
source: this.$refs.menu,
@@ -439,34 +250,13 @@ export default Vue.extend({
> a
color var(--text)
- >>> b
- display block
- font-size 110%
-
- >>> span
- display block
- font-size 80%
- opacity 0.7
-
- .sainvnaq
- display grid
- grid-template-columns 1fr 1fr 1fr
- gap 8px
- padding 16px
-
- > *
- height 70px
- background-position center center
- background-size cover
- background-clip content-box
- border-radius 4px
-
- > .activity
- > div
- background var(--face)
+ > b
+ display block
+ font-size 110%
- > .tl
- > div
- background var(--face)
+ > span
+ display block
+ font-size 80%
+ opacity 0.7
</style>
diff --git a/src/client/app/desktop/views/home/user/user.vue b/src/client/app/desktop/views/home/user/index.vue
index 6a827f4beb..24abeadd6a 100644
--- a/src/client/app/desktop/views/home/user/user.vue
+++ b/src/client/app/desktop/views/home/user/index.vue
@@ -1,22 +1,10 @@
<template>
-<div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching">
+<div class="omechnps" v-if="!fetching">
<div class="is-suspended" v-if="user.isSuspended"><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</div>
<div class="is-remote" v-if="user.host != null"><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a></div>
<div class="main">
- <x-header :user="user"/>
- <x-integrations :user="user" v-if="user.twitter || user.github || user.discord"/>
- <mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
- <!--<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>-->
- <div class="activity">
- <ui-container :body-togglable="true">
- <template slot="header"><fa icon="chart-bar"/>{{ $t('activity') }}</template>
- <x-activity :user="user" :limit="35" style="padding: 16px;"/>
- </ui-container>
- </div>
- <x-photos :user="user"/>
- <x-friends :user="user"/>
- <x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
- <x-timeline class="timeline" ref="tl" :user="user"/>
+ <x-header class="header" :user="user"/>
+ <router-view :user="user"></router-view>
</div>
</div>
</template>
@@ -27,23 +15,11 @@ import i18n from '../../../../i18n';
import parseAcct from '../../../../../../misc/acct/parse';
import Progress from '../../../../common/scripts/loading';
import XHeader from './user.header.vue';
-import XTimeline from './user.timeline.vue';
-import XPhotos from './user.photos.vue';
-import XFollowersYouKnow from './user.followers-you-know.vue';
-import XFriends from './user.friends.vue';
-import XIntegrations from './user.integrations.vue';
-import XActivity from '../../../../common/views/components/activity.vue';
export default Vue.extend({
i18n: i18n(),
components: {
- XHeader,
- XTimeline,
- XPhotos,
- XFollowersYouKnow,
- XFriends,
- XIntegrations,
- XActivity
+ XHeader
},
data() {
return {
@@ -76,7 +52,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.xygkxeaeontfaokvqmiblezmhvhostak
+.omechnps
width 100%
margin 0 auto
@@ -100,10 +76,7 @@ export default Vue.extend({
font-weight bold
> .main
- > *
+ > .header
margin-bottom 16px
- > .timeline
- box-shadow var(--shadow)
-
</style>
diff --git a/src/client/app/desktop/views/home/user/user.header.vue b/src/client/app/desktop/views/home/user/user.header.vue
index 05d3674996..debfb24393 100644
--- a/src/client/app/desktop/views/home/user/user.header.vue
+++ b/src/client/app/desktop/views/home/user/user.header.vue
@@ -40,7 +40,7 @@
<span class="birthday" v-if="user.host === null && user.profile.birthday"><fa icon="birthday-cake"/> {{ user.profile.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span>
</div>
<div class="status">
- <span class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</span>
+ <router-link :to="user | userPage()" class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</router-link>
<router-link :to="user | userPage('following')" class="following clickable"><b>{{ user.followingCount | number }}</b>{{ $t('following') }}</router-link>
<router-link :to="user | userPage('followers')" class="followers clickable"><b>{{ user.followersCount | number }}</b>{{ $t('followers') }}</router-link>
</div>
diff --git a/src/client/app/desktop/views/home/user/user.home.vue b/src/client/app/desktop/views/home/user/user.home.vue
new file mode 100644
index 0000000000..b4426ac755
--- /dev/null
+++ b/src/client/app/desktop/views/home/user/user.home.vue
@@ -0,0 +1,63 @@
+<template>
+<div class="lnctpgve">
+ <x-integrations :user="user" v-if="user.twitter || user.github || user.discord"/>
+ <mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
+ <!--<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>-->
+ <div class="activity">
+ <ui-container :body-togglable="true">
+ <template slot="header"><fa icon="chart-bar"/>{{ $t('activity') }}</template>
+ <x-activity :user="user" :limit="35" style="padding: 16px;"/>
+ </ui-container>
+ </div>
+ <x-photos :user="user"/>
+ <x-friends :user="user"/>
+ <x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
+ <x-timeline class="timeline" ref="tl" :user="user"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../../../../i18n';
+import parseAcct from '../../../../../../misc/acct/parse';
+import Progress from '../../../../common/scripts/loading';
+import XTimeline from './user.timeline.vue';
+import XPhotos from './user.photos.vue';
+import XFollowersYouKnow from './user.followers-you-know.vue';
+import XFriends from './user.friends.vue';
+import XIntegrations from './user.integrations.vue';
+import XActivity from '../../../../common/views/components/activity.vue';
+
+export default Vue.extend({
+ i18n: i18n(),
+ components: {
+ XTimeline,
+ XPhotos,
+ XFollowersYouKnow,
+ XFriends,
+ XIntegrations,
+ XActivity
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true
+ }
+ },
+ methods: {
+ warp(date) {
+ (this.$refs.tl as any).warp(date);
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.lnctpgve
+ > *
+ margin-bottom 16px
+
+ > .timeline
+ box-shadow var(--shadow)
+
+</style>
diff --git a/src/client/app/desktop/views/pages/user-following-or-followers.vue b/src/client/app/desktop/views/pages/user-following-or-followers.vue
deleted file mode 100644
index fd842cbcd4..0000000000
--- a/src/client/app/desktop/views/pages/user-following-or-followers.vue
+++ /dev/null
@@ -1,120 +0,0 @@
-<template>
-<mk-ui>
- <div class="yyyocnobkvdlnyapyauyopbskldsnipz" v-if="!fetching">
- <header>
- <mk-avatar class="avatar" :user="user"/>
- <i18n :path="isFollowing ? 'following' : 'followers'" tag="p">
- <router-link :to="user | userPage" place="user">
- <mk-user-name :user="user"/>
- </router-link>
- </i18n>
- </header>
- <div class="users">
- <mk-user-card v-for="user in users" :user="user" :key="user.id"/>
- </div>
- <div class="more" v-if="next">
- <ui-button inline @click="fetchMore">{{ $t('@.load-more') }}</ui-button>
- </div>
- </div>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import parseAcct from '../../../../../misc/acct/parse';
-import Progress from '../../../common/scripts/loading';
-
-const limit = 16;
-
-export default Vue.extend({
- i18n: i18n('desktop/views/pages/user-following-or-followers.vue'),
-
- data() {
- return {
- fetching: true,
- user: null,
- users: [],
- next: undefined
- };
- },
- computed: {
- isFollowing(): boolean {
- return this.$route.name == 'userFollowing';
- },
- endpoint(): string {
- return this.isFollowing ? 'users/following' : 'users/followers';
- }
- },
- watch: {
- $route: 'fetch'
- },
- created() {
- this.fetch();
- },
- methods: {
- fetch() {
- this.fetching = true;
- Progress.start();
- this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
- this.user = user;
- this.$root.api(this.endpoint, {
- userId: this.user.id,
- iknow: false,
- limit: limit
- }).then(x => {
- this.users = x.users;
- this.next = x.next;
- this.fetching = false;
- Progress.done();
- });
- });
- },
-
- fetchMore() {
- this.$root.api(this.endpoint, {
- userId: this.user.id,
- iknow: false,
- limit: limit,
- cursor: this.next
- }).then(x => {
- this.users = this.users.concat(x.users);
- this.next = x.next;
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-.yyyocnobkvdlnyapyauyopbskldsnipz
- width 100%
- max-width 1280px
- padding 32px
- margin 0 auto
-
- > header
- display flex
- align-items center
- margin 0 0 16px 0
- color var(--text)
-
- > .avatar
- width 64px
- height 64px
-
- > p
- margin 0 16px
- font-size 24px
- font-weight bold
-
- > .users
- display grid
- grid-template-columns 1fr 1fr 1fr 1fr
- gap 16px
-
- > .more
- margin 32px 16px 16px 16px
- text-align center
-
-</style>
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 1feff3d5eb..ad37ba70ab 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -21,8 +21,6 @@ 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 MkFollowers from './views/pages/followers.vue';
-import MkFollowing from './views/pages/following.vue';
import MkFavorites from './views/pages/favorites.vue';
import MkUserLists from './views/pages/user-lists.vue';
import MkUserList from './views/pages/user-list.vue';
@@ -137,9 +135,11 @@ init((launch) => {
{ path: '/explore', name: 'explore', component: () => import('./views/pages/explore.vue').then(m => m.default) },
{ path: '/share', component: MkShare },
{ path: '/games/reversi/:game?', name: 'reversi', component: MkReversi },
- { path: '/@:user', component: () => import('./views/pages/user.vue').then(m => m.default) },
- { path: '/@:user/followers', component: MkFollowers },
- { path: '/@:user/following', component: MkFollowing },
+ { path: '/@:user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [
+ { path: '', name: 'user', component: () => import('./views/pages/user/home.vue').then(m => m.default) },
+ { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
+ { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
+ ]},
{ path: '/notes/:note', component: MkNote },
{ path: '/authorize-follow', component: MkFollow },
{ path: '*', component: MkNotFound }
diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts
index 94bc8d23fd..864098640b 100644
--- a/src/client/app/mobile/views/components/index.ts
+++ b/src/client/app/mobile/views/components/index.ts
@@ -13,7 +13,6 @@ import friendsMaker from './friends-maker.vue';
import notification from './notification.vue';
import notifications from './notifications.vue';
import notificationPreview from './notification-preview.vue';
-import usersList from './users-list.vue';
import userPreview from './user-preview.vue';
import userTimeline from './user-timeline.vue';
import userListTimeline from './user-list-timeline.vue';
@@ -33,7 +32,6 @@ Vue.component('mk-friends-maker', friendsMaker);
Vue.component('mk-notification', notification);
Vue.component('mk-notifications', notifications);
Vue.component('mk-notification-preview', notificationPreview);
-Vue.component('mk-users-list', usersList);
Vue.component('mk-user-preview', userPreview);
Vue.component('mk-user-timeline', userTimeline);
Vue.component('mk-user-list-timeline', userListTimeline);
diff --git a/src/client/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue
deleted file mode 100644
index 0c5c934dcf..0000000000
--- a/src/client/app/mobile/views/components/users-list.vue
+++ /dev/null
@@ -1,135 +0,0 @@
-<template>
-<div class="mk-users-list">
- <nav>
- <span :data-active="mode == 'all'" @click="mode = 'all'">{{ $t('all') }}<span>{{ count }}</span></span>
- <span v-if="$store.getters.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">{{ $t('known') }}<span>{{ youKnowCount }}</span></span>
- </nav>
- <div class="users" v-if="!fetching && users.length != 0">
- <mk-user-preview v-for="u in users" :user="u" :key="u.id"/>
- </div>
- <ui-button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching">
- <span v-if="!moreFetching">{{ $t('@.load-more') }}</span>
- <span v-if="moreFetching">{{ $t('@.loading') }}<mk-ellipsis/></span>
- </ui-button>
- <p class="no" v-if="!fetching && users.length == 0">
- <slot></slot>
- </p>
- <p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-export default Vue.extend({
- i18n: i18n('mobile/views/components/users-list.vue'),
- props: ['fetch', 'count', 'youKnowCount'],
- data() {
- return {
- limit: 30,
- mode: 'all',
- fetching: true,
- moreFetching: false,
- users: [],
- next: null
- };
- },
- watch: {
- mode() {
- this._fetch();
- }
- },
- mounted() {
- this._fetch(() => {
- this.$emit('loaded');
- });
- },
- methods: {
- _fetch(cb?) {
- this.fetching = true;
- this.fetch(this.mode == 'iknow', this.limit, null, obj => {
- this.users = obj.users;
- this.next = obj.next;
- this.fetching = false;
- if (cb) cb();
- });
- },
- more() {
- this.moreFetching = true;
- this.fetch(this.mode == 'iknow', this.limit, this.next, obj => {
- this.moreFetching = false;
- this.users = this.users.concat(obj.users);
- this.next = obj.next;
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-
-
-.mk-users-list
-
- > nav
- display flex
- justify-content center
- margin 0 auto
- max-width 600px
- border-bottom solid 1px rgba(#000, 0.2)
-
- > span
- display block
- flex 1 1
- text-align center
- line-height 52px
- font-size 14px
- color #657786
- border-bottom solid 2px transparent
-
- &[data-active]
- font-weight bold
- color var(--primary)
- border-color var(--primary)
-
- > span
- display inline-block
- margin-left 4px
- padding 2px 5px
- font-size 12px
- line-height 1
- color #fff
- background rgba(#000, 0.3)
- border-radius 20px
-
- > .users
- margin 8px auto
- max-width 500px
- width calc(100% - 16px)
- background #fff
- border-radius 8px
- box-shadow 0 0 0 1px rgba(#000, 0.2)
-
- @media (min-width 500px)
- margin 16px auto
- width calc(100% - 32px)
-
- > *
- border-bottom solid 1px rgba(#000, 0.05)
-
- > .no
- margin 0
- padding 16px
- text-align center
- color var(--text)
-
- > .fetching
- margin 0
- padding 16px
- text-align center
- color var(--text)
-
- > [data-icon]
- margin-right 4px
-
-</style>
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
deleted file mode 100644
index f5ac8ef195..0000000000
--- a/src/client/app/mobile/views/pages/followers.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-<mk-ui>
- <template slot="header" v-if="!fetching">
- <img :src="user.avatarUrl" alt="">
- <mfm :text="$t('followers-of', { name })" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/>
- </template>
- <mk-users-list
- v-if="!fetching"
- :fetch="fetchUsers"
- :count="user.followersCount"
- :you-know-count="user.followersYouKnowCount"
- @loaded="onLoaded"
- >
- %i18n:@no-users%
- </mk-users-list>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../misc/acct/parse';
-import getUserName from '../../../../../misc/get-user-name';
-
-export default Vue.extend({
- i18n: i18n('mobile/views/pages/followers.vue'),
- data() {
- return {
- fetching: true,
- user: null
- };
- },
- computed: {
- name() {
- return getUserName(this.user);
- }
- },
- watch: {
- $route: 'fetch'
- },
- created() {
- this.fetch();
- },
- methods: {
- fetch() {
- Progress.start();
- this.fetching = true;
-
- this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
- this.user = user;
- this.fetching = false;
-
- document.title = `${this.$t('followers-of').replace('{}', this.name)} | ${this.$root.instanceName}`;
- });
- },
- onLoaded() {
- Progress.done();
- },
- fetchUsers(iknow, limit, cursor, cb) {
- this.$root.api('users/followers', {
- userId: this.user.id,
- iknow: iknow,
- limit: limit,
- cursor: cursor ? cursor : undefined
- }).then(cb);
- }
- }
-});
-</script>
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
deleted file mode 100644
index d603532498..0000000000
--- a/src/client/app/mobile/views/pages/following.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<template>
-<mk-ui>
- <template slot="header" v-if="!fetching">
- <img :src="user.avatarUrl" alt="">
- <mfm :text="$t('following-of', { name })" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/>
- </template>
- <mk-users-list
- v-if="!fetching"
- :fetch="fetchUsers"
- :count="user.followingCount"
- :you-know-count="user.followingYouKnowCount"
- @loaded="onLoaded"
- >
- %i18n:@no-users%
- </mk-users-list>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../misc/acct/parse';
-
-export default Vue.extend({
- i18n: i18n('mobile/views/pages/following.vue'),
- data() {
- return {
- fetching: true,
- user: null
- };
- },
- computed: {
- name(): string {
- return Vue.filter('userName')(this.user);
- }
- },
- watch: {
- $route: 'fetch'
- },
- created() {
- this.fetch();
- },
- methods: {
- fetch() {
- Progress.start();
- this.fetching = true;
-
- this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
- this.user = user;
- this.fetching = false;
-
- document.title = `${this.$t('followers-of').replace('{}', this.name)} | ${this.$root.instanceName}`;
- });
- },
- onLoaded() {
- Progress.done();
- },
- fetchUsers(iknow, limit, cursor, cb) {
- this.$root.api('users/following', {
- userId: this.user.id,
- iknow: iknow,
- limit: limit,
- cursor: cursor ? cursor : undefined
- }).then(cb);
- }
- }
-});
-</script>
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user/index.vue
index 5d15a9718a..48b65624ef 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user/index.vue
@@ -43,22 +43,22 @@
</p>
</div>
<div class="status">
- <a>
+ <router-link :to="user | userPage()">
<b>{{ user.notesCount | number }}</b>
<i>{{ $t('notes') }}</i>
- </a>
- <a :href="user | userPage('following')">
+ </router-link>
+ <router-link :to="user | userPage('following')">
<b>{{ user.followingCount | number }}</b>
<i>{{ $t('following') }}</i>
- </a>
- <a :href="user | userPage('followers')">
+ </router-link>
+ <router-link :to="user | userPage('followers')">
<b>{{ user.followersCount | number }}</b>
<i>{{ $t('followers') }}</i>
- </a>
+ </router-link>
</div>
</div>
</header>
- <nav>
+ <nav v-if="$route.name == 'user'">
<div class="nav-container">
<a :data-active="page == 'home'" @click="page = 'home'"><fa icon="home"/> {{ $t('overview') }}</a>
<a :data-active="page == 'notes'" @click="page = 'notes'"><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</a>
@@ -66,9 +66,12 @@
</div>
</nav>
<div class="body">
- <x-home v-if="page == 'home'" :user="user"/>
- <mk-user-timeline v-if="page == 'notes'" :user="user" key="tl"/>
- <mk-user-timeline v-if="page == 'media'" :user="user" :with-media="true" key="media"/>
+ <template v-if="$route.name == 'user'">
+ <x-home v-if="page == 'home'" :user="user"/>
+ <mk-user-timeline v-if="page == 'notes'" :user="user" key="tl"/>
+ <mk-user-timeline v-if="page == 'media'" :user="user" :with-media="true" key="media"/>
+ </template>
+ <router-view :user="user"></router-view>
</div>
</main>
</mk-ui>
@@ -76,13 +79,13 @@
<script lang="ts">
import Vue from 'vue';
-import i18n from '../../../i18n';
+import i18n from '../../../../i18n';
import * as age from 's-age';
-import parseAcct from '../../../../../misc/acct/parse';
-import Progress from '../../../common/scripts/loading';
-import XUserMenu from '../../../common/views/components/user-menu.vue';
-import XHome from './user/home.vue';
-import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url';
+import parseAcct from '../../../../../../misc/acct/parse';
+import Progress from '../../../../common/scripts/loading';
+import XUserMenu from '../../../../common/views/components/user-menu.vue';
+import XHome from './home.vue';
+import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url';
export default Vue.extend({
i18n: i18n('mobile/views/pages/user.vue'),
@@ -93,7 +96,7 @@ export default Vue.extend({
return {
fetching: true,
user: null,
- page: 'home'
+ page: this.$route.name == 'user' ? 'home' : null
};
},
computed: {
diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts
index 2a39da4064..2f7f1af6a5 100644
--- a/src/server/api/endpoints/users/followers.ts
+++ b/src/server/api/endpoints/users/followers.ts
@@ -16,7 +16,7 @@ export const meta = {
params: {
userId: {
- validator: $.type(ID),
+ validator: $.optional.type(ID),
transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
@@ -24,6 +24,14 @@ export const meta = {
}
},
+ username: {
+ validator: $.optional.str
+ },
+
+ host: {
+ validator: $.optional.nullable.str
+ },
+
limit: {
validator: $.optional.num.range(1, 100),
default: 10
@@ -43,14 +51,11 @@ export const meta = {
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
- // Lookup user
- const user = await User.findOne({
- _id: ps.userId
- }, {
- fields: {
- _id: true
- }
- });
+ const q: any = ps.userId != null
+ ? { _id: ps.userId }
+ : { usernameLower: ps.username.toLowerCase(), host: ps.host };
+
+ const user = await User.findOne(q);
if (user === null) {
return rej('user not found');
diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
index 4ccc13f633..1485a63f24 100644
--- a/src/server/api/endpoints/users/following.ts
+++ b/src/server/api/endpoints/users/following.ts
@@ -16,7 +16,7 @@ export const meta = {
params: {
userId: {
- validator: $.type(ID),
+ validator: $.optional.type(ID),
transform: transform,
desc: {
'ja-JP': '対象のユーザーのID',
@@ -24,6 +24,14 @@ export const meta = {
}
},
+ username: {
+ validator: $.optional.str
+ },
+
+ host: {
+ validator: $.optional.nullable.str
+ },
+
limit: {
validator: $.optional.num.range(1, 100),
default: 10
@@ -43,14 +51,11 @@ export const meta = {
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
- // Lookup user
- const user = await User.findOne({
- _id: ps.userId
- }, {
- fields: {
- _id: true
- }
- });
+ const q: any = ps.userId != null
+ ? { _id: ps.userId }
+ : { usernameLower: ps.username.toLowerCase(), host: ps.host };
+
+ const user = await User.findOne(q);
if (user === null) {
return rej('user not found');