diff options
Diffstat (limited to 'src/client')
14 files changed, 126 insertions, 65 deletions
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..91af26bffe 100644 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -10,7 +10,7 @@ <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.text" ref="text" :text="message.text" :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"/> @@ -35,19 +35,25 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../common/user/get-acct'; +import parse from '../../../../../common/text/parse'; export default Vue.extend({ - props: ['message'], + props: { + message: { + required: true + } + }, computed: { - acct() { + acct(): string { 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 + if (this.message.text) { + const ast = parse(this.message.text); + return ast .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) .map(t => t.url); } else { diff --git a/src/client/app/common/views/components/post-html.ts b/src/client/app/common/views/components/post-html.ts index 39d783aac5..c5c3b72758 100644 --- a/src/client/app/common/views/components/post-html.ts +++ b/src/client/app/common/views/components/post-html.ts @@ -1,5 +1,6 @@ import Vue from 'vue'; import * as emojilib from 'emojilib'; +import parse from '../../../../../common/text/parse'; import getAcct from '../../../../../common/user/get-acct'; import { url } from '../../../config'; import MkUrl from './url.vue'; @@ -10,10 +11,14 @@ const flatten = list => list.reduce( export default Vue.component('mk-post-html', { props: { - ast: { - type: Array, + text: { + type: String, required: true }, + ast: { + type: [], + required: false + }, shouldBreak: { type: Boolean, default: true @@ -23,13 +28,24 @@ export default Vue.component('mk-post-html', { default: null } }, + render(createElement) { - const els = flatten((this as any).ast.map(token => { + let ast; + + if (this.ast == null) { + // Parse text to ast + ast = parse(this.text); + } else { + ast = this.ast; + } + + // Parse ast to DOM + const els = flatten(ast.map(token => { switch (token.type) { case 'text': const text = token.content.replace(/(\r\n|\n|\r)/g, '\n'); - if ((this as any).shouldBreak) { + if (this.shouldBreak) { const x = text.split('\n') .map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]); x[x.length - 1].pop(); @@ -99,7 +115,7 @@ export default Vue.component('mk-post-html', { case 'quote': const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n'); - if ((this as any).shouldBreak) { + if (this.shouldBreak) { const x = text2.split('\n') .map(t => [createElement('span', t), createElement('br')]); x[x.length - 1].pop(); diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue index 14d4fc82f3..e6ffe4466d 100644 --- a/src/client/app/common/views/components/url.vue +++ b/src/client/app/common/views/components/url.vue @@ -12,7 +12,6 @@ <script lang="ts"> import Vue from 'vue'; - export default Vue.extend({ props: ['url', 'target'], data() { @@ -27,7 +26,6 @@ export default Vue.extend({ }, created() { const url = new URL(this.url); - this.schema = url.protocol; this.hostname = url.hostname; this.port = url.port; @@ -41,26 +39,19 @@ export default Vue.extend({ <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..09b090bdc1 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 :text="post.text"/> </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..1d5649cf92 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.text" :text="post.text" :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..d6481e13d0 100644 --- a/src/client/app/desktop/views/components/post-detail.vue +++ b/src/client/app/desktop/views/components/post-detail.vue @@ -27,19 +27,19 @@ </p> </div> <article> - <router-link class="avatar-anchor" :to="`/@${acct}`"> + <router-link class="avatar-anchor" :to="`/@${pAcct}`"> <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="`/@${acct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="time" :to="`/@${acct}/${p.id}`"> + <router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link> + <span class="username">@{{ pAcct }}</span> + <router-link class="time" :to="`/@${pAcct}/${p.id}`"> <mk-time :time="p.createdAt"/> </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" :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"/> @@ -79,6 +79,7 @@ import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; import getAcct from '../../../../../common/user/get-acct'; +import parse from '../../../../../common/text/parse'; import MkPostFormWindow from './post-form-window.vue'; import MkRepostFormWindow from './repost-form-window.vue'; @@ -90,6 +91,7 @@ export default Vue.extend({ components: { XSub }, + props: { post: { type: Object, @@ -99,18 +101,15 @@ export default Vue.extend({ default: false } }, - computed: { - acct() { - return getAcct(this.post.user); - } - }, + data() { return { context: [], contextFetching: false, - replies: [], + replies: [] }; }, + computed: { isRepost(): boolean { return (this.post.repost && @@ -131,9 +130,16 @@ export default Vue.extend({ title(): string { return dateStringify(this.p.createdAt); }, + acct(): string { + return getAcct(this.post.user); + }, + pAcct(): string { + return getAcct(this.p.user); + }, urls(): string[] { - if (this.p.ast) { - return this.p.ast + 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 { @@ -141,6 +147,7 @@ export default Vue.extend({ } } }, + mounted() { // Get replies if (!this.compact) { @@ -170,6 +177,7 @@ export default Vue.extend({ } } }, + 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..c31e28d67f 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" :text="p.text" :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"/> @@ -86,6 +86,8 @@ import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; import getAcct from '../../../../../common/user/get-acct'; +import parse from '../../../../../common/text/parse'; + import MkPostFormWindow from './post-form-window.vue'; import MkRepostFormWindow from './repost-form-window.vue'; import MkPostMenu from '../../../common/views/components/post-menu.vue'; @@ -107,7 +109,9 @@ export default Vue.extend({ components: { XSub }, + props: ['post'], + data() { return { isDetailOpened: false, @@ -115,8 +119,9 @@ export default Vue.extend({ connectionId: null }; }, + computed: { - acct() { + acct(): string { return getAcct(this.p.user); }, isRepost(): boolean { @@ -142,8 +147,9 @@ export default Vue.extend({ return `/@${this.acct}/${this.p.id}`; }, urls(): string[] { - if (this.p.ast) { - return this.p.ast + 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 { @@ -151,12 +157,14 @@ export default Vue.extend({ } } }, + 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); @@ -182,6 +190,7 @@ export default Vue.extend({ } } }, + beforeDestroy() { this.decapture(true); @@ -190,6 +199,7 @@ export default Vue.extend({ (this as any).os.stream.dispose(this.connectionId); } }, + methods: { capture(withHandler = false) { if ((this as any).os.isSignedIn) { 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..17899af280 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 :text="post.text" :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/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue index 7e337d2ae5..448d04d261 100644 --- a/src/client/app/desktop/views/components/ui.header.vue +++ b/src/client/app/desktop/views/components/ui.header.vue @@ -95,7 +95,7 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.header +root(isDark) position -webkit-sticky position sticky top 0 @@ -112,7 +112,7 @@ export default Vue.extend({ z-index 1000 width 100% height 48px - background #f7f7f7 + background isDark ? #313543 : #f7f7f7 > .main z-index 1001 @@ -169,4 +169,10 @@ export default Vue.extend({ > .mk-ui-header-search display none +.header[data-is-darkmode] + root(true) + +.header + root(false) + </style> diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue index f0af1a61aa..0a4e36fc60 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"/> @@ -81,6 +81,8 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../common/user/get-acct'; +import parse from '../../../../../common/text/parse'; + import MkPostMenu from '../../../common/views/components/post-menu.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; import XSub from './post-detail.sub.vue'; @@ -89,6 +91,7 @@ export default Vue.extend({ components: { XSub }, + props: { post: { type: Object, @@ -98,18 +101,20 @@ export default Vue.extend({ default: false } }, + data() { return { context: [], contextFetching: false, - replies: [], + replies: [] }; }, + computed: { - acct() { + acct(): string { return getAcct(this.post.user); }, - pAcct() { + pAcct(): string { return getAcct(this.p.user); }, isRepost(): boolean { @@ -129,8 +134,9 @@ export default Vue.extend({ : 0; }, urls(): string[] { - if (this.p.ast) { - return this.p.ast + 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 { @@ -138,6 +144,7 @@ export default Vue.extend({ } } }, + mounted() { // Get replies if (!this.compact) { @@ -167,6 +174,7 @@ export default Vue.extend({ } } }, + 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..f4f845b49a 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" :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"/> @@ -78,6 +78,8 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../common/user/get-acct'; +import parse from '../../../../../common/text/parse'; + import MkPostMenu from '../../../common/views/components/post-menu.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; import XSub from './post.sub.vue'; @@ -86,18 +88,21 @@ export default Vue.extend({ components: { XSub }, + props: ['post'], + data() { return { connection: null, connectionId: null }; }, + computed: { - acct() { + acct(): string { return getAcct(this.post.user); }, - pAcct() { + pAcct(): string { return getAcct(this.p.user); }, isRepost(): boolean { @@ -120,8 +125,9 @@ export default Vue.extend({ return `/@${this.pAcct}/${this.p.id}`; }, urls(): string[] { - if (this.p.ast) { - return this.p.ast + 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 { @@ -129,12 +135,14 @@ export default Vue.extend({ } } }, + 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); @@ -160,6 +168,7 @@ export default Vue.extend({ } } }, + beforeDestroy() { this.decapture(true); @@ -168,6 +177,7 @@ export default Vue.extend({ (this as any).os.stream.dispose(this.connectionId); } }, + methods: { capture(withHandler = false) { if ((this as any).os.isSignedIn) { 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..97dd987dd7 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" :text="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 diff --git a/src/client/docs/api/gulpfile.ts b/src/client/docs/api/gulpfile.ts index 16066b0d2e..4b962fe0c6 100644 --- a/src/client/docs/api/gulpfile.ts +++ b/src/client/docs/api/gulpfile.ts @@ -101,7 +101,7 @@ gulp.task('doc:api:endpoints', async () => { } //console.log(files); files.forEach(file => { - const ep = yaml.safeLoad(fs.readFileSync(file, 'utf-8')); + const ep: any = yaml.safeLoad(fs.readFileSync(file, 'utf-8')); const vars = { endpoint: ep.endpoint, url: { |