From 98fe9c39ebd23bc6359960cf56e51604050f5b6c Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Mon, 9 Apr 2018 18:52:29 +0900 Subject: [PATCH] Refactor --- .../common/views/components/autocomplete.vue | 11 +- .../app/common/views/components/messaging.vue | 12 +- .../views/components/welcome-timeline.vue | 10 +- src/client/app/common/views/filters/index.ts | 2 + src/client/app/common/views/filters/note.ts | 5 + src/client/app/common/views/filters/user.ts | 15 + .../views/components/followers-window.vue | 10 +- .../views/components/following-window.vue | 10 +- .../views/components/friends-maker.vue | 10 +- .../components/messaging-room-window.vue | 6 +- .../views/components/note-detail.sub.vue | 16 +- .../desktop/views/components/note-detail.vue | 26 +- .../desktop/views/components/note-preview.vue | 16 +- .../views/components/notes.note.sub.vue | 16 +- .../desktop/views/components/notes.note.vue | 21 +- .../views/components/notifications.vue | 32 +- .../views/components/post-detail.sub.vue | 122 ++++ .../desktop/views/components/post-detail.vue | 434 +++++++++++++ .../desktop/views/components/post-preview.vue | 99 +++ .../views/components/posts.post.sub.vue | 108 ++++ .../desktop/views/components/posts.post.vue | 585 ++++++++++++++++++ .../views/components/settings.mute.vue | 8 +- .../desktop/views/components/ui.header.vue | 9 +- .../desktop/views/components/user-preview.vue | 6 +- .../views/components/users-list.item.vue | 18 +- .../pages/user/user.followers-you-know.vue | 10 +- .../desktop/views/pages/user/user.friends.vue | 6 +- .../desktop/views/pages/user/user.header.vue | 14 +- .../app/desktop/views/pages/user/user.vue | 2 +- .../app/desktop/views/pages/welcome.vue | 2 +- .../views/widgets/channel.channel.note.vue | 14 +- .../views/widgets/channel.channel.post.vue | 65 ++ .../app/desktop/views/widgets/profile.vue | 10 +- .../app/desktop/views/widgets/users.vue | 10 +- .../app/mobile/views/components/note-card.vue | 12 +- .../views/components/note-detail.sub.vue | 20 +- .../mobile/views/components/note-detail.vue | 28 +- .../mobile/views/components/note-preview.vue | 20 +- .../app/mobile/views/components/note.sub.vue | 18 +- .../app/mobile/views/components/note.vue | 21 +- .../views/components/notification-preview.vue | 18 +- .../mobile/views/components/notification.vue | 22 +- .../app/mobile/views/components/post-card.vue | 85 +++ .../views/components/post-detail.sub.vue | 103 +++ .../mobile/views/components/post-detail.vue | 444 +++++++++++++ .../mobile/views/components/post-preview.vue | 100 +++ .../app/mobile/views/components/post.vue | 523 ++++++++++++++++ .../app/mobile/views/components/ui.header.vue | 8 +- .../app/mobile/views/components/ui.nav.vue | 13 +- .../app/mobile/views/components/user-card.vue | 18 +- .../mobile/views/components/user-preview.vue | 18 +- .../app/mobile/views/pages/following.vue | 5 +- .../app/mobile/views/pages/messaging-room.vue | 10 +- .../app/mobile/views/pages/settings.vue | 5 +- src/client/app/mobile/views/pages/user.vue | 14 +- .../pages/user/home.followers-you-know.vue | 14 +- .../app/mobile/views/widgets/profile.vue | 9 +- 57 files changed, 2846 insertions(+), 422 deletions(-) create mode 100644 src/client/app/common/views/filters/note.ts create mode 100644 src/client/app/common/views/filters/user.ts create mode 100644 src/client/app/desktop/views/components/post-detail.sub.vue create mode 100644 src/client/app/desktop/views/components/post-detail.vue create mode 100644 src/client/app/desktop/views/components/post-preview.vue create mode 100644 src/client/app/desktop/views/components/posts.post.sub.vue create mode 100644 src/client/app/desktop/views/components/posts.post.vue create mode 100644 src/client/app/desktop/views/widgets/channel.channel.post.vue create mode 100644 src/client/app/mobile/views/components/post-card.vue create mode 100644 src/client/app/mobile/views/components/post-detail.sub.vue create mode 100644 src/client/app/mobile/views/components/post-detail.vue create mode 100644 src/client/app/mobile/views/components/post-preview.vue create mode 100644 src/client/app/mobile/views/components/post.vue diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue index 8837fde6b..5c8f61a2a 100644 --- a/src/client/app/common/views/components/autocomplete.vue +++ b/src/client/app/common/views/components/autocomplete.vue @@ -3,8 +3,8 @@ <ol class="users" ref="suggests" v-if="users.length > 0"> <li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1"> <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/> - <span class="name">{{ getUserName(user) }}</span> - <span class="username">@{{ getAcct(user) }}</span> + <span class="name">{{ user | userName }}</span> + <span class="username">@{{ user | acct }}</span> </li> </ol> <ol class="emojis" ref="suggests" v-if="emojis.length > 0"> @@ -21,17 +21,17 @@ import Vue from 'vue'; import * as emojilib from 'emojilib'; import contains from '../../../common/scripts/contains'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; const lib = Object.entries(emojilib.lib).filter((x: any) => { return x[1].category != 'flags'; }); + const emjdb = lib.map((x: any) => ({ emoji: x[1].char, name: x[0], alias: null })); + lib.forEach((x: any) => { if (x[1].keywords) { x[1].keywords.forEach(k => { @@ -43,6 +43,7 @@ lib.forEach((x: any) => { }); } }); + emjdb.sort((a, b) => a.name.length - b.name.length); export default Vue.extend({ @@ -107,8 +108,6 @@ export default Vue.extend({ }); }, methods: { - getAcct, - getUserName, exec() { this.select = -1; if (this.$refs.suggests) { diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue index 9b1449daa..751e4de50 100644 --- a/src/client/app/common/views/components/messaging.vue +++ b/src/client/app/common/views/components/messaging.vue @@ -14,8 +14,8 @@ tabindex="-1" > <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/> - <span class="name">{{ getUserName(user) }}</span> - <span class="username">@{{ getAcct(user) }}</span> + <span class="name">{{ user | userName }}</span> + <span class="username">@{{ user | acct }}</span> </li> </ol> </div> @@ -33,8 +33,8 @@ <div> <img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/> <header> - <span class="name">{{ getUserName(isMe(message) ? message.recipient : message.user) }}</span> - <span class="username">@{{ getAcct(isMe(message) ? message.recipient : message.user) }}</span> + <span class="name">{{ isMe(message) ? message.recipient : message.use | userName }}</span> + <span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span> <mk-time :time="message.createdAt"/> </header> <div class="body"> @@ -51,8 +51,6 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: { @@ -94,8 +92,6 @@ export default Vue.extend({ (this as any).os.streams.messagingIndexStream.dispose(this.connectionId); }, methods: { - getAcct, - getUserName, isMe(message) { return message.userId == (this as any).os.i.id; }, diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue index 61616da14..7571cfc5f 100644 --- a/src/client/app/common/views/components/welcome-timeline.vue +++ b/src/client/app/common/views/components/welcome-timeline.vue @@ -1,13 +1,13 @@ <template> <div class="mk-welcome-timeline"> <div v-for="note in notes"> - <router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`" v-user-preview="note.user.id"> + <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.user.id"> <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/> </router-link> <div class="body"> <header> - <router-link class="name" :to="`/@${getAcct(note.user)}`" v-user-preview="note.user.id">{{ getUserName(note.user) }}</router-link> - <span class="username">@{{ getAcct(note.user) }}</span> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> <div class="info"> <router-link class="created-at" :to="`/@${getAcct(note.user)}/${note.id}`"> <mk-time :time="note.createdAt"/> @@ -24,8 +24,6 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -38,8 +36,6 @@ export default Vue.extend({ this.fetch(); }, methods: { - getAcct, - getUserName, fetch(cb?) { this.fetching = true; (this as any).api('notes', { diff --git a/src/client/app/common/views/filters/index.ts b/src/client/app/common/views/filters/index.ts index 3a1d1ac23..1759c19c2 100644 --- a/src/client/app/common/views/filters/index.ts +++ b/src/client/app/common/views/filters/index.ts @@ -1,2 +1,4 @@ require('./bytes'); require('./number'); +require('./user'); +require('./note'); diff --git a/src/client/app/common/views/filters/note.ts b/src/client/app/common/views/filters/note.ts new file mode 100644 index 000000000..a611dc868 --- /dev/null +++ b/src/client/app/common/views/filters/note.ts @@ -0,0 +1,5 @@ +import Vue from 'vue'; + +Vue.filter('notePage', note => { + return '/notes/' + note.id; +}); diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts new file mode 100644 index 000000000..167bb7758 --- /dev/null +++ b/src/client/app/common/views/filters/user.ts @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; + +Vue.filter('acct', user => { + return getAcct(user); +}); + +Vue.filter('userName', user => { + return getUserName(user); +}); + +Vue.filter('userPage', user => { + return '/@' + Vue.filter('acct')(user); +}); diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue index d37ca745a..16206299d 100644 --- a/src/client/app/desktop/views/components/followers-window.vue +++ b/src/client/app/desktop/views/components/followers-window.vue @@ -1,7 +1,7 @@ <template> <mk-window width="400px" height="550px" @closed="$destroy"> <span slot="header" :class="$style.header"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロワー + <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user | userName }}のフォロワー </span> <mk-followers :user="user"/> </mk-window> @@ -9,15 +9,9 @@ <script lang="ts"> import Vue from 'vue'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ - props: ['user'], - computed { - name() { - return getUserName(this.user); - } - } + props: ['user'] }); </script> diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue index cbd8ec5f9..cc3d77198 100644 --- a/src/client/app/desktop/views/components/following-window.vue +++ b/src/client/app/desktop/views/components/following-window.vue @@ -1,7 +1,7 @@ <template> <mk-window width="400px" height="550px" @closed="$destroy"> <span slot="header" :class="$style.header"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロー + <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user | userName }}のフォロー </span> <mk-following :user="user"/> </mk-window> @@ -9,15 +9,9 @@ <script lang="ts"> import Vue from 'vue'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ - props: ['user'], - computed: { - name() { - return getUserName(this.user); - } - } + props: ['user'] }); </script> diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue index acc4542d9..af5bde3ad 100644 --- a/src/client/app/desktop/views/components/friends-maker.vue +++ b/src/client/app/desktop/views/components/friends-maker.vue @@ -3,12 +3,12 @@ <p class="title">気になるユーザーをフォロー:</p> <div class="users" v-if="!fetching && users.length > 0"> <div class="user" v-for="user in users" :key="user.id"> - <router-link class="avatar-anchor" :to="`/@${getAcct(user)}`"> + <router-link class="avatar-anchor" :to="user | userPage"> <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/> </router-link> <div class="body"> - <router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ getUserName(user) }}</router-link> - <p class="username">@{{ getAcct(user) }}</p> + <router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link> + <p class="username">@{{ user | acct }}</p> </div> <mk-follow-button :user="user"/> </div> @@ -22,8 +22,6 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -38,8 +36,6 @@ export default Vue.extend({ this.fetch(); }, methods: { - getAcct, - getUserName, fetch() { this.fetching = true; this.users = []; diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue index 7f8c35c2f..dbe326673 100644 --- a/src/client/app/desktop/views/components/messaging-room-window.vue +++ b/src/client/app/desktop/views/components/messaging-room-window.vue @@ -1,6 +1,6 @@ <template> <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy"> - <span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ name }}</span> + <span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ user | userName }}</span> <mk-messaging-room :user="user" :class="$style.content"/> </mk-window> </template> @@ -9,14 +9,10 @@ import Vue from 'vue'; import { url } from '../../../config'; import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['user'], computed: { - name(): string { - return getUserName(this.user); - }, popout(): string { return `${url}/i/messaging/${getAcct(this.user)}`; } diff --git a/src/client/app/desktop/views/components/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue index 79f5de1f8..16bc2a1d9 100644 --- a/src/client/app/desktop/views/components/note-detail.sub.vue +++ b/src/client/app/desktop/views/components/note-detail.sub.vue @@ -1,16 +1,16 @@ <template> <div class="sub" :title="title"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> + <router-link class="avatar-anchor" :to="note.user | userPage"> <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> </router-link> <div class="main"> <header> <div class="left"> - <router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link> - <span class="username">@{{ acct }}</span> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> </div> <div class="right"> - <router-link class="time" :to="`/@${acct}/${note.id}`"> + <router-link class="time" :to="note | notePage"> <mk-time :time="note.createdAt"/> </router-link> </div> @@ -28,18 +28,10 @@ <script lang="ts"> import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['note'], computed: { - acct() { - return getAcct(this.note.user); - }, - name() { - return getUserName(this.note.user); - }, title(): string { return dateStringify(this.note.createdAt); } diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue index eead82dd0..50bbb7698 100644 --- a/src/client/app/desktop/views/components/note-detail.vue +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -18,22 +18,22 @@ </div> <div class="renote" v-if="isRenote"> <p> - <router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId"> + <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId"> <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> </router-link> %fa:retweet% - <router-link class="name" :href="`/@${acct}`">{{ name }}</router-link> + <router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link> がRenote </p> </div> <article> - <router-link class="avatar-anchor" :to="`/@${pAcct}`"> + <router-link class="avatar-anchor" :to="p.user | userPage"> <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/> </router-link> <header> - <router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ name }}</router-link> - <span class="username">@{{ pAcct }}</span> - <router-link class="time" :to="`/@${pAcct}/${p.id}`"> + <router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link> + <span class="username">@{{ p.user | acct }}</span> + <router-link class="time" :to="p | notePage"> <mk-time :time="p.createdAt"/> </router-link> </header> @@ -78,8 +78,6 @@ <script lang="ts"> import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; import parse from '../../../../../text/parse'; import MkPostFormWindow from './post-form-window.vue'; @@ -131,18 +129,6 @@ export default Vue.extend({ title(): string { return dateStringify(this.p.createdAt); }, - acct(): string { - return getAcct(this.note.user); - }, - name(): string { - return getUserName(this.note.user); - }, - pAcct(): string { - return getAcct(this.p.user); - }, - pName(): string { - return getUserName(this.p.user); - }, urls(): string[] { if (this.p.text) { const ast = parse(this.p.text); diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue index bff199c09..ff3ecadc2 100644 --- a/src/client/app/desktop/views/components/note-preview.vue +++ b/src/client/app/desktop/views/components/note-preview.vue @@ -1,13 +1,13 @@ <template> <div class="mk-note-preview" :title="title"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> + <router-link class="avatar-anchor" :to="note.user | userPage"> <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="time" :to="`/@${acct}/${note.id}`"> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="time" :to="note | notePage"> <mk-time :time="note.createdAt"/> </router-link> </header> @@ -21,18 +21,10 @@ <script lang="ts"> import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['note'], computed: { - acct() { - return getAcct(this.note.user); - }, - name() { - return getUserName(this.note.user); - }, title(): string { return dateStringify(this.note.createdAt); } diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue index b49d12b92..e85478578 100644 --- a/src/client/app/desktop/views/components/notes.note.sub.vue +++ b/src/client/app/desktop/views/components/notes.note.sub.vue @@ -1,13 +1,13 @@ <template> <div class="sub" :title="title"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> + <router-link class="avatar-anchor" :to="note.user | userPage"> <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="created-at" :to="`/@${acct}/${note.id}`"> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="created-at" :to="note | notePage"> <mk-time :time="note.createdAt"/> </router-link> </header> @@ -21,18 +21,10 @@ <script lang="ts"> import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['note'], computed: { - acct() { - return getAcct(this.note.user); - }, - name(): string { - return getUserName(this.note.user); - }, title(): string { return dateStringify(this.note.createdAt); } diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index 0712069e5..8561643c9 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -5,29 +5,29 @@ </div> <div class="renote" v-if="isRenote"> <p> - <router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`" v-user-preview="note.userId"> + <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId"> <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> </router-link> %fa:retweet% <span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span> - <a class="name" :href="`/@${getAcct(note.user)}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</a> + <a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a> <span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span> </p> <mk-time :time="note.createdAt"/> </div> <article> - <router-link class="avatar-anchor" :to="`/@${getAcct(p.user)}`"> + <router-link class="avatar-anchor" :to="p.user | userPage"> <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/> </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${getAcct(p.user)}`" v-user-preview="p.user.id">{{ getUserName(p.user) }}</router-link> + <router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link> <span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span> - <span class="username">@{{ getAcct(p.user) }}</span> + <span class="username">@{{ p.user | acct }}</span> <div class="info"> <span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> <span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span> - <router-link class="created-at" :to="url"> + <router-link class="created-at" :to="p | notePage"> <mk-time :time="p.createdAt"/> </router-link> </div> @@ -85,8 +85,6 @@ <script lang="ts"> import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; import parse from '../../../../../text/parse'; import MkPostFormWindow from './post-form-window.vue'; @@ -117,9 +115,7 @@ export default Vue.extend({ return { isDetailOpened: false, connection: null, - connectionId: null, - getAcct, - getUserName + connectionId: null }; }, @@ -144,9 +140,6 @@ export default Vue.extend({ title(): string { return dateStringify(this.p.createdAt); }, - url(): string { - return `/@${this.acct}/${this.p.id}`; - }, urls(): string[] { if (this.p.text) { const ast = parse(this.p.text); diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue index 100a803cc..8b17c8c43 100644 --- a/src/client/app/desktop/views/components/notifications.vue +++ b/src/client/app/desktop/views/components/notifications.vue @@ -5,13 +5,13 @@ <div class="notification" :class="notification.type" :key="notification.id"> <mk-time :time="notification.createdAt"/> <template v-if="notification.type == 'reaction'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id"> + <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> </router-link> <div class="text"> <p> <mk-reaction-icon :reaction="notification.reaction"/> - <router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link> + <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> </p> <router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`"> %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% @@ -19,12 +19,12 @@ </div> </template> <template v-if="notification.type == 'renote'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId"> + <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> </router-link> <div class="text"> <p>%fa:retweet% - <router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link> + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> </p> <router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`"> %fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right% @@ -32,54 +32,54 @@ </div> </template> <template v-if="notification.type == 'quote'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId"> + <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> </router-link> <div class="text"> <p>%fa:quote-left% - <router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link> + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> </p> <router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link> </div> </template> <template v-if="notification.type == 'follow'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id"> + <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> </router-link> <div class="text"> <p>%fa:user-plus% - <router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link> + <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link> </p> </div> </template> <template v-if="notification.type == 'reply'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId"> + <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> </router-link> <div class="text"> <p>%fa:reply% - <router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link> + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> </p> <router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link> </div> </template> <template v-if="notification.type == 'mention'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId"> + <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId"> <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> </router-link> <div class="text"> <p>%fa:at% - <router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link> + <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link> </p> <a class="note-preview" :href="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</a> </div> </template> <template v-if="notification.type == 'poll_vote'"> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id"> + <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> </router-link> <div class="text"> - <p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</a></p> + <p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p> <router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`"> %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% </router-link> @@ -102,9 +102,7 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; import getNoteSummary from '../../../../../renderers/get-note-summary'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -154,8 +152,6 @@ export default Vue.extend({ (this as any).os.stream.dispose(this.connectionId); }, methods: { - getAcct, - getUserName, fetchMoreNotifications() { this.fetchingMoreNotifications = true; diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue new file mode 100644 index 000000000..16bc2a1d9 --- /dev/null +++ b/src/client/app/desktop/views/components/post-detail.sub.vue @@ -0,0 +1,122 @@ +<template> +<div class="sub" :title="title"> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> + </router-link> + <div class="main"> + <header> + <div class="left"> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + </div> + <div class="right"> + <router-link class="time" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </div> + </header> + <div class="body"> + <mk-note-html v-if="note.text" :text="note.text" :i="os.i" :class="$style.text"/> + <div class="media" v-if="note.media > 0"> + <mk-media-list :media-list="note.media"/> + </div> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import dateStringify from '../../../common/scripts/date-stringify'; + +export default Vue.extend({ + props: ['note'], + computed: { + title(): string { + return dateStringify(this.note.createdAt); + } + } +}); +</script> + +<style lang="stylus" scoped> +.sub + margin 0 + padding 20px 32px + background #fdfdfd + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 16px 0 0 + + > .avatar + display block + width 44px + height 44px + margin 0 + border-radius 4px + vertical-align bottom + + > .main + float left + width calc(100% - 60px) + + > header + margin-bottom 4px + white-space nowrap + + &:after + content "" + display block + clear both + + > .left + float left + + > .name + display inline + margin 0 + padding 0 + color #777 + font-size 1em + font-weight 700 + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 0 0 8px + color #ccc + + > .right + float right + + > .time + font-size 0.9em + color #c0c0c0 + +</style> + +<style lang="stylus" module> +.text + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1em + color #717171 +</style> diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue new file mode 100644 index 000000000..50bbb7698 --- /dev/null +++ b/src/client/app/desktop/views/components/post-detail.vue @@ -0,0 +1,434 @@ +<template> +<div class="mk-note-detail" :title="title"> + <button + class="read-more" + v-if="p.reply && p.reply.replyId && context == null" + title="会話をもっと読み込む" + @click="fetchContext" + :disabled="contextFetching" + > + <template v-if="!contextFetching">%fa:ellipsis-v%</template> + <template v-if="contextFetching">%fa:spinner .pulse%</template> + </button> + <div class="context"> + <x-sub v-for="note in context" :key="note.id" :note="note"/> + </div> + <div class="reply-to" v-if="p.reply"> + <x-sub :note="p.reply"/> + </div> + <div class="renote" v-if="isRenote"> + <p> + <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> + </router-link> + %fa:retweet% + <router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link> + がRenote + </p> + </div> + <article> + <router-link class="avatar-anchor" :to="p.user | userPage"> + <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/> + </router-link> + <header> + <router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link> + <span class="username">@{{ p.user | acct }}</span> + <router-link class="time" :to="p | notePage"> + <mk-time :time="p.createdAt"/> + </router-link> + </header> + <div class="body"> + <mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/> + <div class="media" v-if="p.media.length > 0"> + <mk-media-list :media-list="p.media"/> + </div> + <mk-poll v-if="p.poll" :note="p"/> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> + </div> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <div class="map" v-if="p.geo" ref="map"></div> + <div class="renote" v-if="p.renote"> + <mk-note-preview :note="p.renote"/> + </div> + </div> + <footer> + <mk-reactions-viewer :note="p"/> + <button @click="reply" title="返信"> + %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> + </button> + <button @click="renote" title="Renote"> + %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> + </button> + <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="リアクション"> + %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> + </button> + <button @click="menu" ref="menuButton"> + %fa:ellipsis-h% + </button> + </footer> + </article> + <div class="replies" v-if="!compact"> + <x-sub v-for="note in replies" :key="note.id" :note="note"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import dateStringify from '../../../common/scripts/date-stringify'; +import parse from '../../../../../text/parse'; + +import MkPostFormWindow from './post-form-window.vue'; +import MkRenoteFormWindow from './renote-form-window.vue'; +import MkNoteMenu from '../../../common/views/components/note-menu.vue'; +import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; +import XSub from './note-detail.sub.vue'; + +export default Vue.extend({ + components: { + XSub + }, + + props: { + note: { + type: Object, + required: true + }, + compact: { + default: false + } + }, + + data() { + return { + context: [], + contextFetching: false, + replies: [] + }; + }, + + computed: { + isRenote(): boolean { + return (this.note.renote && + this.note.text == null && + this.note.mediaIds.length == 0 && + this.note.poll == null); + }, + p(): any { + return this.isRenote ? this.note.renote : this.note; + }, + reactionsCount(): number { + return this.p.reactionCounts + ? Object.keys(this.p.reactionCounts) + .map(key => this.p.reactionCounts[key]) + .reduce((a, b) => a + b) + : 0; + }, + title(): string { + return dateStringify(this.p.createdAt); + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } + } + }, + + mounted() { + // Get replies + if (!this.compact) { + (this as any).api('notes/replies', { + noteId: this.p.id, + limit: 8 + }).then(replies => { + this.replies = replies; + }); + } + + // Draw map + if (this.p.geo) { + const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true; + if (shouldShowMap) { + (this as any).os.getGoogleMaps().then(maps => { + const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); + const map = new maps.Map(this.$refs.map, { + center: uluru, + zoom: 15 + }); + new maps.Marker({ + position: uluru, + map: map + }); + }); + } + } + }, + + methods: { + fetchContext() { + this.contextFetching = true; + + // Fetch context + (this as any).api('notes/context', { + noteId: this.p.replyId + }).then(context => { + this.contextFetching = false; + this.context = context.reverse(); + }); + }, + reply() { + (this as any).os.new(MkPostFormWindow, { + reply: this.p + }); + }, + renote() { + (this as any).os.new(MkRenoteFormWindow, { + note: this.p + }); + }, + react() { + (this as any).os.new(MkReactionPicker, { + source: this.$refs.reactButton, + note: this.p + }); + }, + menu() { + (this as any).os.new(MkNoteMenu, { + source: this.$refs.menuButton, + note: this.p + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-note-detail + margin 0 + padding 0 + overflow hidden + text-align left + background #fff + border solid 1px rgba(0, 0, 0, 0.1) + border-radius 8px + + > .read-more + display block + margin 0 + padding 10px 0 + width 100% + font-size 1em + text-align center + color #999 + cursor pointer + background #fafafa + outline none + border none + border-bottom solid 1px #eef0f2 + border-radius 6px 6px 0 0 + + &:hover + background #f6f6f6 + + &:active + background #f0f0f0 + + &:disabled + color #ccc + + > .context + > * + border-bottom 1px solid #eef0f2 + + > .renote + color #9dbb00 + background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + + > p + margin 0 + padding 16px 32px + + .avatar-anchor + display inline-block + + .avatar + vertical-align bottom + min-width 28px + min-height 28px + max-width 28px + max-height 28px + margin 0 8px 0 0 + border-radius 6px + + [data-fa] + margin-right 4px + + .name + font-weight bold + + & + article + padding-top 8px + + > .reply-to + border-bottom 1px solid #eef0f2 + + > article + padding 28px 32px 18px 32px + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + width 60px + height 60px + + > .avatar + display block + width 60px + height 60px + margin 0 + border-radius 8px + vertical-align bottom + + > header + position absolute + top 28px + left 108px + width calc(100% - 108px) + + > .name + display inline-block + margin 0 + line-height 24px + color #777 + font-size 18px + font-weight 700 + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + display block + text-align left + margin 0 + color #ccc + + > .time + position absolute + top 0 + right 32px + font-size 1em + color #c0c0c0 + + > .body + padding 8px 0 + + > .renote + margin 8px 0 + + > .mk-note-preview + padding 16px + border dashed 1px #c0dac6 + border-radius 8px + + > .location + margin 4px 0 + font-size 12px + color #ccc + + > .map + width 100% + height 300px + + &:empty + display none + + > .mk-url-preview + margin-top 8px + + > .tags + margin 4px 0 0 0 + + > * + display inline-block + margin 0 8px 0 0 + padding 2px 8px 2px 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + + &:hover + text-decoration none + background #e2e7ec + + > footer + font-size 1.2em + + > button + margin 0 28px 0 0 + padding 8px + background transparent + border none + font-size 1em + color #ddd + cursor pointer + + &:hover + color #666 + + > .count + display inline + margin 0 0 0 8px + color #999 + + &.reacted + color $theme-color + + > .replies + > * + border-top 1px solid #eef0f2 + +</style> + +<style lang="stylus" module> +.text + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1.5em + color #717171 +</style> diff --git a/src/client/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/post-preview.vue new file mode 100644 index 000000000..ff3ecadc2 --- /dev/null +++ b/src/client/app/desktop/views/components/post-preview.vue @@ -0,0 +1,99 @@ +<template> +<div class="mk-note-preview" :title="title"> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="time" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </header> + <div class="body"> + <mk-sub-note-content class="text" :note="note"/> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import dateStringify from '../../../common/scripts/date-stringify'; + +export default Vue.extend({ + props: ['note'], + computed: { + title(): string { + return dateStringify(this.note.createdAt); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-note-preview + font-size 0.9em + background #fff + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 16px 0 0 + + > .avatar + display block + width 52px + height 52px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 68px) + + > header + display flex + white-space nowrap + + > .name + margin 0 .5em 0 0 + padding 0 + color #607073 + font-size 1em + font-weight bold + text-decoration none + white-space normal + + &:hover + text-decoration underline + + > .username + margin 0 .5em 0 0 + color #d1d8da + + > .time + margin-left auto + color #b2b8bb + + > .body + + > .text + cursor default + margin 0 + padding 0 + font-size 1.1em + color #717171 + +</style> diff --git a/src/client/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/posts.post.sub.vue new file mode 100644 index 000000000..e85478578 --- /dev/null +++ b/src/client/app/desktop/views/components/posts.post.sub.vue @@ -0,0 +1,108 @@ +<template> +<div class="sub" :title="title"> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="created-at" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </header> + <div class="body"> + <mk-sub-note-content class="text" :note="note"/> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import dateStringify from '../../../common/scripts/date-stringify'; + +export default Vue.extend({ + props: ['note'], + computed: { + title(): string { + return dateStringify(this.note.createdAt); + } + } +}); +</script> + +<style lang="stylus" scoped> +.sub + margin 0 + padding 16px + font-size 0.9em + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 14px 0 0 + + > .avatar + display block + width 52px + height 52px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 66px) + + > header + display flex + margin-bottom 2px + white-space nowrap + line-height 21px + + > .name + display block + margin 0 .5em 0 0 + padding 0 + overflow hidden + color #607073 + font-size 1em + font-weight bold + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .username + margin 0 .5em 0 0 + color #d1d8da + + > .created-at + margin-left auto + color #b2b8bb + + > .body + + > .text + cursor default + margin 0 + padding 0 + font-size 1.1em + color #717171 + + pre + max-height 120px + font-size 80% + +</style> diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue new file mode 100644 index 000000000..322bf2922 --- /dev/null +++ b/src/client/app/desktop/views/components/posts.post.vue @@ -0,0 +1,585 @@ +<template> +<div class="note" tabindex="-1" :title="title" @keydown="onKeydown"> + <div class="reply-to" v-if="p.reply"> + <x-sub :note="p.reply"/> + </div> + <div class="renote" v-if="isRenote"> + <p> + <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> + </router-link> + %fa:retweet% + <span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span> + <a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a> + <span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span> + </p> + <mk-time :time="note.createdAt"/> + </div> + <article> + <router-link class="avatar-anchor" :to="p.user | userPage"> + <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link> + <span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span> + <span class="username">@{{ p.user | acct }}</span> + <div class="info"> + <span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> + <span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span> + <router-link class="created-at" :to="url"> + <mk-time :time="p.createdAt"/> + </router-link> + </div> + </header> + <div class="body"> + <p class="channel" v-if="p.channel"> + <a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>: + </p> + <div class="text"> + <a class="reply" v-if="p.reply">%fa:reply%</a> + <mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/> + <a class="rp" v-if="p.renote">RP:</a> + </div> + <div class="media" v-if="p.media.length > 0"> + <mk-media-list :media-list="p.media"/> + </div> + <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> + </div> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <div class="map" v-if="p.geo" ref="map"></div> + <div class="renote" v-if="p.renote"> + <mk-note-preview :note="p.renote"/> + </div> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + </div> + <footer> + <mk-reactions-viewer :note="p" ref="reactionsViewer"/> + <button @click="reply" title="%i18n:desktop.tags.mk-timeline-note.reply%"> + %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> + </button> + <button @click="renote" title="%i18n:desktop.tags.mk-timeline-note.renote%"> + %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> + </button> + <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-note.add-reaction%"> + %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> + </button> + <button @click="menu" ref="menuButton"> + %fa:ellipsis-h% + </button> + <button title="%i18n:desktop.tags.mk-timeline-note.detail"> + <template v-if="!isDetailOpened">%fa:caret-down%</template> + <template v-if="isDetailOpened">%fa:caret-up%</template> + </button> + </footer> + </div> + </article> + <div class="detail" v-if="isDetailOpened"> + <mk-note-status-graph width="462" height="130" :note="p"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import dateStringify from '../../../common/scripts/date-stringify'; +import parse from '../../../../../text/parse'; + +import MkPostFormWindow from './post-form-window.vue'; +import MkRenoteFormWindow from './renote-form-window.vue'; +import MkNoteMenu from '../../../common/views/components/note-menu.vue'; +import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; +import XSub from './notes.note.sub.vue'; + +function focus(el, fn) { + const target = fn(el); + if (target) { + if (target.hasAttribute('tabindex')) { + target.focus(); + } else { + focus(target, fn); + } + } +} + +export default Vue.extend({ + components: { + XSub + }, + + props: ['note'], + + data() { + return { + isDetailOpened: false, + connection: null, + connectionId: null + }; + }, + + computed: { + isRenote(): boolean { + return (this.note.renote && + this.note.text == null && + this.note.mediaIds.length == 0 && + this.note.poll == null); + }, + p(): any { + return this.isRenote ? this.note.renote : this.note; + }, + reactionsCount(): number { + return this.p.reactionCounts + ? Object.keys(this.p.reactionCounts) + .map(key => this.p.reactionCounts[key]) + .reduce((a, b) => a + b) + : 0; + }, + title(): string { + return dateStringify(this.p.createdAt); + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } + } + }, + + created() { + if ((this as any).os.isSignedIn) { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + } + }, + + mounted() { + this.capture(true); + + if ((this as any).os.isSignedIn) { + this.connection.on('_connected_', this.onStreamConnected); + } + + // Draw map + if (this.p.geo) { + const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true; + if (shouldShowMap) { + (this as any).os.getGoogleMaps().then(maps => { + const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); + const map = new maps.Map(this.$refs.map, { + center: uluru, + zoom: 15 + }); + new maps.Marker({ + position: uluru, + map: map + }); + }); + } + } + }, + + beforeDestroy() { + this.decapture(true); + + if ((this as any).os.isSignedIn) { + this.connection.off('_connected_', this.onStreamConnected); + (this as any).os.stream.dispose(this.connectionId); + } + }, + + methods: { + capture(withHandler = false) { + if ((this as any).os.isSignedIn) { + this.connection.send({ + type: 'capture', + id: this.p.id + }); + if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); + } + }, + decapture(withHandler = false) { + if ((this as any).os.isSignedIn) { + this.connection.send({ + type: 'decapture', + id: this.p.id + }); + if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated); + } + }, + onStreamConnected() { + this.capture(); + }, + onStreamNoteUpdated(data) { + const note = data.note; + if (note.id == this.note.id) { + this.$emit('update:note', note); + } else if (note.id == this.note.renoteId) { + this.note.renote = note; + } + }, + reply() { + (this as any).os.new(MkPostFormWindow, { + reply: this.p + }); + }, + renote() { + (this as any).os.new(MkRenoteFormWindow, { + note: this.p + }); + }, + react() { + (this as any).os.new(MkReactionPicker, { + source: this.$refs.reactButton, + note: this.p + }); + }, + menu() { + (this as any).os.new(MkNoteMenu, { + source: this.$refs.menuButton, + note: this.p + }); + }, + onKeydown(e) { + let shouldBeCancel = true; + + switch (true) { + case e.which == 38: // [↑] + case e.which == 74: // [j] + case e.which == 9 && e.shiftKey: // [Shift] + [Tab] + focus(this.$el, e => e.previousElementSibling); + break; + + case e.which == 40: // [↓] + case e.which == 75: // [k] + case e.which == 9: // [Tab] + focus(this.$el, e => e.nextElementSibling); + break; + + case e.which == 81: // [q] + case e.which == 69: // [e] + this.renote(); + break; + + case e.which == 70: // [f] + case e.which == 76: // [l] + //this.like(); + break; + + case e.which == 82: // [r] + this.reply(); + break; + + default: + shouldBeCancel = false; + } + + if (shouldBeCancel) e.preventDefault(); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.note + margin 0 + padding 0 + background #fff + border-bottom solid 1px #eaeaea + + &:first-child + border-top-left-radius 6px + border-top-right-radius 6px + + > .renote + border-top-left-radius 6px + border-top-right-radius 6px + + &:last-of-type + border-bottom none + + &:focus + z-index 1 + + &:after + content "" + pointer-events none + position absolute + top 2px + right 2px + bottom 2px + left 2px + border 2px solid rgba($theme-color, 0.3) + border-radius 4px + + > .renote + color #9dbb00 + background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + + > p + margin 0 + padding 16px 32px + line-height 28px + + .avatar-anchor + display inline-block + + .avatar + vertical-align bottom + width 28px + height 28px + margin 0 8px 0 0 + border-radius 6px + + [data-fa] + margin-right 4px + + .name + font-weight bold + + > .mk-time + position absolute + top 16px + right 32px + font-size 0.9em + line-height 28px + + & + article + padding-top 8px + + > .reply-to + padding 0 16px + background rgba(0, 0, 0, 0.0125) + + > .mk-note-preview + background transparent + + > article + padding 28px 32px 18px 32px + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 16px 10px 0 + //position -webkit-sticky + //position sticky + //top 74px + + > .avatar + display block + width 58px + height 58px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 74px) + + > header + display flex + align-items center + margin-bottom 4px + white-space nowrap + + > .name + display block + margin 0 .5em 0 0 + padding 0 + overflow hidden + color #627079 + font-size 1em + font-weight bold + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .is-bot + margin 0 .5em 0 0 + padding 1px 6px + font-size 12px + color #aaa + border solid 1px #ddd + border-radius 3px + + > .username + margin 0 .5em 0 0 + color #ccc + + > .info + margin-left auto + font-size 0.9em + + > .mobile + margin-right 8px + color #ccc + + > .app + margin-right 8px + padding-right 8px + color #ccc + border-right solid 1px #eaeaea + + > .created-at + color #c0c0c0 + + > .body + + > .text + cursor default + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1.1em + color #717171 + + >>> .quote + margin 8px + padding 6px 12px + color #aaa + border-left solid 3px #eee + + > .reply + margin-right 8px + color #717171 + + > .rp + margin-left 4px + font-style oblique + color #a0bf46 + + > .location + margin 4px 0 + font-size 12px + color #ccc + + > .map + width 100% + height 300px + + &:empty + display none + + > .tags + margin 4px 0 0 0 + + > * + display inline-block + margin 0 8px 0 0 + padding 2px 8px 2px 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + + &:hover + text-decoration none + background #e2e7ec + + .mk-url-preview + margin-top 8px + + > .channel + margin 0 + + > .mk-poll + font-size 80% + + > .renote + margin 8px 0 + + > .mk-note-preview + padding 16px + border dashed 1px #c0dac6 + border-radius 8px + + > footer + > button + margin 0 28px 0 0 + padding 0 8px + line-height 32px + font-size 1em + color #ddd + background transparent + border none + cursor pointer + + &:hover + color #666 + + > .count + display inline + margin 0 0 0 8px + color #999 + + &.reacted + color $theme-color + + &:last-child + position absolute + right 0 + margin 0 + + > .detail + padding-top 4px + background rgba(0, 0, 0, 0.0125) + +</style> + +<style lang="stylus" module> +.text + + code + padding 4px 8px + margin 0 0.5em + font-size 80% + color #525252 + background #f8f8f8 + border-radius 2px + + pre > code + padding 16px + margin 0 + + [data-is-me]:after + content "you" + padding 0 4px + margin-left 4px + font-size 80% + color $theme-color-foreground + background $theme-color + border-radius 4px +</style> diff --git a/src/client/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue index 6bdc76653..94492ad26 100644 --- a/src/client/app/desktop/views/components/settings.mute.vue +++ b/src/client/app/desktop/views/components/settings.mute.vue @@ -5,7 +5,7 @@ </div> <div class="users" v-if="users.length != 0"> <div v-for="user in users" :key="user.id"> - <p><b>{{ getUserName(user) }}</b> @{{ getAcct(user) }}</p> + <p><b>{{ user | userName }}</b> @{{ user | acct }}</p> </div> </div> </div> @@ -13,8 +13,6 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -23,10 +21,6 @@ export default Vue.extend({ users: [] }; }, - methods: { - getAcct, - getUserName - }, mounted() { (this as any).api('mute/list').then(x => { this.users = x.users; diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue index 527d10843..2b63030cd 100644 --- a/src/client/app/desktop/views/components/ui.header.vue +++ b/src/client/app/desktop/views/components/ui.header.vue @@ -4,7 +4,7 @@ <div class="main" ref="main"> <div class="backdrop"></div> <div class="main"> - <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p> + <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i | userName }}</b>さん</p> <div class="container" ref="mainContainer"> <div class="left"> <x-nav/> @@ -33,14 +33,7 @@ import XNotifications from './ui.header.notifications.vue'; import XPost from './ui.header.post.vue'; import XClock from './ui.header.clock.vue'; -import getUserName from '../../../../../renderers/get-user-name'; - export default Vue.extend({ - computed: { - name(): string { - return getUserName((this as any).os.i); - } - }, components: { XNav, XSearch, diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue index 1cc53743a..24337eea2 100644 --- a/src/client/app/desktop/views/components/user-preview.vue +++ b/src/client/app/desktop/views/components/user-preview.vue @@ -2,12 +2,12 @@ <div class="mk-user-preview"> <template v-if="u != null"> <div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div> - <router-link class="avatar" :to="`/@${getAcct(u)}`"> + <router-link class="avatar" :to="u | userPage"> <img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> <div class="title"> - <router-link class="name" :to="`/@${getAcct(u)}`">{{ u.name }}</router-link> - <p class="username">@{{ getAcct(u) }}</p> + <router-link class="name" :to="u | userPage">{{ u.name }}</router-link> + <p class="username">@{{ u | acct }}</p> </div> <div class="description">{{ u.description }}</div> <div class="status"> diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue index c7a132ecf..005c9cd6d 100644 --- a/src/client/app/desktop/views/components/users-list.item.vue +++ b/src/client/app/desktop/views/components/users-list.item.vue @@ -1,12 +1,12 @@ <template> <div class="root item"> - <router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="user.id"> + <router-link class="avatar-anchor" :to="user | userPage" v-user-preview="user.id"> <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`" v-user-preview="user.id">{{ name }}</router-link> - <span class="username">@{{ acct }}</span> + <router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link> + <span class="username">@{{ user | acct }}</span> </header> <div class="body"> <p class="followed" v-if="user.isFollowed">フォローされています</p> @@ -19,19 +19,9 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ - props: ['user'], - computed: { - acct() { - return getAcct(this.user); - }, - name() { - return getUserName(this.user); - } - } + props: ['user'] }); </script> diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue index 351b1264f..4113ef13a 100644 --- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue +++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue @@ -3,8 +3,8 @@ <p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p> <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p> <div v-if="!fetching && users.length > 0"> - <router-link v-for="user in users" :to="`/@${getAcct(user)}`" :key="user.id"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)" v-user-preview="user.id"/> + <router-link v-for="user in users" :to="user | userPage" :key="user.id"> + <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName" v-user-preview="user.id"/> </router-link> </div> <p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p> @@ -13,8 +13,6 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../../acct/render'; -import getUserName from '../../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['user'], @@ -24,10 +22,6 @@ export default Vue.extend({ fetching: true }; }, - methods: { - getAcct, - getUserName - }, mounted() { (this as any).api('users/followers', { userId: this.user.id, diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue index c9213cb50..8512e8027 100644 --- a/src/client/app/desktop/views/pages/user/user.friends.vue +++ b/src/client/app/desktop/views/pages/user/user.friends.vue @@ -4,12 +4,12 @@ <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p> <template v-if="!fetching && users.length != 0"> <div class="user" v-for="friend in users"> - <router-link class="avatar-anchor" :to="`/@${getAcct(friend)}`"> + <router-link class="avatar-anchor" :to="friend | userPage"> <img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/> </router-link> <div class="body"> - <router-link class="name" :to="`/@${getAcct(friend)}`" v-user-preview="friend.id">{{ friend.name }}</router-link> - <p class="username">@{{ getAcct(friend) }}</p> + <router-link class="name" :to="friend | userPage" v-user-preview="friend.id">{{ friend.name }}</router-link> + <p class="username">@{{ friend | acct }}</p> </div> <mk-follow-button :user="friend"/> </div> 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 f67bf5da2..67e52d173 100644 --- a/src/client/app/desktop/views/pages/user/user.header.vue +++ b/src/client/app/desktop/views/pages/user/user.header.vue @@ -7,8 +7,8 @@ <div class="container"> <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/> <div class="title"> - <p class="name">{{ name }}</p> - <p class="username">@{{ acct }}</p> + <p class="name">{{ user | userName }}</p> + <p class="username">@{{ user | acct }}</p> <p class="location" v-if="user.host === null && user.profile.location">%fa:map-marker%{{ user.profile.location }}</p> </div> <footer> @@ -22,19 +22,9 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../../acct/render'; -import getUserName from '../../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['user'], - computed: { - acct() { - return getAcct(this.user); - }, - name() { - return getUserName(this.user); - } - }, mounted() { window.addEventListener('load', this.onScroll); window.addEventListener('scroll', this.onScroll); diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue index d07b462b5..3644286fb 100644 --- a/src/client/app/desktop/views/pages/user/user.vue +++ b/src/client/app/desktop/views/pages/user/user.vue @@ -45,7 +45,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; Progress.done(); - document.title = getUserName(user) + ' | Misskey'; + document.title = getUserName(this.user) + ' | Misskey'; }); } } diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue index 41b015b8a..bc6ebae77 100644 --- a/src/client/app/desktop/views/pages/welcome.vue +++ b/src/client/app/desktop/views/pages/welcome.vue @@ -8,7 +8,7 @@ <p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p> <p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p> <div class="users"> - <router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${getAcct(user)}`" v-user-preview="user.id"> + <router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="user | userPage" v-user-preview="user.id"> <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> </div> diff --git a/src/client/app/desktop/views/widgets/channel.channel.note.vue b/src/client/app/desktop/views/widgets/channel.channel.note.vue index 313a2e3f4..776791906 100644 --- a/src/client/app/desktop/views/widgets/channel.channel.note.vue +++ b/src/client/app/desktop/views/widgets/channel.channel.note.vue @@ -2,8 +2,8 @@ <div class="note"> <header> <a class="index" @click="reply">{{ note.index }}:</a> - <router-link class="name" :to="`/@${acct}`" v-user-preview="note.user.id"><b>{{ name }}</b></router-link> - <span>ID:<i>{{ acct }}</i></span> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"><b>{{ note.user | userName }}</b></router-link> + <span>ID:<i>{{ note.user | acct }}</i></span> </header> <div> <a v-if="note.reply">>>{{ note.reply.index }}</a> @@ -19,19 +19,9 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['note'], - computed: { - acct() { - return getAcct(this.note.user); - }, - name() { - return getUserName(this.note.user); - } - }, methods: { reply() { this.$emit('reply', this.note); diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue new file mode 100644 index 000000000..776791906 --- /dev/null +++ b/src/client/app/desktop/views/widgets/channel.channel.post.vue @@ -0,0 +1,65 @@ +<template> +<div class="note"> + <header> + <a class="index" @click="reply">{{ note.index }}:</a> + <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"><b>{{ note.user | userName }}</b></router-link> + <span>ID:<i>{{ note.user | acct }}</i></span> + </header> + <div> + <a v-if="note.reply">>>{{ note.reply.index }}</a> + {{ note.text }} + <div class="media" v-if="note.media"> + <a v-for="file in note.media" :href="file.url" target="_blank"> + <img :src="`${file.url}?thumbnail&size=512`" :alt="file.name" :title="file.name"/> + </a> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'], + methods: { + reply() { + this.$emit('reply', this.note); + } + } +}); +</script> + +<style lang="stylus" scoped> +.note + margin 0 + padding 0 + color #444 + + > header + position -webkit-sticky + position sticky + z-index 1 + top 0 + padding 8px 4px 4px 16px + background rgba(255, 255, 255, 0.9) + + > .index + margin-right 0.25em + + > .name + margin-right 0.5em + color #008000 + + > div + padding 0 16px 16px 16px + + > .media + > a + display inline-block + + > img + max-width 100% + vertical-align bottom + +</style> diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue index 98e42222e..1b4b11de3 100644 --- a/src/client/app/desktop/views/widgets/profile.vue +++ b/src/client/app/desktop/views/widgets/profile.vue @@ -15,14 +15,13 @@ title="クリックでアバター編集" v-user-preview="os.i.id" /> - <router-link class="name" :to="`/@${os.i.username}`">{{ name }}</router-link> - <p class="username">@{{ os.i.username }}</p> + <router-link class="name" :to="os.i | userPage">{{ os.i | userName }}</router-link> + <p class="username">@{{ os.i | acct }}</p> </div> </template> <script lang="ts"> import define from '../../../common/define-widget'; -import getUserName from '../../../../../renderers/get-user-name'; export default define({ name: 'profile', @@ -30,11 +29,6 @@ export default define({ design: 0 }) }).extend({ - computed: { - name() { - return getUserName(this.os.i); - } - }, methods: { func() { if (this.props.design == 2) { diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue index a5dabb68f..c7075d9a5 100644 --- a/src/client/app/desktop/views/widgets/users.vue +++ b/src/client/app/desktop/views/widgets/users.vue @@ -7,12 +7,12 @@ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> <template v-else-if="users.length != 0"> <div class="user" v-for="_user in users"> - <router-link class="avatar-anchor" :to="`/@${getAcct(_user)}`"> + <router-link class="avatar-anchor" :to="_user | userPage"> <img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/> </router-link> <div class="body"> - <router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ getUserName(_user) }}</router-link> - <p class="username">@{{ getAcct(_user) }}</p> + <router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link> + <p class="username">@{{ _user | acct }}</p> </div> <mk-follow-button :user="_user"/> </div> @@ -23,8 +23,6 @@ <script lang="ts"> import define from '../../../common/define-widget'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; const limit = 3; @@ -45,8 +43,6 @@ export default define({ this.fetch(); }, methods: { - getAcct, - getUserName, func() { this.props.compact = !this.props.compact; }, diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue index 9ad0d3e29..393fa9b83 100644 --- a/src/client/app/mobile/views/components/note-card.vue +++ b/src/client/app/mobile/views/components/note-card.vue @@ -1,8 +1,8 @@ <template> <div class="mk-note-card"> - <a :href="`/@${acct}/${note.id}`"> + <a :href="note | notePage"> <header> - <img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ name }}</h3> + <img :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/><h3>{{ note.user | userName }}</h3> </header> <div> {{ text }} @@ -15,18 +15,10 @@ <script lang="ts"> import Vue from 'vue'; import summary from '../../../../../renderers/get-note-summary'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['note'], computed: { - acct() { - return getAcct(this.note.user); - }, - name() { - return getUserName(this.note.user); - }, text(): string { return summary(this.note); } diff --git a/src/client/app/mobile/views/components/note-detail.sub.vue b/src/client/app/mobile/views/components/note-detail.sub.vue index 38aea4ba2..06f442d30 100644 --- a/src/client/app/mobile/views/components/note-detail.sub.vue +++ b/src/client/app/mobile/views/components/note-detail.sub.vue @@ -1,13 +1,13 @@ <template> <div class="root sub"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> + <router-link class="avatar-anchor" :to="note.user | userPage"> <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`">{{ getUserName(note.user) }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="time" :to="`/@${acct}/${note.id}`"> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="time" :to="note | notePage"> <mk-time :time="note.createdAt"/> </router-link> </header> @@ -20,19 +20,9 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ - props: ['note'], - computed: { - acct() { - return getAcct(this.note.user); - }, - name() { - return getUserName(this.note.user); - } - } + props: ['note'] }); </script> diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue index 483f5aaf3..de32f0a74 100644 --- a/src/client/app/mobile/views/components/note-detail.vue +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -17,24 +17,20 @@ </div> <div class="renote" v-if="isRenote"> <p> - <router-link class="avatar-anchor" :to="`/@${acct}`"> + <router-link class="avatar-anchor" :to="note.user | userPage"> <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> </router-link> - %fa:retweet% - <router-link class="name" :to="`/@${acct}`"> - {{ name }} - </router-link> - がRenote + %fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote </p> </div> <article> <header> - <router-link class="avatar-anchor" :to="`/@${pAcct}`"> + <router-link class="avatar-anchor" :to="p.user | userPage"> <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> <div> - <router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link> - <span class="username">@{{ pAcct }}</span> + <router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> + <span class="username">@{{ p.user | acct }}</span> </div> </header> <div class="body"> @@ -80,8 +76,6 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; import parse from '../../../../../text/parse'; import MkNoteMenu from '../../../common/views/components/note-menu.vue'; @@ -112,18 +106,6 @@ export default Vue.extend({ }, computed: { - acct(): string { - return getAcct(this.note.user); - }, - name(): string { - return getUserName(this.note.user); - }, - pAcct(): string { - return getAcct(this.p.user); - }, - pName(): string { - return getUserName(this.p.user); - }, isRenote(): boolean { return (this.note.renote && this.note.text == null && diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue index 8c8d8645b..b9a6db315 100644 --- a/src/client/app/mobile/views/components/note-preview.vue +++ b/src/client/app/mobile/views/components/note-preview.vue @@ -1,13 +1,13 @@ <template> <div class="mk-note-preview"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> + <router-link class="avatar-anchor" :to="note.user | userPage"> <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`">{{ name }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="time" :to="`/@${acct}/${note.id}`"> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="time" :to="note | notePage"> <mk-time :time="note.createdAt"/> </router-link> </header> @@ -20,19 +20,9 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ - props: ['note'], - computed: { - acct() { - return getAcct(this.note.user); - }, - name() { - return getUserName(this.note.user); - } - } + props: ['note'] }); </script> diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue index 96f8265cc..d489f3a05 100644 --- a/src/client/app/mobile/views/components/note.sub.vue +++ b/src/client/app/mobile/views/components/note.sub.vue @@ -1,13 +1,13 @@ <template> <div class="sub"> - <router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`"> + <router-link class="avatar-anchor" :to="note.user | userPage"> <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/> </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${getAcct(note.user)}`">{{ getUserName(note.user) }}</router-link> - <span class="username">@{{ getAcct(note.user) }}</span> - <router-link class="created-at" :to="`/@${getAcct(note.user)}/${note.id}`"> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="created-at" :to="note | notePage"> <mk-time :time="note.createdAt"/> </router-link> </header> @@ -20,17 +20,9 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ - props: ['note'], - data() { - return { - getAcct, - getUserName - }; - } + props: ['note'] }); </script> diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue index 295fe4d6a..033de4f42 100644 --- a/src/client/app/mobile/views/components/note.vue +++ b/src/client/app/mobile/views/components/note.vue @@ -5,28 +5,28 @@ </div> <div class="renote" v-if="isRenote"> <p> - <router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`"> + <router-link class="avatar-anchor" :to="note.user | userPage"> <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> %fa:retweet% <span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span> - <router-link class="name" :to="`/@${getAcct(note.user)}`">{{ getUserName(note.user) }}</router-link> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> <span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span> </p> <mk-time :time="note.createdAt"/> </div> <article> - <router-link class="avatar-anchor" :to="`/@${getAcct(p.user)}`"> + <router-link class="avatar-anchor" :to="p.user | userPage"> <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/> </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${getAcct(p.user)}`">{{ getUserName(p.user) }}</router-link> + <router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> <span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span> - <span class="username">@{{ getAcct(p.user) }}</span> + <span class="username">@{{ p.user | acct }}</span> <div class="info"> <span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span> - <router-link class="created-at" :to="url"> + <router-link class="created-at" :to="p | notePage"> <mk-time :time="p.createdAt"/> </router-link> </div> @@ -77,8 +77,6 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; import parse from '../../../../../text/parse'; import MkNoteMenu from '../../../common/views/components/note-menu.vue'; @@ -95,9 +93,7 @@ export default Vue.extend({ data() { return { connection: null, - connectionId: null, - getAcct, - getUserName + connectionId: null }; }, @@ -118,9 +114,6 @@ export default Vue.extend({ .reduce((a, b) => a + b) : 0; }, - url(): string { - return `/@${this.pAcct}/${this.p.id}`; - }, urls(): string[] { if (this.p.text) { const ast = parse(this.p.text); diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue index f0921f91d..d39b2fbf9 100644 --- a/src/client/app/mobile/views/components/notification-preview.vue +++ b/src/client/app/mobile/views/components/notification-preview.vue @@ -3,7 +3,7 @@ <template v-if="notification.type == 'reaction'"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p><mk-reaction-icon :reaction="notification.reaction"/>{{ getUserName(notification.user) }}</p> + <p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user | userName }}</p> <p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p> </div> </template> @@ -11,7 +11,7 @@ <template v-if="notification.type == 'renote'"> <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:retweet%{{ getUserName(notification.note.user) }}</p> + <p>%fa:retweet%{{ notification.note.user | userName }}</p> <p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%</p> </div> </template> @@ -19,7 +19,7 @@ <template v-if="notification.type == 'quote'"> <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:quote-left%{{ getUserName(notification.note.user) }}</p> + <p>%fa:quote-left%{{ notification.note.user | userName }}</p> <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> </div> </template> @@ -27,14 +27,14 @@ <template v-if="notification.type == 'follow'"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:user-plus%{{ getUserName(notification.user) }}</p> + <p>%fa:user-plus%{{ notification.user | userName }}</p> </div> </template> <template v-if="notification.type == 'reply'"> <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:reply%{{ getUserName(notification.note.user) }}</p> + <p>%fa:reply%{{ notification.note.user | userName }}</p> <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> </div> </template> @@ -42,7 +42,7 @@ <template v-if="notification.type == 'mention'"> <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:at%{{ getUserName(notification.note.user) }}</p> + <p>%fa:at%{{ notification.note.user | userName }}</p> <p class="note-preview">{{ getNoteSummary(notification.note) }}</p> </div> </template> @@ -50,7 +50,7 @@ <template v-if="notification.type == 'poll_vote'"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:chart-pie%{{ getUserName(notification.user) }}</p> + <p>%fa:chart-pie%{{ notification.user | userName }}</p> <p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p> </div> </template> @@ -60,14 +60,12 @@ <script lang="ts"> import Vue from 'vue'; import getNoteSummary from '../../../../../renderers/get-note-summary'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['notification'], data() { return { - getNoteSummary, - getUserName + getNoteSummary }; } }); diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue index 4c98e1990..5456c2c17 100644 --- a/src/client/app/mobile/views/components/notification.vue +++ b/src/client/app/mobile/views/components/notification.vue @@ -2,13 +2,13 @@ <div class="mk-notification"> <div class="notification reaction" v-if="notification.type == 'reaction'"> <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`"> + <router-link class="avatar-anchor" :to="notification.user | userPage"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> <div class="text"> <p> <mk-reaction-icon :reaction="notification.reaction"/> - <router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link> + <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> </p> <router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`"> %fa:quote-left%{{ getNoteSummary(notification.note) }} @@ -19,13 +19,13 @@ <div class="notification renote" v-if="notification.type == 'renote'"> <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`"> + <router-link class="avatar-anchor" :to="notification.user | userPage"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> <div class="text"> <p> %fa:retweet% - <router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link> + <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> </p> <router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`"> %fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right% @@ -39,13 +39,13 @@ <div class="notification follow" v-if="notification.type == 'follow'"> <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`"> + <router-link class="avatar-anchor" :to="notification.user | userPage"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> <div class="text"> <p> %fa:user-plus% - <router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link> + <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> </p> </div> </div> @@ -60,13 +60,13 @@ <div class="notification poll_vote" v-if="notification.type == 'poll_vote'"> <mk-time :time="notification.createdAt"/> - <router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`"> + <router-link class="avatar-anchor" :to="notification.user | userPage"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> <div class="text"> <p> %fa:chart-pie% - <router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link> + <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link> </p> <router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`"> %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right% @@ -79,16 +79,12 @@ <script lang="ts"> import Vue from 'vue'; import getNoteSummary from '../../../../../renderers/get-note-summary'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['notification'], data() { return { - getNoteSummary, - getAcct, - getUserName + getNoteSummary }; } }); diff --git a/src/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue new file mode 100644 index 000000000..393fa9b83 --- /dev/null +++ b/src/client/app/mobile/views/components/post-card.vue @@ -0,0 +1,85 @@ +<template> +<div class="mk-note-card"> + <a :href="note | notePage"> + <header> + <img :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/><h3>{{ note.user | userName }}</h3> + </header> + <div> + {{ text }} + </div> + <mk-time :time="note.createdAt"/> + </a> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import summary from '../../../../../renderers/get-note-summary'; + +export default Vue.extend({ + props: ['note'], + computed: { + text(): string { + return summary(this.note); + } + } +}); +</script> + +<style lang="stylus" scoped> +.mk-note-card + display inline-block + width 150px + //height 120px + font-size 12px + background #fff + border-radius 4px + + > a + display block + color #2c3940 + + &:hover + text-decoration none + + > header + > img + position absolute + top 8px + left 8px + width 28px + height 28px + border-radius 6px + + > h3 + display inline-block + overflow hidden + width calc(100% - 45px) + margin 8px 0 0 42px + line-height 28px + white-space nowrap + text-overflow ellipsis + font-size 12px + + > div + padding 2px 8px 8px 8px + height 60px + overflow hidden + white-space normal + + &:after + content "" + display block + position absolute + top 40px + left 0 + width 100% + height 20px + background linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #fff 100%) + + > .mk-time + display inline-block + padding 8px + color #aaa + +</style> diff --git a/src/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue new file mode 100644 index 000000000..06f442d30 --- /dev/null +++ b/src/client/app/mobile/views/components/post-detail.sub.vue @@ -0,0 +1,103 @@ +<template> +<div class="root sub"> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="time" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </header> + <div class="body"> + <mk-sub-note-content class="text" :note="note"/> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'] +}); +</script> + +<style lang="stylus" scoped> +.root.sub + padding 8px + font-size 0.9em + background #fdfdfd + + @media (min-width 500px) + padding 12px + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 12px 0 0 + + > .avatar + display block + width 48px + height 48px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 60px) + + > header + display flex + margin-bottom 4px + white-space nowrap + + > .name + display block + margin 0 .5em 0 0 + padding 0 + overflow hidden + color #607073 + font-size 1em + font-weight 700 + text-align left + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 .5em 0 0 + color #d1d8da + + > .time + margin-left auto + color #b2b8bb + + > .body + + > .text + cursor default + margin 0 + padding 0 + font-size 1.1em + color #717171 + +</style> + diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue new file mode 100644 index 000000000..de32f0a74 --- /dev/null +++ b/src/client/app/mobile/views/components/post-detail.vue @@ -0,0 +1,444 @@ +<template> +<div class="mk-note-detail"> + <button + class="more" + v-if="p.reply && p.reply.replyId && context == null" + @click="fetchContext" + :disabled="fetchingContext" + > + <template v-if="!contextFetching">%fa:ellipsis-v%</template> + <template v-if="contextFetching">%fa:spinner .pulse%</template> + </button> + <div class="context"> + <x-sub v-for="note in context" :key="note.id" :note="note"/> + </div> + <div class="reply-to" v-if="p.reply"> + <x-sub :note="p.reply"/> + </div> + <div class="renote" v-if="isRenote"> + <p> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> + </router-link> + %fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote + </p> + </div> + <article> + <header> + <router-link class="avatar-anchor" :to="p.user | userPage"> + <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div> + <router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> + <span class="username">@{{ p.user | acct }}</span> + </div> + </header> + <div class="body"> + <mk-note-html v-if="p.text" :ast="p.text" :i="os.i" :class="$style.text"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> + </div> + <div class="media" v-if="p.media.length > 0"> + <mk-media-list :media-list="p.media"/> + </div> + <mk-poll v-if="p.poll" :note="p"/> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <div class="map" v-if="p.geo" ref="map"></div> + <div class="renote" v-if="p.renote"> + <mk-note-preview :note="p.renote"/> + </div> + </div> + <router-link class="time" :to="`/@${pAcct}/${p.id}`"> + <mk-time :time="p.createdAt" mode="detail"/> + </router-link> + <footer> + <mk-reactions-viewer :note="p"/> + <button @click="reply" title="%i18n:mobile.tags.mk-note-detail.reply%"> + %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> + </button> + <button @click="renote" title="Renote"> + %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> + </button> + <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-note-detail.reaction%"> + %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> + </button> + <button @click="menu" ref="menuButton"> + %fa:ellipsis-h% + </button> + </footer> + </article> + <div class="replies" v-if="!compact"> + <x-sub v-for="note in replies" :key="note.id" :note="note"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import parse from '../../../../../text/parse'; + +import MkNoteMenu from '../../../common/views/components/note-menu.vue'; +import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; +import XSub from './note-detail.sub.vue'; + +export default Vue.extend({ + components: { + XSub + }, + + props: { + note: { + type: Object, + required: true + }, + compact: { + default: false + } + }, + + data() { + return { + context: [], + contextFetching: false, + replies: [] + }; + }, + + computed: { + isRenote(): boolean { + return (this.note.renote && + this.note.text == null && + this.note.mediaIds.length == 0 && + this.note.poll == null); + }, + p(): any { + return this.isRenote ? this.note.renote : this.note; + }, + reactionsCount(): number { + return this.p.reactionCounts + ? Object.keys(this.p.reactionCounts) + .map(key => this.p.reactionCounts[key]) + .reduce((a, b) => a + b) + : 0; + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } + } + }, + + mounted() { + // Get replies + if (!this.compact) { + (this as any).api('notes/replies', { + noteId: this.p.id, + limit: 8 + }).then(replies => { + this.replies = replies; + }); + } + + // Draw map + if (this.p.geo) { + const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true; + if (shouldShowMap) { + (this as any).os.getGoogleMaps().then(maps => { + const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); + const map = new maps.Map(this.$refs.map, { + center: uluru, + zoom: 15 + }); + new maps.Marker({ + position: uluru, + map: map + }); + }); + } + } + }, + + methods: { + fetchContext() { + this.contextFetching = true; + + // Fetch context + (this as any).api('notes/context', { + noteId: this.p.replyId + }).then(context => { + this.contextFetching = false; + this.context = context.reverse(); + }); + }, + reply() { + (this as any).apis.post({ + reply: this.p + }); + }, + renote() { + (this as any).apis.post({ + renote: this.p + }); + }, + react() { + (this as any).os.new(MkReactionPicker, { + source: this.$refs.reactButton, + note: this.p, + compact: true + }); + }, + menu() { + (this as any).os.new(MkNoteMenu, { + source: this.$refs.menuButton, + note: this.p, + compact: true + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.mk-note-detail + overflow hidden + margin 0 auto + padding 0 + width 100% + text-align left + background #fff + border-radius 8px + box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2) + + > .fetching + padding 64px 0 + + > .more + display block + margin 0 + padding 10px 0 + width 100% + font-size 1em + text-align center + color #999 + cursor pointer + background #fafafa + outline none + border none + border-bottom solid 1px #eef0f2 + border-radius 6px 6px 0 0 + box-shadow none + + &:hover + background #f6f6f6 + + &:active + background #f0f0f0 + + &:disabled + color #ccc + + > .context + > * + border-bottom 1px solid #eef0f2 + + > .renote + color #9dbb00 + background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + + > p + margin 0 + padding 16px 32px + + .avatar-anchor + display inline-block + + .avatar + vertical-align bottom + min-width 28px + min-height 28px + max-width 28px + max-height 28px + margin 0 8px 0 0 + border-radius 6px + + [data-fa] + margin-right 4px + + .name + font-weight bold + + & + article + padding-top 8px + + > .reply-to + border-bottom 1px solid #eef0f2 + + > article + padding 14px 16px 9px 16px + + @media (min-width 500px) + padding 28px 32px 18px 32px + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > header + display flex + line-height 1.1 + + > .avatar-anchor + display block + padding 0 .5em 0 0 + + > .avatar + display block + width 54px + height 54px + margin 0 + border-radius 8px + vertical-align bottom + + @media (min-width 500px) + width 60px + height 60px + + > div + + > .name + display inline-block + margin .4em 0 + color #777 + font-size 16px + font-weight bold + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + display block + text-align left + margin 0 + color #ccc + + > .body + padding 8px 0 + + > .renote + margin 8px 0 + + > .mk-note-preview + padding 16px + border dashed 1px #c0dac6 + border-radius 8px + + > .location + margin 4px 0 + font-size 12px + color #ccc + + > .map + width 100% + height 200px + + &:empty + display none + + > .mk-url-preview + margin-top 8px + + > .media + > img + display block + max-width 100% + + > .tags + margin 4px 0 0 0 + + > * + display inline-block + margin 0 8px 0 0 + padding 2px 8px 2px 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + + > .time + font-size 16px + color #c0c0c0 + + > footer + font-size 1.2em + + > button + margin 0 + padding 8px + background transparent + border none + box-shadow none + font-size 1em + color #ddd + cursor pointer + + &:not(:last-child) + margin-right 28px + + &:hover + color #666 + + > .count + display inline + margin 0 0 0 8px + color #999 + + &.reacted + color $theme-color + + > .replies + > * + border-top 1px solid #eef0f2 + +</style> + +<style lang="stylus" module> +.text + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 16px + color #717171 + + @media (min-width 500px) + font-size 24px + +</style> diff --git a/src/client/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/post-preview.vue new file mode 100644 index 000000000..b9a6db315 --- /dev/null +++ b/src/client/app/mobile/views/components/post-preview.vue @@ -0,0 +1,100 @@ +<template> +<div class="mk-note-preview"> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span class="username">@{{ note.user | acct }}</span> + <router-link class="time" :to="note | notePage"> + <mk-time :time="note.createdAt"/> + </router-link> + </header> + <div class="body"> + <mk-sub-note-content class="text" :note="note"/> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: ['note'] +}); +</script> + +<style lang="stylus" scoped> +.mk-note-preview + margin 0 + padding 0 + font-size 0.9em + background #fff + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 12px 0 0 + + > .avatar + display block + width 48px + height 48px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 60px) + + > header + display flex + margin-bottom 4px + white-space nowrap + + > .name + display block + margin 0 .5em 0 0 + padding 0 + overflow hidden + color #607073 + font-size 1em + font-weight 700 + text-align left + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 .5em 0 0 + color #d1d8da + + > .time + margin-left auto + color #b2b8bb + + > .body + + > .text + cursor default + margin 0 + padding 0 + font-size 1.1em + color #717171 + +</style> diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue new file mode 100644 index 000000000..033de4f42 --- /dev/null +++ b/src/client/app/mobile/views/components/post.vue @@ -0,0 +1,523 @@ +<template> +<div class="note" :class="{ renote: isRenote }"> + <div class="reply-to" v-if="p.reply"> + <x-sub :note="p.reply"/> + </div> + <div class="renote" v-if="isRenote"> + <p> + <router-link class="avatar-anchor" :to="note.user | userPage"> + <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + </router-link> + %fa:retweet% + <span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span> + <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> + <span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span> + </p> + <mk-time :time="note.createdAt"/> + </div> + <article> + <router-link class="avatar-anchor" :to="p.user | userPage"> + <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/> + </router-link> + <div class="main"> + <header> + <router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> + <span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span> + <span class="username">@{{ p.user | acct }}</span> + <div class="info"> + <span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span> + <router-link class="created-at" :to="p | notePage"> + <mk-time :time="p.createdAt"/> + </router-link> + </div> + </header> + <div class="body"> + <p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p> + <div class="text"> + <a class="reply" v-if="p.reply"> + %fa:reply% + </a> + <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/> + <a class="rp" v-if="p.renote != null">RP:</a> + </div> + <div class="media" v-if="p.media.length > 0"> + <mk-media-list :media-list="p.media"/> + </div> + <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> + <div class="tags" v-if="p.tags && p.tags.length > 0"> + <router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link> + </div> + <mk-url-preview v-for="url in urls" :url="url" :key="url"/> + <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <div class="map" v-if="p.geo" ref="map"></div> + <span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> + <div class="renote" v-if="p.renote"> + <mk-note-preview :note="p.renote"/> + </div> + </div> + <footer> + <mk-reactions-viewer :note="p" ref="reactionsViewer"/> + <button @click="reply"> + %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> + </button> + <button @click="renote" title="Renote"> + %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> + </button> + <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton"> + %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> + </button> + <button class="menu" @click="menu" ref="menuButton"> + %fa:ellipsis-h% + </button> + </footer> + </div> + </article> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import parse from '../../../../../text/parse'; + +import MkNoteMenu from '../../../common/views/components/note-menu.vue'; +import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; +import XSub from './note.sub.vue'; + +export default Vue.extend({ + components: { + XSub + }, + + props: ['note'], + + data() { + return { + connection: null, + connectionId: null + }; + }, + + computed: { + isRenote(): boolean { + return (this.note.renote && + this.note.text == null && + this.note.mediaIds.length == 0 && + this.note.poll == null); + }, + p(): any { + return this.isRenote ? this.note.renote : this.note; + }, + reactionsCount(): number { + return this.p.reactionCounts + ? Object.keys(this.p.reactionCounts) + .map(key => this.p.reactionCounts[key]) + .reduce((a, b) => a + b) + : 0; + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } + } + }, + + created() { + if ((this as any).os.isSignedIn) { + this.connection = (this as any).os.stream.getConnection(); + this.connectionId = (this as any).os.stream.use(); + } + }, + + mounted() { + this.capture(true); + + if ((this as any).os.isSignedIn) { + this.connection.on('_connected_', this.onStreamConnected); + } + + // Draw map + if (this.p.geo) { + const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true; + if (shouldShowMap) { + (this as any).os.getGoogleMaps().then(maps => { + const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]); + const map = new maps.Map(this.$refs.map, { + center: uluru, + zoom: 15 + }); + new maps.Marker({ + position: uluru, + map: map + }); + }); + } + } + }, + + beforeDestroy() { + this.decapture(true); + + if ((this as any).os.isSignedIn) { + this.connection.off('_connected_', this.onStreamConnected); + (this as any).os.stream.dispose(this.connectionId); + } + }, + + methods: { + capture(withHandler = false) { + if ((this as any).os.isSignedIn) { + this.connection.send({ + type: 'capture', + id: this.p.id + }); + if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); + } + }, + decapture(withHandler = false) { + if ((this as any).os.isSignedIn) { + this.connection.send({ + type: 'decapture', + id: this.p.id + }); + if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated); + } + }, + onStreamConnected() { + this.capture(); + }, + onStreamNoteUpdated(data) { + const note = data.note; + if (note.id == this.note.id) { + this.$emit('update:note', note); + } else if (note.id == this.note.renoteId) { + this.note.renote = note; + } + }, + reply() { + (this as any).apis.post({ + reply: this.p + }); + }, + renote() { + (this as any).apis.post({ + renote: this.p + }); + }, + react() { + (this as any).os.new(MkReactionPicker, { + source: this.$refs.reactButton, + note: this.p, + compact: true + }); + }, + menu() { + (this as any).os.new(MkNoteMenu, { + source: this.$refs.menuButton, + note: this.p, + compact: true + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.note + font-size 12px + border-bottom solid 1px #eaeaea + + &:first-child + border-radius 8px 8px 0 0 + + > .renote + border-radius 8px 8px 0 0 + + &:last-of-type + border-bottom none + + @media (min-width 350px) + font-size 14px + + @media (min-width 500px) + font-size 16px + + > .renote + color #9dbb00 + background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + + > p + margin 0 + padding 8px 16px + line-height 28px + + @media (min-width 500px) + padding 16px + + .avatar-anchor + display inline-block + + .avatar + vertical-align bottom + width 28px + height 28px + margin 0 8px 0 0 + border-radius 6px + + [data-fa] + margin-right 4px + + .name + font-weight bold + + > .mk-time + position absolute + top 8px + right 16px + font-size 0.9em + line-height 28px + + @media (min-width 500px) + top 16px + + & + article + padding-top 8px + + > .reply-to + background rgba(0, 0, 0, 0.0125) + + > .mk-note-preview + background transparent + + > article + padding 14px 16px 9px 16px + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 10px 8px 0 + position -webkit-sticky + position sticky + top 62px + + @media (min-width 500px) + margin-right 16px + + > .avatar + display block + width 48px + height 48px + margin 0 + border-radius 6px + vertical-align bottom + + @media (min-width 500px) + width 58px + height 58px + border-radius 8px + + > .main + float left + width calc(100% - 58px) + + @media (min-width 500px) + width calc(100% - 74px) + + > header + display flex + align-items center + white-space nowrap + + @media (min-width 500px) + margin-bottom 2px + + > .name + display block + margin 0 0.5em 0 0 + padding 0 + overflow hidden + color #627079 + font-size 1em + font-weight bold + text-decoration none + text-overflow ellipsis + + &:hover + text-decoration underline + + > .is-bot + margin 0 0.5em 0 0 + padding 1px 6px + font-size 12px + color #aaa + border solid 1px #ddd + border-radius 3px + + > .username + margin 0 0.5em 0 0 + color #ccc + + > .info + margin-left auto + font-size 0.9em + + > .mobile + margin-right 6px + color #c0c0c0 + + > .created-at + color #c0c0c0 + + > .body + + > .text + display block + margin 0 + padding 0 + overflow-wrap break-word + font-size 1.1em + color #717171 + + >>> .quote + margin 8px + padding 6px 12px + color #aaa + border-left solid 3px #eee + + > .reply + margin-right 8px + color #717171 + + > .rp + margin-left 4px + font-style oblique + color #a0bf46 + + [data-is-me]:after + content "you" + padding 0 4px + margin-left 4px + font-size 80% + color $theme-color-foreground + background $theme-color + border-radius 4px + + .mk-url-preview + margin-top 8px + + > .channel + margin 0 + + > .tags + margin 4px 0 0 0 + + > * + display inline-block + margin 0 8px 0 0 + padding 2px 8px 2px 16px + font-size 90% + color #8d969e + background #edf0f3 + border-radius 4px + + &:before + content "" + display block + position absolute + top 0 + bottom 0 + left 4px + width 8px + height 8px + margin auto 0 + background #fff + border-radius 100% + + > .media + > img + display block + max-width 100% + + > .location + margin 4px 0 + font-size 12px + color #ccc + + > .map + width 100% + height 200px + + &:empty + display none + + > .app + font-size 12px + color #ccc + + > .mk-poll + font-size 80% + + > .renote + margin 8px 0 + + > .mk-note-preview + padding 16px + border dashed 1px #c0dac6 + border-radius 8px + + > footer + > button + margin 0 + padding 8px + background transparent + border none + box-shadow none + font-size 1em + color #ddd + cursor pointer + + &:not(:last-child) + margin-right 28px + + &:hover + color #666 + + > .count + display inline + margin 0 0 0 8px + color #999 + + &.reacted + color $theme-color + + &.menu + @media (max-width 350px) + display none + +</style> + +<style lang="stylus" module> +.text + code + padding 4px 8px + margin 0 0.5em + font-size 80% + color #525252 + background #f8f8f8 + border-radius 2px + + pre > code + padding 16px + margin 0 +</style> diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue index f664341cd..f1b24bf2d 100644 --- a/src/client/app/mobile/views/components/ui.header.vue +++ b/src/client/app/mobile/views/components/ui.header.vue @@ -3,7 +3,7 @@ <mk-special-message/> <div class="main" ref="main"> <div class="backdrop"></div> - <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p> + <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i | userName }}</b>さん</p> <div class="content" ref="mainContainer"> <button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button> <template v-if="hasUnreadNotifications || hasUnreadMessagingMessages || hasGameInvitations">%fa:circle%</template> @@ -19,15 +19,9 @@ <script lang="ts"> import Vue from 'vue'; import * as anime from 'animejs'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['func'], - computed: { - name() { - return getUserName(this.os.i); - } - }, data() { return { hasUnreadNotifications: false, diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index 61dd8ca9d..764f9374e 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -11,7 +11,7 @@ <div class="body" v-if="isOpen"> <router-link class="me" v-if="os.isSignedIn" :to="`/@${os.i.username}`"> <img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/> - <p class="name">{{ name }}</p> + <p class="name">{{ os.i | userName }}</p> </router-link> <div class="links"> <ul> @@ -39,16 +39,10 @@ <script lang="ts"> import Vue from 'vue'; -import { docsUrl, chUrl, lang } from '../../../config'; -import getUserName from '../../../../../renderers/get-user-name'; +import { docsUrl, lang } from '../../../config'; export default Vue.extend({ props: ['isOpen'], - computed: { - name() { - return getUserName(this.os.i); - } - }, data() { return { hasUnreadNotifications: false, @@ -56,8 +50,7 @@ export default Vue.extend({ hasGameInvitations: false, connection: null, connectionId: null, - aboutUrl: `${docsUrl}/${lang}/about`, - chUrl + aboutUrl: `${docsUrl}/${lang}/about` }; }, mounted() { diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue index e8698a62f..432560a54 100644 --- a/src/client/app/mobile/views/components/user-card.vue +++ b/src/client/app/mobile/views/components/user-card.vue @@ -1,31 +1,21 @@ <template> <div class="mk-user-card"> <header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"> - <a :href="`/@${acct}`"> + <a :href="user | userPage"> <img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/> </a> </header> - <a class="name" :href="`/@${acct}`" target="_blank">{{ name }}</a> - <p class="username">@{{ acct }}</p> + <a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a> + <p class="username">@{{ user | acct }}</p> <mk-follow-button :user="user"/> </div> </template> <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ - props: ['user'], - computed: { - acct() { - return getAcct(this.user); - }, - name() { - return getUserName(this.user); - } - } + props: ['user'] }); </script> diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue index 72a6bcf8a..23a83b5e3 100644 --- a/src/client/app/mobile/views/components/user-preview.vue +++ b/src/client/app/mobile/views/components/user-preview.vue @@ -1,12 +1,12 @@ <template> <div class="mk-user-preview"> - <router-link class="avatar-anchor" :to="`/@${acct}`"> + <router-link class="avatar-anchor" :to="user | userPage"> <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`">{{ name }}</router-link> - <span class="username">@{{ acct }}</span> + <router-link class="name" :to="user | userPage">{{ user | userName }}</router-link> + <span class="username">@{{ user | acct }}</span> </header> <div class="body"> <div class="description">{{ user.description }}</div> @@ -17,19 +17,9 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ - props: ['user'], - computed: { - acct() { - return getAcct(this.user); - }, - name() { - return getUserName(this.user); - } - } + props: ['user'] }); </script> diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue index cc2442e47..d0dcc117c 100644 --- a/src/client/app/mobile/views/pages/following.vue +++ b/src/client/app/mobile/views/pages/following.vue @@ -20,7 +20,6 @@ import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; import parseAcct from '../../../../../acct/parse'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -30,8 +29,8 @@ export default Vue.extend({ }; }, computed: { - name() { - return getUserName(this.user); + name(): string { + return Vue.filter('userName')(this.user); } }, watch: { diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue index ae6662dc0..3b6fb11db 100644 --- a/src/client/app/mobile/views/pages/messaging-room.vue +++ b/src/client/app/mobile/views/pages/messaging-room.vue @@ -1,7 +1,7 @@ <template> <mk-ui> <span slot="header"> - <template v-if="user">%fa:R comments%{{ name }}</template> + <template v-if="user">%fa:R comments%{{ user | userName }}</template> <template v-else><mk-ellipsis/></template> </span> <mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/> @@ -11,7 +11,6 @@ <script lang="ts"> import Vue from 'vue'; import parseAcct from '../../../../../acct/parse'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -20,11 +19,6 @@ export default Vue.extend({ user: null }; }, - computed: { - name() { - return getUserName(this.user); - } - }, watch: { $route: 'fetch' }, @@ -39,7 +33,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${this.name} | Misskey`; + document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${Vue.filter('userName')(this.user)} | Misskey`; }); } } diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index 58a9d4e37..8d248f5cb 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -20,7 +20,6 @@ <script lang="ts"> import Vue from 'vue'; import { version, codename } from '../../../config'; -import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -30,8 +29,8 @@ export default Vue.extend({ }; }, computed: { - name() { - return getUserName(this.os.i); + name(): string { + return Vue.filter('userName')((this as any).os.i); } }, mounted() { diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index aac8b628b..fb9220ee8 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -1,6 +1,6 @@ <template> <mk-ui> - <span slot="header" v-if="!fetching">%fa:user% {{ user }}</span> + <span slot="header" v-if="!fetching">%fa:user% {{ user | userName }}</span> <main v-if="!fetching"> <header> <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div> @@ -12,8 +12,8 @@ <mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/> </div> <div class="title"> - <h1>{{ getUserName(user) }}</h1> - <span class="username">@{{ getAcct(user) }}</span> + <h1>{{ user | userName }}</h1> + <span class="username">@{{ user | acct }}</span> <span class="followed" v-if="user.isFollowed">%i18n:mobile.tags.mk-user.follows-you%</span> </div> <div class="description">{{ user.description }}</div> @@ -61,8 +61,6 @@ import Vue from 'vue'; import * as age from 's-age'; import parseAcct from '../../../../../acct/parse'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; import Progress from '../../../common/scripts/loading'; import XHome from './user/home.vue'; @@ -74,9 +72,7 @@ export default Vue.extend({ return { fetching: true, user: null, - page: 'home', - getAcct, - getUserName + page: 'home' }; }, computed: { @@ -102,7 +98,7 @@ export default Vue.extend({ this.fetching = false; Progress.done(); - document.title = this.getUserName(this.user) + ' | Misskey'; + document.title = Vue.filter('userName')(this.user) + ' | Misskey'; }); } } diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue index 1b128e2f2..2841c0d63 100644 --- a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue +++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue @@ -2,8 +2,8 @@ <div class="root followers-you-know"> <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p> <div v-if="!fetching && users.length > 0"> - <a v-for="user in users" :key="user.id" :href="`/@${getAcct(user)}`"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)"/> + <a v-for="user in users" :key="user.id" :href="user | userPage"> + <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName"/> </a> </div> <p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p> @@ -12,8 +12,6 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../../acct/render'; -import getUserName from '../../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['user'], @@ -23,14 +21,6 @@ export default Vue.extend({ users: [] }; }, - computed: { - name() { - return getUserName(this.user); - } - }, - methods: { - getAcct - }, mounted() { (this as any).api('users/followers', { userId: this.user.id, diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue index bd257a3ff..502f886ce 100644 --- a/src/client/app/mobile/views/widgets/profile.vue +++ b/src/client/app/mobile/views/widgets/profile.vue @@ -8,23 +8,16 @@ :src="`${os.i.avatarUrl}?thumbnail&size=96`" alt="avatar" /> - <router-link :class="$style.name" :to="`/@${os.i.username}`">{{ name }}</router-link> + <router-link :class="$style.name" :to="os.i | userPage">{{ os.i | userName }}</router-link> </mk-widget-container> </div> </template> <script lang="ts"> import define from '../../../common/define-widget'; -import getUserName from '../../../../../renderers/get-user-name'; export default define({ name: 'profile' -}).extend({ - computed: { - name() { - return getUserName(this.os.i); - } - } }); </script>