Merge branch 'develop' into improve-media

This commit is contained in:
tamaina
2018-09-15 22:15:56 +09:00
committed by GitHub
200 changed files with 6781 additions and 2331 deletions

View File

@ -78,7 +78,7 @@ export default Vue.extend({
scale: 0.8,
duration: 300,
easing: [ 0.5, -0.5, 1, 0.5 ],
complete: () => this.$destroy()
complete: () => this.destroyDom()
});
},
onBgClick() {

View File

@ -31,15 +31,15 @@ export default Vue.extend({
},
onSelected(file) {
this.$emit('selected', file);
this.$destroy();
this.destroyDom();
},
cancel() {
this.$emit('canceled');
this.$destroy();
this.destroyDom();
},
ok() {
this.$emit('selected', this.files);
this.$destroy();
this.destroyDom();
}
}
});

View File

@ -19,11 +19,11 @@ export default Vue.extend({
methods: {
cancel() {
this.$emit('canceled');
this.$destroy();
this.destroyDom();
},
ok() {
this.$emit('selected', (this.$refs.browser as any).folder);
this.$destroy();
this.destroyDom();
}
}
});

View File

@ -67,7 +67,7 @@
import Vue from 'vue';
import * as EXIF from 'exif-js';
import * as hljs from 'highlight.js';
import gcd from '../../../common/scripts/gcd';
import { gcd } from '../../../../../prelude/math';
export default Vue.extend({
props: ['file'],

View File

@ -47,7 +47,7 @@ export default Vue.extend({
this.fetch();
},
close() {
this.$destroy();
this.destroyDom();
}
}
});

View File

@ -1,5 +1,5 @@
<template>
<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide" @click="hide = false">
<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
<div>
<b>%fa:exclamation-triangle% %i18n:@sensitive%</b>
<span>%i18n:@click-to-show%</span>

View File

