diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2018-03-31 20:50:40 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-03-31 20:50:40 +0900 |
| commit | eafb0f61ef6465697a0fbd9b931fc306417cd2a1 (patch) | |
| tree | 7d6c502c1e2c61eb3327f678f766f23bda10c1e7 /src/client | |
| parent | デフォルトでドライブ容量は128MiBにした (diff) | |
| parent | Implement remote status retrieval (diff) | |
| download | sharkey-eafb0f61ef6465697a0fbd9b931fc306417cd2a1.tar.gz sharkey-eafb0f61ef6465697a0fbd9b931fc306417cd2a1.tar.bz2 sharkey-eafb0f61ef6465697a0fbd9b931fc306417cd2a1.zip | |
Merge pull request #1341 from akihikodaki/github
Implement remote status retrieval
Diffstat (limited to 'src/client')
14 files changed, 219 insertions, 278 deletions
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index b58ba37ecb..8c10bdee28 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -4,7 +4,7 @@ import signin from './signin.vue'; import signup from './signup.vue'; import forkit from './forkit.vue'; import nav from './nav.vue'; -import postHtml from './post-html'; +import postHtml from './post-html.vue'; import poll from './poll.vue'; import pollEditor from './poll-editor.vue'; import reactionIcon from './reaction-icon.vue'; diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue index 94f87fd709..25ceab85a1 100644 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -4,13 +4,13 @@ <img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/> </router-link> <div class="content"> - <div class="balloon" :data-no-text="message.text == null"> + <div class="balloon" :data-no-text="message.textHtml == null"> <p class="read" v-if="isMe && message.isRead">%i18n:common.tags.mk-messaging-message.is-read%</p> <button class="delete-button" v-if="isMe" title="%i18n:common.delete%"> <img src="/assets/desktop/messaging/delete.png" alt="Delete"/> </button> <div class="content" v-if="!message.isDeleted"> - <mk-post-html class="text" v-if="message.ast" :ast="message.ast" :i="os.i"/> + <mk-post-html class="text" v-if="message.textHtml" ref="text" :html="message.textHtml" :i="os.i"/> <div class="file" v-if="message.file"> <a :href="message.file.url" target="_blank" :title="message.file.name"> <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/> @@ -38,21 +38,32 @@ import getAcct from '../../../../../common/user/get-acct'; export default Vue.extend({ props: ['message'], + data() { + return { + urls: [] + }; + }, computed: { acct() { return getAcct(this.message.user); }, isMe(): boolean { return this.message.userId == (this as any).os.i.id; - }, - urls(): string[] { - if (this.message.ast) { - return this.message.ast - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => t.url); - } else { - return null; - } + } + }, + watch: { + message: { + handler(newMessage, oldMessage) { + if (!oldMessage || newMessage.textHtml !== oldMessage.textHtml) { + this.$nextTick(() => { + const elements = this.$refs.text.$el.getElementsByTagName('a'); + + this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin) + .map(({ href }) => href); + }); + } + }, + immediate: true } } }); diff --git a/src/client/app/common/views/components/post-html.ts b/src/client/app/common/views/components/post-html.ts deleted file mode 100644 index 39d783aac5..0000000000 --- a/src/client/app/common/views/components/post-html.ts +++ /dev/null @@ -1,141 +0,0 @@ -import Vue from 'vue'; -import * as emojilib from 'emojilib'; -import getAcct from '../../../../../common/user/get-acct'; -import { url } from '../../../config'; -import MkUrl from './url.vue'; - -const flatten = list => list.reduce( - (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] -); - -export default Vue.component('mk-post-html', { - props: { - ast: { - type: Array, - required: true - }, - shouldBreak: { - type: Boolean, - default: true - }, - i: { - type: Object, - default: null - } - }, - render(createElement) { - const els = flatten((this as any).ast.map(token => { - switch (token.type) { - case 'text': - const text = token.content.replace(/(\r\n|\n|\r)/g, '\n'); - - if ((this as any).shouldBreak) { - const x = text.split('\n') - .map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]); - x[x.length - 1].pop(); - return x; - } else { - return createElement('span', text.replace(/\n/g, ' ')); - } - - case 'bold': - return createElement('strong', token.bold); - - case 'url': - return createElement(MkUrl, { - props: { - url: token.content, - target: '_blank' - } - }); - - case 'link': - return createElement('a', { - attrs: { - class: 'link', - href: token.url, - target: '_blank', - title: token.url - } - }, token.title); - - case 'mention': - return (createElement as any)('a', { - attrs: { - href: `${url}/@${getAcct(token)}`, - target: '_blank', - dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token) - }, - directives: [{ - name: 'user-preview', - value: token.content - }] - }, token.content); - - case 'hashtag': - return createElement('a', { - attrs: { - href: `${url}/search?q=${token.content}`, - target: '_blank' - } - }, token.content); - - case 'code': - return createElement('pre', [ - createElement('code', { - domProps: { - innerHTML: token.html - } - }) - ]); - - case 'inline-code': - return createElement('code', { - domProps: { - innerHTML: token.html - } - }); - - case 'quote': - const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n'); - - if ((this as any).shouldBreak) { - const x = text2.split('\n') - .map(t => [createElement('span', t), createElement('br')]); - x[x.length - 1].pop(); - return createElement('div', { - attrs: { - class: 'quote' - } - }, x); - } else { - return createElement('span', { - attrs: { - class: 'quote' - } - }, text2.replace(/\n/g, ' ')); - } - - case 'emoji': - const emoji = emojilib.lib[token.emoji]; - return createElement('span', emoji ? emoji.char : token.content); - - default: - console.log('unknown ast type:', token.type); - } - })); - - const _els = []; - els.forEach((el, i) => { - if (el.tag == 'br') { - if (els[i - 1].tag != 'div') { - _els.push(el); - } - } else { - _els.push(el); - } - }); - - return createElement('span', _els); - } -}); diff --git a/src/client/app/common/views/components/post-html.vue b/src/client/app/common/views/components/post-html.vue new file mode 100644 index 0000000000..1c949052b9 --- /dev/null +++ b/src/client/app/common/views/components/post-html.vue @@ -0,0 +1,103 @@ +<template><div class="mk-post-html" v-html="html"></div></template> + +<script lang="ts"> +import Vue from 'vue'; +import getAcct from '../../../../../common/user/get-acct'; +import { url } from '../../../config'; + +function markUrl(a) { + while (a.firstChild) { + a.removeChild(a.firstChild); + } + + const schema = document.createElement('span'); + const delimiter = document.createTextNode('//'); + const host = document.createElement('span'); + const pathname = document.createElement('span'); + const query = document.createElement('span'); + const hash = document.createElement('span'); + + schema.className = 'schema'; + schema.textContent = a.protocol; + + host.className = 'host'; + host.textContent = a.host; + + pathname.className = 'pathname'; + pathname.textContent = a.pathname; + + query.className = 'query'; + query.textContent = a.search; + + hash.className = 'hash'; + hash.textContent = a.hash; + + a.appendChild(schema); + a.appendChild(delimiter); + a.appendChild(host); + a.appendChild(pathname); + a.appendChild(query); + a.appendChild(hash); +} + +function markMe(me, a) { + a.setAttribute("data-is-me", me && `${url}/@${getAcct(me)}` == a.href); +} + +function markTarget(a) { + a.setAttribute("target", "_blank"); +} + +export default Vue.component('mk-post-html', { + props: { + html: { + type: String, + required: true + }, + i: { + type: Object, + default: null + } + }, + watch { + html: { + handler() { + this.$nextTick(() => [].forEach.call(this.$el.getElementsByTagName('a'), a => { + if (a.href === a.textContent) { + markUrl(a); + } else { + markMe((this as any).i, a); + } + + markTarget(a); + })); + }, + immediate: true, + } + } +}); +</script> + +<style lang="stylus"> +.mk-post-html + a + word-break break-all + + > .schema + opacity 0.5 + + > .host + font-weight bold + + > .pathname + opacity 0.8 + + > .query + opacity 0.5 + + > .hash + font-style italic + + p + margin 0 +</style> diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue deleted file mode 100644 index 14d4fc82f3..0000000000 --- a/src/client/app/common/views/components/url.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> -<a class="mk-url" :href="url" :target="target"> - <span class="schema">{{ schema }}//</span> - <span class="hostname">{{ hostname }}</span> - <span class="port" v-if="port != ''">:{{ port }}</span> - <span class="pathname" v-if="pathname != ''">{{ pathname }}</span> - <span class="query">{{ query }}</span> - <span class="hash">{{ hash }}</span> - %fa:external-link-square-alt% -</a> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: ['url', 'target'], - data() { - return { - schema: null, - hostname: null, - port: null, - pathname: null, - query: null, - hash: null - }; - }, - created() { - const url = new URL(this.url); - - this.schema = url.protocol; - this.hostname = url.hostname; - this.port = url.port; - this.pathname = url.pathname; - this.query = url.search; - this.hash = url.hash; - } -}); -</script> - -<style lang="stylus" scoped> -.mk-url - word-break break-all - - > [data-fa] - padding-left 2px - font-size .9em - font-weight 400 - font-style normal - - > .schema - opacity 0.5 - - > .hostname - font-weight bold - - > .pathname - opacity 0.8 - - > .query - opacity 0.5 - - > .hash - font-style italic - -</style> diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue index 8f6199732a..f379029f9f 100644 --- a/src/client/app/common/views/components/welcome-timeline.vue +++ b/src/client/app/common/views/components/welcome-timeline.vue @@ -15,7 +15,7 @@ </div> </header> <div class="text"> - <mk-post-html :ast="post.ast"/> + <mk-post-html :html="post.textHtml"/> </div> </div> </div> diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue index 35377e7c24..b6148d9b28 100644 --- a/src/client/app/desktop/views/components/post-detail.sub.vue +++ b/src/client/app/desktop/views/components/post-detail.sub.vue @@ -16,8 +16,8 @@ </div> </header> <div class="body"> - <mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i" :class="$style.text"/> - <div class="media" v-if="post.media"> + <mk-post-html v-if="post.textHtml" :html="post.textHtml" :i="os.i" :class="$style.text"/> + <div class="media" v-if="post.media > 0"> <mk-media-list :media-list="post.media"/> </div> </div> diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue index 5c7a7dfdbe..e75ebe34b4 100644 --- a/src/client/app/desktop/views/components/post-detail.vue +++ b/src/client/app/desktop/views/components/post-detail.vue @@ -38,8 +38,8 @@ </router-link> </header> <div class="body"> - <mk-post-html :class="$style.text" v-if="p.ast" :ast="p.ast" :i="os.i"/> - <div class="media" v-if="p.media"> + <mk-post-html :class="$style.text" v-if="p.text" ref="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" :post="p"/> @@ -109,6 +109,7 @@ export default Vue.extend({ context: [], contextFetching: false, replies: [], + urls: [] }; }, computed: { @@ -130,15 +131,6 @@ export default Vue.extend({ }, title(): string { return dateStringify(this.p.createdAt); - }, - urls(): string[] { - if (this.p.ast) { - return this.p.ast - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => t.url); - } else { - return null; - } } }, mounted() { @@ -170,6 +162,21 @@ export default Vue.extend({ } } }, + watch: { + post: { + handler(newPost, oldPost) { + if (!oldPost || newPost.text !== oldPost.text) { + this.$nextTick(() => { + const elements = this.$refs.text.$el.getElementsByTagName('a'); + + this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin) + .map(({ href }) => href); + }); + } + }, + immediate: true + } + }, methods: { fetchContext() { this.contextFetching = true; diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue index 37c6e63043..f3566c81bf 100644 --- a/src/client/app/desktop/views/components/posts.post.vue +++ b/src/client/app/desktop/views/components/posts.post.vue @@ -38,10 +38,10 @@ </p> <div class="text"> <a class="reply" v-if="p.reply">%fa:reply%</a> - <mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/> + <mk-post-html v-if="p.textHtml" ref="text" :html="p.textHtml" :i="os.i" :class="$style.text"/> <a class="rp" v-if="p.repost">RP:</a> </div> - <div class="media" v-if="p.media"> + <div class="media" v-if="p.media.length > 0"> <mk-media-list :media-list="p.media"/> </div> <mk-poll v-if="p.poll" :post="p" ref="pollViewer"/> @@ -112,7 +112,8 @@ export default Vue.extend({ return { isDetailOpened: false, connection: null, - connectionId: null + connectionId: null, + urls: [] }; }, computed: { @@ -140,15 +141,6 @@ export default Vue.extend({ }, url(): string { return `/@${this.acct}/${this.p.id}`; - }, - urls(): string[] { - if (this.p.ast) { - return this.p.ast - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => t.url); - } else { - return null; - } } }, created() { @@ -190,6 +182,21 @@ export default Vue.extend({ (this as any).os.stream.dispose(this.connectionId); } }, + watch: { + post: { + handler(newPost, oldPost) { + if (!oldPost || newPost.textHtml !== oldPost.textHtml) { + this.$nextTick(() => { + const elements = this.$refs.text.$el.getElementsByTagName('a'); + + this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin) + .map(({ href }) => href); + }); + } + }, + immediate: true + } + }, methods: { capture(withHandler = false) { if ((this as any).os.isSignedIn) { @@ -450,7 +457,7 @@ export default Vue.extend({ font-size 1.1em color #717171 - >>> .quote + >>> blockquote margin 8px padding 6px 12px color #aaa diff --git a/src/client/app/desktop/views/components/sub-post-content.vue b/src/client/app/desktop/views/components/sub-post-content.vue index a79e5e0a4e..58c81e7552 100644 --- a/src/client/app/desktop/views/components/sub-post-content.vue +++ b/src/client/app/desktop/views/components/sub-post-content.vue @@ -2,10 +2,10 @@ <div class="mk-sub-post-content"> <div class="body"> <a class="reply" v-if="post.replyId">%fa:reply%</a> - <mk-post-html :ast="post.ast" :i="os.i"/> + <mk-post-html ref="text" :html="post.textHtml" :i="os.i"/> <a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">RP: ...</a> </div> - <details v-if="post.media"> + <details v-if="post.media.length > 0"> <summary>({{ post.media.length }}つのメディア)</summary> <mk-media-list :media-list="post.media"/> </details> diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue index f0af1a61aa..77a73426f2 100644 --- a/src/client/app/mobile/views/components/post-detail.vue +++ b/src/client/app/mobile/views/components/post-detail.vue @@ -38,11 +38,11 @@ </div> </header> <div class="body"> - <mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/> + <mk-post-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"> + <div class="media" v-if="p.media.length > 0"> <mk-media-list :media-list="p.media"/> </div> <mk-poll v-if="p.poll" :post="p"/> @@ -103,6 +103,7 @@ export default Vue.extend({ context: [], contextFetching: false, replies: [], + urls: [] }; }, computed: { @@ -127,15 +128,6 @@ export default Vue.extend({ .map(key => this.p.reactionCounts[key]) .reduce((a, b) => a + b) : 0; - }, - urls(): string[] { - if (this.p.ast) { - return this.p.ast - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => t.url); - } else { - return null; - } } }, mounted() { @@ -167,6 +159,21 @@ export default Vue.extend({ } } }, + watch: { + post: { + handler(newPost, oldPost) { + if (!oldPost || newPost.text !== oldPost.text) { + this.$nextTick(() => { + const elements = this.$refs.text.$el.getElementsByTagName('a'); + + this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin) + .map(({ href }) => href); + }); + } + }, + immediate: true + } + }, methods: { fetchContext() { this.contextFetching = true; diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue index a01eb7669e..96ec9632f1 100644 --- a/src/client/app/mobile/views/components/post.vue +++ b/src/client/app/mobile/views/components/post.vue @@ -37,10 +37,10 @@ <a class="reply" v-if="p.reply"> %fa:reply% </a> - <mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/> + <mk-post-html v-if="p.text" ref="text" :text="p.text" :i="os.i" :class="$style.text"/> <a class="rp" v-if="p.repost != null">RP:</a> </div> - <div class="media" v-if="p.media"> + <div class="media" v-if="p.media.length > 0"> <mk-media-list :media-list="p.media"/> </div> <mk-poll v-if="p.poll" :post="p" ref="pollViewer"/> @@ -90,7 +90,8 @@ export default Vue.extend({ data() { return { connection: null, - connectionId: null + connectionId: null, + urls: [] }; }, computed: { @@ -118,15 +119,6 @@ export default Vue.extend({ }, url(): string { return `/@${this.pAcct}/${this.p.id}`; - }, - urls(): string[] { - if (this.p.ast) { - return this.p.ast - .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) - .map(t => t.url); - } else { - return null; - } } }, created() { @@ -168,6 +160,21 @@ export default Vue.extend({ (this as any).os.stream.dispose(this.connectionId); } }, + watch: { + post: { + handler(newPost, oldPost) { + if (!oldPost || newPost.text !== oldPost.text) { + this.$nextTick(() => { + const elements = this.$refs.text.$el.getElementsByTagName('a'); + + this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin) + .map(({ href }) => href); + }); + } + }, + immediate: true + } + }, methods: { capture(withHandler = false) { if ((this as any).os.isSignedIn) { @@ -389,7 +396,7 @@ export default Vue.extend({ font-size 1.1em color #717171 - >>> .quote + >>> blockquote margin 8px padding 6px 12px color #aaa diff --git a/src/client/app/mobile/views/components/sub-post-content.vue b/src/client/app/mobile/views/components/sub-post-content.vue index b95883de77..955bb406b4 100644 --- a/src/client/app/mobile/views/components/sub-post-content.vue +++ b/src/client/app/mobile/views/components/sub-post-content.vue @@ -2,10 +2,10 @@ <div class="mk-sub-post-content"> <div class="body"> <a class="reply" v-if="post.replyId">%fa:reply%</a> - <mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i"/> + <mk-post-html v-if="post.text" :ast="post.text" :i="os.i"/> <a class="rp" v-if="post.repostId">RP: ...</a> </div> - <details v-if="post.media"> + <details v-if="post.media.length > 0"> <summary>({{ post.media.length }}個のメディア)</summary> <mk-media-list :media-list="post.media"/> </details> diff --git a/src/client/docs/api/entities/post.yaml b/src/client/docs/api/entities/post.yaml index 74d7973e38..7077700129 100644 --- a/src/client/docs/api/entities/post.yaml +++ b/src/client/docs/api/entities/post.yaml @@ -27,14 +27,20 @@ props: type: "string" optional: true desc: - ja: "投稿の本文" - en: "The text of this post" + ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)" + en: "The text of this post (in Markdown like format if local)" + - name: "textHtml" + type: "string" + optional: true + desc: + ja: "投稿の本文 (HTML) (投稿時は無視)" + en: "The text of this post (in HTML. Ignored when posting.)" - name: "mediaIds" type: "id(DriveFile)[]" optional: true desc: - ja: "添付されているメディアのID" - en: "The IDs of the attached media" + ja: "添付されているメディアのID (なければレスポンスでは空配列)" + en: "The IDs of the attached media (empty array for response if no media is attached)" - name: "media" type: "entity(DriveFile)[]" optional: true |