diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 572c8ccdfe..66a9a83353 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -810,6 +810,10 @@ desktop/views/components/renote-form.vue: desktop/views/components/renote-form-window.vue: title: "この投稿をRenoteしますか?" +desktop/views/pages/user-following-or-followers.vue: + following: "{user}のフォロー" + followers: "{user}のフォロワー" + desktop/views/components/settings-window.vue: settings: "設定" diff --git a/package.json b/package.json index ea00e41178..0ff686c2fb 100644 --- a/package.json +++ b/package.json @@ -215,6 +215,7 @@ "vue-color": "2.7.0", "vue-content-loading": "1.5.3", "vue-cropperjs": "2.2.2", + "vue-i18n": "8.3.0", "vue-js-modal": "1.3.26", "vue-loader": "15.4.2", "vue-router": "3.0.1", diff --git a/src/client/app/common/views/components/ui/button.vue b/src/client/app/common/views/components/ui/button.vue index 71496da5cd..132518da92 100644 --- a/src/client/app/common/views/components/ui/button.vue +++ b/src/client/app/common/views/components/ui/button.vue @@ -53,7 +53,7 @@ export default Vue.extend({ display block width 100% margin 0 - padding 8px + padding 8px 10px text-align center font-weight normal font-size 16px diff --git a/src/client/app/config.ts b/src/client/app/config.ts index 2abc3f7226..637d643d8d 100644 --- a/src/client/app/config.ts +++ b/src/client/app/config.ts @@ -1,5 +1,6 @@ declare const _LANG_: string; declare const _LANGS_: string; +declare const _LOCALE_: { [key: string]: any }; declare const _THEME_COLOR_: string; declare const _COPYRIGHT_: string; declare const _VERSION_: string; @@ -16,6 +17,7 @@ export const apiUrl = url + '/api'; export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming'; export const lang = _LANG_; export const langs = _LANGS_; +export const locale = _LOCALE_; export const themeColor = _THEME_COLOR_; export const copyright = _COPYRIGHT_; export const version = _VERSION_; diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index eabb1fe163..ca9771e2fe 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -24,6 +24,7 @@ import MkIndex from './views/pages/index.vue'; import MkHome from './views/pages/home.vue'; import MkDeck from './views/pages/deck/deck.vue'; import MkUser from './views/pages/user/user.vue'; +import MkUserFollowingOrFollowers from './views/pages/user-following-or-followers.vue'; import MkFavorites from './views/pages/favorites.vue'; import MkSelectDrive from './views/pages/selectdrive.vue'; import MkDrive from './views/pages/drive.vue'; @@ -66,6 +67,8 @@ init(async (launch) => { { path: '/share', component: MkShare }, { path: '/reversi/:game?', component: MkReversi }, { path: '/@:user', name: 'user', component: MkUser }, + { path: '/@:user/following', name: 'userFollowing', component: MkUserFollowingOrFollowers }, + { path: '/@:user/followers', name: 'userFollowers', component: MkUserFollowingOrFollowers }, { path: '/notes/:note', name: 'note', component: MkNote }, { path: '/authorize-follow', component: MkFollow } ] diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue deleted file mode 100644 index d5214adb2f..0000000000 --- a/src/client/app/desktop/views/components/followers-window.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> -<mk-window width="400px" height="550px" @closed="destroyDom"> - <span slot="header" :class="$style.header"> - <img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }} - </span> - <mk-followers :user="user"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['user'], - computed: { - name(): string { - return Vue.filter('userName')(this.user); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > img - display inline-block - vertical-align bottom - height calc(100% - 10px) - margin 5px - border-radius 4px - -</style> diff --git a/src/client/app/desktop/views/components/followers.vue b/src/client/app/desktop/views/components/followers.vue deleted file mode 100644 index 1ef9f69771..0000000000 --- a/src/client/app/desktop/views/components/followers.vue +++ /dev/null @@ -1,26 +0,0 @@ -<template> -<mk-users-list - :fetch="fetch" - :count="user.followersCount" - :you-know-count="user.followersYouKnowCount" -> - %i18n:@empty% -</mk-users-list> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['user'], - methods: { - fetch(iknow, limit, cursor, cb) { - (this as any).api('users/followers', { - userId: this.user.id, - iknow: iknow, - limit: limit, - cursor: cursor ? cursor : undefined - }).then(cb); - } - } -}); -</script> diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue deleted file mode 100644 index aa9f2bde7b..0000000000 --- a/src/client/app/desktop/views/components/following-window.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> -<mk-window width="400px" height="550px" @closed="destroyDom"> - <span slot="header" :class="$style.header"> - <img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }} - </span> - <mk-following :user="user"/> -</mk-window> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['user'], - computed: { - name(): string { - return Vue.filter('userName')(this.user); - } - } -}); -</script> - -<style lang="stylus" module> -.header - > img - display inline-block - vertical-align bottom - height calc(100% - 10px) - margin 5px - border-radius 4px - -</style> diff --git a/src/client/app/desktop/views/components/following.vue b/src/client/app/desktop/views/components/following.vue deleted file mode 100644 index d55ce1c0d4..0000000000 --- a/src/client/app/desktop/views/components/following.vue +++ /dev/null @@ -1,26 +0,0 @@ -<template> -<mk-users-list - :fetch="fetch" - :count="user.followingCount" - :you-know-count="user.followingYouKnowCount" -> - %i18n:@empty% -</mk-users-list> -</template> - -<script lang="ts"> -import Vue from 'vue'; -export default Vue.extend({ - props: ['user'], - methods: { - fetch(iknow, limit, cursor, cb) { - (this as any).api('users/following', { - userId: this.user.id, - iknow: iknow, - limit: limit, - cursor: cursor ? cursor : undefined - }).then(cb); - } - } -}); -</script> diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts index ff4e845f62..2478f75ac4 100644 --- a/src/client/app/desktop/views/components/index.ts +++ b/src/client/app/desktop/views/components/index.ts @@ -22,9 +22,7 @@ import settings from './settings.vue'; import calendar from './calendar.vue'; import activity from './activity.vue'; import friendsMaker from './friends-maker.vue'; -import followers from './followers.vue'; -import following from './following.vue'; -import usersList from './users-list.vue'; +import userCard from './user-card.vue'; import userListTimeline from './user-list-timeline.vue'; import widgetContainer from './widget-container.vue'; @@ -50,8 +48,6 @@ Vue.component('mk-settings', settings); Vue.component('mk-calendar', calendar); Vue.component('mk-activity', activity); Vue.component('mk-friends-maker', friendsMaker); -Vue.component('mk-followers', followers); -Vue.component('mk-following', following); -Vue.component('mk-users-list', usersList); +Vue.component('mk-user-card', userCard); Vue.component('mk-user-list-timeline', userListTimeline); Vue.component('mk-widget-container', widgetContainer); diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/user-card.vue similarity index 77% rename from src/client/app/desktop/views/components/users-list.item.vue rename to src/client/app/desktop/views/components/user-card.vue index 66a002c708..ccc0a49dc0 100644 --- a/src/client/app/desktop/views/components/users-list.item.vue +++ b/src/client/app/desktop/views/components/user-card.vue @@ -9,7 +9,6 @@ <div class="description"> <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/> </div> - <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p> </div> </div> </template> @@ -34,17 +33,18 @@ export default Vue.extend({ <style lang="stylus" scoped> .zvdbznxvfixtmujpsigoccczftvpiwqh - $bg = #fff + $bg = var(--face) - margin 16px auto - max-width calc(100% - 32px) - font-size 16px + height 280px + overflow hidden + font-size 14px text-align center background $bg box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) + color var(--faceText) > .banner - height 100px + height 90px background-color #f9f4f4 background-position center background-size cover @@ -63,13 +63,10 @@ export default Vue.extend({ right 16px > .body - padding 4px 32px 32px 32px - - @media (max-width 400px) - padding 4px 16px 16px 16px + padding 0px 24px > .name - font-size 20px + font-size 120% font-weight bold > .username @@ -77,15 +74,6 @@ export default Vue.extend({ opacity 0.7 > .description - margin 16px 0 - - > .followed - margin 0 - padding 0 - line-height 24px - font-size 0.8em - color #71afc7 - background #eefaff - border-radius 4px + margin 8px 0 16px 0 </style> diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue deleted file mode 100644 index 05fe6c292e..0000000000 --- a/src/client/app/desktop/views/components/users-list.vue +++ /dev/null @@ -1,145 +0,0 @@ -<template> -<div class="mk-users-list"> - <nav> - <div> - <span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span> - <span v-if="$store.getters.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@iknow%<span>{{ youKnowCount }}</span></span> - </div> - </nav> - <div class="users" v-if="!fetching && users.length != 0"> - <div v-for="u in users" :key="u.id"> - <x-item :user="u"/> - </div> - </div> - <button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching"> - <span v-if="!moreFetching">%i18n:@load-more%</span> - <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span> - </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/>%i18n:@fetching%<mk-ellipsis/></p> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import XItem from './users-list.item.vue'; - -export default Vue.extend({ - components: { - XItem - }, - props: ['fetch', 'count', 'youKnowCount'], - data() { - return { - limit: 20, - mode: 'all', - fetching: true, - moreFetching: false, - users: [], - next: null - }; - }, - 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 - height 100% - overflow auto - background #eee - - > nav - z-index 10 - position sticky - top 0 - background #fff - box-shadow 0 1px 0 rgba(#000, 0.1) - - > div - display flex - justify-content center - margin 0 auto - max-width 600px - - > span - display block - flex 1 1 - text-align center - line-height 52px - font-size 14px - color #657786 - border-bottom solid 2px transparent - cursor pointer - - * - pointer-events none - - &[data-active] - font-weight bold - color var(--primary) - border-color var(--primary) - cursor default - - > span - display inline-block - margin-left 4px - padding 2px 5px - font-size 12px - line-height 1 - color #888 - background #eee - border-radius 20px - - > button - display block - width calc(100% - 32px) - margin 16px - padding 16px - - &:hover - background rgba(#000, 0.1) - - > .no - margin 0 - padding 16px - text-align center - color #aaa - - > .fetching - margin 0 - padding 16px - text-align center - color #aaa - - > [data-icon] - margin-right 4px - -</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 new file mode 100644 index 0000000000..db0de20b64 --- /dev/null +++ b/src/client/app/desktop/views/pages/user-following-or-followers.vue @@ -0,0 +1,126 @@ +<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">{{ user | userName }}</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">%i18n:@load-more%</ui-button> + </div> + </div> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import VueI18n from 'vue-i18n'; +import parseAcct from '../../../../../misc/acct/parse'; +import Progress from '../../../common/scripts/loading'; +import { lang, locale } from '../../../config'; + +const limit = 16; + +const i18n = new VueI18n({ + locale: lang, + messages: { + [lang]: locale['desktop/views/pages/user-following-or-followers.vue'] + } +}); + +export default Vue.extend({ + i18n, + + 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 as any).api('users/show', parseAcct(this.$route.params.user)).then(user => { + this.user = user; + (this as any).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 as any).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 16px + 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/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue index 4c30942828..6d7827d1ae 100644 --- a/src/client/app/desktop/views/pages/user/user.header.vue +++ b/src/client/app/desktop/views/pages/user/user.header.vue @@ -22,8 +22,8 @@ </div> <div class="status"> <span class="notes-count"><b>{{ user.notesCount | number }}</b>%i18n:@posts%</span> - <span class="following clickable" @click="showFollowing"><b>{{ user.followingCount | number }}</b>%i18n:@following%</span> - <span class="followers clickable" @click="showFollowers"><b>{{ user.followersCount | number }}</b>%i18n:@followers%</span> + <router-link :to="user | userPage('following')" class="following clickable" @click="showFollowing"><b>{{ user.followingCount | number }}</b>%i18n:@following%</router-link> + <router-link :to="user | userPage('followers')" class="followers clickable" @click="showFollowers"><b>{{ user.followersCount | number }}</b>%i18n:@followers%</router-link> </div> </div> </div> @@ -31,8 +31,6 @@ <script lang="ts"> import Vue from 'vue'; -import MkFollowingWindow from '../../components/following-window.vue'; -import MkFollowersWindow from '../../components/followers-window.vue'; import * as age from 's-age'; export default Vue.extend({ @@ -84,19 +82,7 @@ export default Vue.extend({ (this as any).apis.updateBanner().then(i => { this.user.bannerUrl = i.bannerUrl; }); - }, - - showFollowing() { - (this as any).os.new(MkFollowingWindow, { - user: this.user - }); - }, - - showFollowers() { - (this as any).os.new(MkFollowersWindow, { - user: this.user - }); - }, + } } }); </script> diff --git a/src/client/app/init.ts b/src/client/app/init.ts index bc5a349987..3d1560633a 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -8,6 +8,7 @@ import VueRouter from 'vue-router'; import VAnimateCss from 'v-animate-css'; import VModal from 'vue-js-modal'; import VueSweetalert2 from 'vue-sweetalert2'; +import VueI18n from 'vue-i18n'; import VueHotkey from './common/hotkey'; import App from './app.vue'; @@ -121,6 +122,7 @@ Vue.use(VAnimateCss); Vue.use(VModal); Vue.use(VueHotkey); Vue.use(VueSweetalert2); +Vue.use(VueI18n); Vue.component('fa', FontAwesomeIcon); diff --git a/webpack.config.ts b/webpack.config.ts index 03ceeaa51c..37faec0069 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -66,6 +66,7 @@ const consts = { _CODENAME_: codename, _LANG_: '%lang%', _LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]), + _LOCALE_: '%locale%', _ENV_: process.env.NODE_ENV }; @@ -100,6 +101,7 @@ const plugins = [ src = src.replace(i18nReplacer.pattern, i18nReplacer.replacement); src = src.replace('%lang%', lang); + src = src.replace('"%locale%"', JSON.stringify(locales[lang])); fs.writeFileSync(`${__dirname}/built/client/assets/${file}.${version}.${lang}.js`, src, 'utf-8'); });