@ -35,20 +35,26 @@
</div>
</header>
<div class="body">
<div class="text">
<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
</div>
<div class="media" v-if="p.media.length > 0">
<mk-media-list :media-list="p.media" :raw="true"/>
</div>
<mk-poll v-if="p.poll" :note="p"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/>
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
<mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
</div>
<div class="files" v-if="p.files.length > 0">
<mk-media-list :media-list="p.files" :raw="true"/>
</div>
<mk-poll v-if="p.poll" :note="p"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</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>
</div>
<router-link class="time" :to="p | notePage">
@ -85,6 +91,7 @@ import parse from '../../../../../mfm/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';
import { sum } from '../../../../../prelude/array';
export default Vue.extend({
components: {
@ -103,6 +110,7 @@ export default Vue.extend({
data() {
return {
showContent: false,
conversation: [],
conversationFetching: false,
replies: []
@ -113,7 +121,7 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
this.note.mediaIds.length == 0 &&
this.note.fileIds.length == 0 &&
this.note.poll == null);
},
@ -123,9 +131,7 @@ export default Vue.extend({
reactionsCount(): number {
return this.p.reactionCounts
? Object.keys(this.p.reactionCounts)
.map(key => this.p.reactionCounts[key])
.reduce((a, b) => a + b)
? sum(Object.values(this.p.reactionCounts))
: 0;
},
@ -335,44 +341,57 @@ root(isDark)
> .body
padding 8px 0
> .text
> .cw
cursor default
display block
margin 0
padding 0
overflow-wrap break-word
font-size 16px
color isDark ? #fff : #717171
@media (min-width 500px)
font-size 24px
> .text
margin-right 8px
> .renote
margin 8px 0
> .content
> .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
> .text
display block
max-width 100%
margin 0
padding 0
overflow-wrap break-word
font-size 16px
color isDark ? #fff : #717171
@media (min-width 500px)
font-size 24px
> .renote
margin 8px 0
> *
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
> .files
> img
display block
max-width 100%
> .time
font-size 16px

View File

@ -1,10 +1,16 @@
<template>
<div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<div class="yohlumlkhizgfkvvscwfcrcggkotpvry" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
<p v-if="note.cw != null" class="cw">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
<mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="note.cw == null || showContent">
<mk-sub-note-content class="text" :note="note"/>
</div>
</div>
</div>
</div>
@ -14,7 +20,18 @@
import Vue from 'vue';
export default Vue.extend({
props: ['note']
props: {
note: {
type: Object,
required: true
}
},
data() {
return {
showContent: false
};
}
});
</script>
@ -65,16 +82,28 @@ root(isDark)
> .body
> .text
> .cw
cursor default
display block
margin 0
padding 0
color isDark ? #959ba7 : #717171
overflow-wrap break-word
color isDark ? #fff : #717171
.mk-note-preview[data-darkmode]
> .text
margin-right 8px
> .content
> .text
cursor default
margin 0
padding 0
color isDark ? #959ba7 : #717171
.yohlumlkhizgfkvvscwfcrcggkotpvry[data-darkmode]
root(true)
.mk-note-preview:not([data-darkmode])
.yohlumlkhizgfkvvscwfcrcggkotpvry:not([data-darkmode])
root(false)
</style>

View File

@ -1,10 +1,16 @@
<template>
<div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<div class="zlrxdaqttccpwhpaagdmkawtzklsccam" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
<p v-if="note.cw != null" class="cw">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
<mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="note.cw == null || showContent">
<mk-sub-note-content class="text" :note="note"/>
</div>
</div>
</div>
</div>
@ -24,6 +30,12 @@ export default Vue.extend({
type: Boolean,
default: true
}
},
data() {
return {
showContent: false
};
}
});
</script>
@ -77,20 +89,31 @@ root(isDark)
margin-bottom 2px
> .body
> .text
> .cw
cursor default
display block
margin 0
padding 0
color isDark ? #959ba7 : #717171
overflow-wrap break-word
color isDark ? #fff : #717171
pre
max-height 120px
font-size 80%
> .text
margin-right 8px
.sub[data-darkmode]
> .content
> .text
margin 0
padding 0
color isDark ? #959ba7 : #717171
pre
max-height 120px
font-size 80%
.zlrxdaqttccpwhpaagdmkawtzklsccam[data-darkmode]
root(true)
.sub:not([data-darkmode])
.zlrxdaqttccpwhpaagdmkawtzklsccam:not([data-darkmode])
root(false)
</style>

View File

@ -18,7 +18,7 @@
<div class="body">
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
<span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@less%' : '%i18n:@more%' }}</span>
<mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
@ -28,16 +28,14 @@
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.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 class="files" v-if="p.files.length > 0">
<mk-media-list :media-list="p.files"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</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 class="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div>
</div>
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
</div>
@ -70,6 +68,7 @@ import parse from '../../../../../mfm/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';
import { sum } from '../../../../../prelude/array';
export default Vue.extend({
components: {
@ -90,7 +89,7 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
this.note.mediaIds.length == 0 &&
this.note.fileIds.length == 0 &&
this.note.poll == null);
},
@ -100,9 +99,7 @@ export default Vue.extend({
reactionsCount(): number {
return this.p.reactionCounts
? Object.keys(this.p.reactionCounts)
.map(key => this.p.reactionCounts[key])
.reduce((a, b) => a + b)
? sum(Object.values(this.p.reactionCounts))
: 0;
},
@ -353,19 +350,6 @@ root(isDark)
> .text
margin-right 8px
> .toggle
display inline-block
padding 4px 8px
font-size 0.7em
color isDark ? #393f4f : #fff
background isDark ? #687390 : #b1b9c1
border-radius 2px
cursor pointer
user-select none
&:hover
background isDark ? #707b97 : #bbc4ce
> .content
> .text
@ -414,7 +398,7 @@ root(isDark)
.mk-url-preview
margin-top 8px
> .media
> .files
> img
display block
max-width 100%
@ -437,7 +421,7 @@ root(isDark)
> .renote
margin 8px 0
> .mk-note-preview
> *
padding 16px
border dashed 1px isDark ? #4e945e : #c0dac6
border-radius 8px

View File

@ -14,8 +14,7 @@
</div>
<!-- トランジションを有効にするとなぜかメモリリークする -->
<!-- <transition-group name="mk-notes" class="transition"> -->
<div class="transition">
<transition-group name="mk-notes" class="transition" tag="div">
<template v-for="(note, i) in _notes">
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
@ -23,8 +22,7 @@
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
</div>
<!-- </transition-group> -->
</transition-group>
<footer v-if="more">
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
@ -125,7 +123,7 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
const isMyNote = note.userId == this.$store.state.i.id;
const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {

View File

@ -1,5 +1,5 @@
<template>
<div class="mk-notify">
<div class="mk-notify" :class="pos">
<div>
<mk-notification-preview :notification="notification"/>
</div>
@ -12,11 +12,16 @@ import * as anime from 'animejs';
export default Vue.extend({
props: ['notification'],
computed: {
pos() {
return this.$store.state.device.mobileNotificationPosition;
}
},
mounted() {
this.$nextTick(() => {
anime({
targets: this.$el,
bottom: '0px',
[this.pos]: '0px',
duration: 500,
easing: 'easeOutQuad'
});
@ -24,10 +29,10 @@ export default Vue.extend({
setTimeout(() => {
anime({
targets: this.$el,
bottom: `-${this.$el.offsetHeight}px`,
[this.pos]: `-${this.$el.offsetHeight}px`,
duration: 500,
easing: 'easeOutQuad',
complete: () => this.$destroy()
complete: () => this.destroyDom()
});
}, 6000);
});
@ -40,8 +45,7 @@ export default Vue.extend({
$height = 78px
position fixed
z-index 1024
bottom -($height)
z-index 10000
left 0
right 0
width 100%
@ -52,6 +56,12 @@ export default Vue.extend({
pointer-events none
font-size 80%
&.bottom
bottom -($height)
&.top
top -($height)
> div
height 100%
-webkit-backdrop-filter blur(2px)

View File

@ -1,7 +1,7 @@
<template>
<div class="ulveipglmagnxfgvitaxyszerjwiqmwl">
<div class="bg" ref="bg" @click="onBgClick"></div>
<div class="main" ref="main" @click.self="onBgClick">
<div class="bg" ref="bg"></div>
<div class="main" ref="main">
<mk-post-form ref="form"
:reply="reply"
:renote="renote"
@ -79,15 +79,10 @@ export default Vue.extend({
translateY: 16,
duration: 300,
easing: 'easeOutQuad',
complete: () => this.$destroy()
complete: () => this.destroyDom()
});
},
onBgClick() {
this.$emit('cancel');
this.close();
},
onPosted() {
this.$emit('posted');
this.close();

View File

@ -4,14 +4,14 @@
<header>
<button class="cancel" @click="cancel">%fa:times%</button>
<div>
<span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span>
<span class="text-count" :class="{ over: trimmedLength(text) > 1000 }">{{ 1000 - trimmedLength(text) }}</span>
<span class="geo" v-if="geo">%fa:map-marker-alt%</span>
<button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button>
</div>
</header>
<div class="form">
<mk-note-preview v-if="reply" :note="reply"/>
<mk-note-preview v-if="renote" :note="renote"/>
<mk-note-preview class="preview" v-if="reply" :note="reply"/>
<mk-note-preview class="preview" v-if="renote" :note="renote"/>
<div v-if="visibility == 'specified'" class="visibleUsers">
<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
<a @click="addVisibleUser">+%i18n:@add-visible-user%</a>
@ -59,6 +59,9 @@ import MkVisibilityChooser from '../../../common/views/components/visibility-cho
import getFace from '../../../common/scripts/get-face';
import parse from '../../../../../mfm/parse';
import { host } from '../../../config';
import { erase } from '../../../../../prelude/array';
import { length } from 'stringz';
import parseAcct from '../../../../../misc/acct/parse';
export default Vue.extend({
components: {
@ -94,7 +97,7 @@ export default Vue.extend({
files: [],
poll: false,
geo: null,
visibility: this.$store.state.device.visibility || 'public',
visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
visibleUsers: [],
useCw: false,
cw: null,
@ -178,6 +181,10 @@ export default Vue.extend({
},
methods: {
trimmedLength(text: string) {
return length(text.trim());
},
addTag(tag: string) {
insertTextAtCursor(this.$refs.text, ` #${tag} `);
},
@ -200,12 +207,12 @@ export default Vue.extend({
attachMedia(driveFile) {
this.files.push(driveFile);
this.$emit('change-attached-media', this.files);
this.$emit('change-attached-files', this.files);
},
detachMedia(file) {
this.files = this.files.filter(x => x.id != file.id);
this.$emit('change-attached-media', this.files);
this.$emit('change-attached-files', this.files);
},
onChangeFile() {
@ -252,24 +259,23 @@ export default Vue.extend({
addVisibleUser() {
(this as any).apis.input({
title: '%i18n:@username-prompt%'
}).then(username => {
(this as any).api('users/show', {
username
}).then(user => {
}).then(acct => {
if (acct.startsWith('@')) acct = acct.substr(1);
(this as any).api('users/show', parseAcct(acct)).then(user => {
this.visibleUsers.push(user);
});
});
},
removeVisibleUser(user) {
this.visibleUsers = this.visibleUsers.filter(u => u != user);
this.visibleUsers = erase(user, this.visibleUsers);
},
clear() {
this.text = '';
this.files = [];
this.poll = false;
this.$emit('change-attached-media');
this.$emit('change-attached-files');
},
post() {
@ -277,7 +283,7 @@ export default Vue.extend({
const viaMobile = this.$store.state.settings.disableViaMobile !== true;
(this as any).api('notes/create', {
text: this.text == '' ? undefined : this.text,
mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
@ -302,7 +308,7 @@ export default Vue.extend({
if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], [])));
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
},
@ -381,7 +387,7 @@ root(isDark)
max-width 500px
margin 0 auto
> .mk-note-preview
> .preview
padding 16px
> .visibleUsers

View File

@ -7,9 +7,9 @@
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
<a class="rp" v-if="note.renoteId">RP: ...</a>
</div>
<details v-if="note.media.length > 0">
<summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary>
<mk-media-list :media-list="note.media"/>
<details v-if="note.files.length > 0">
<summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>
<mk-media-list :media-list="note.files"/>
</details>
<details v-if="note.poll">
<summary>%i18n:@poll%</summary>

View File

@ -34,6 +34,12 @@
<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li>
</ul>
</div>
<div class="announcements" v-if="announcements && announcements.length > 0">
<article v-for="announcement in announcements">
<span v-html="announcement.title" class="title"></span>
<div v-html="announcement.text"></div>
</article>
</div>
<a :href="aboutUrl"><p class="about">%i18n:@about%</p></a>
</div>
</transition>
@ -46,23 +52,32 @@ import { lang } from '../../../config';
export default Vue.extend({
props: ['isOpen'],
data() {
return {
hasGameInvitation: false,
connection: null,
connectionId: null,
aboutUrl: `/docs/${lang}/about`
aboutUrl: `/docs/${lang}/about`,
announcements: []
};
},
computed: {
hasUnreadNotification(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
},
hasUnreadMessagingMessage(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
}
},
mounted() {
(this as any).os.getMeta().then(meta => {
this.announcements = meta.broadcasts;
});
if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
@ -71,6 +86,7 @@ export default Vue.extend({
this.connection.on('reversi_no_invites', this.onReversiNoInvites);
}
},
beforeDestroy() {
if (this.$store.getters.isSignedIn) {
this.connection.off('reversi_invited', this.onReversiInvited);
@ -78,18 +94,22 @@ export default Vue.extend({
(this as any).os.stream.dispose(this.connectionId);
}
},
methods: {
search() {
const query = window.prompt('%i18n:@search%');
if (query == null || query == '') return;
this.$router.push(`/search?q=${encodeURIComponent(query)}`);
},
onReversiInvited() {
this.hasGameInvitation = true;
},
onReversiNoInvites() {
this.hasGameInvitation = false;
},
dark() {
this.$store.commit('device/set', {
key: 'darkmode',
@ -204,6 +224,17 @@ root(isDark)
color $color
opacity 0.5
.announcements
> article
background isDark ? rgba(30, 129, 216, 0.2) : rgba(155, 196, 232, 0.2)
color isDark ? #fff : #3f4967
padding 16px
margin 8px 0
font-size 12px
> .title
font-weight bold
.about
margin 0 0 8px 0
padding 1em 0

View File

@ -41,7 +41,7 @@ export default Vue.extend({
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('users/notes', {
userId: this.user.id,
withMedia: this.withMedia,
withFiles: this.withMedia,
limit: fetchLimit + 1
}).then(notes => {
if (notes.length == fetchLimit + 1) {
@ -62,7 +62,7 @@ export default Vue.extend({
const promise = (this as any).api('users/notes', {
userId: this.user.id,
withMedia: this.withMedia,
withFiles: this.withMedia,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id
});