This commit is contained in:
syuilo 2018-04-26 11:46:42 +09:00
parent a2a3dd55ad
commit bc8a0083e2
5 changed files with 197 additions and 129 deletions

View file

@ -144,6 +144,7 @@
"koa-multer": "1.0.2",
"koa-router": "7.4.0",
"koa-send": "4.1.3",
"koa-slow": "^2.1.0",
"kue": "0.11.6",
"license-checker": "18.0.0",
"loader-utils": "1.1.0",

View file

@ -1,7 +1,20 @@
<template>
<div class="mk-notes">
<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
<slot name="head"></slot>
<slot></slot>
<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
<div class="init" v-if="fetching">
%fa:spinner .pulse%%i18n:common.loading%
</div>
<div v-if="!fetching && requestInitPromise != null">
<p>読み込みに失敗しました</p>
<button @click="resolveInitPromise">リトライ</button>
</div>
<transition-group name="mk-notes" class="transition">
<template v-for="(note, i) in _notes">
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
@ -11,8 +24,12 @@
</p>
</template>
</transition-group>
<footer>
<slot name="tail"></slot>
<footer v-if="more">
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">%i18n:@load-more%</template>
<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
</button>
</footer>
</div>
</template>
@ -20,13 +37,26 @@
<script lang="ts">
import Vue from 'vue';
const displayLimit = 30;
export default Vue.extend({
props: {
notes: {
type: Array,
default: () => []
more: {
type: Function,
required: false
}
},
data() {
return {
requestInitPromise: null as () => Promise<any[]>,
notes: [],
queue: [],
fetching: true,
moreFetching: false
};
},
computed: {
_notes(): any[] {
return (this.notes as any).map(note => {
@ -38,9 +68,107 @@ export default Vue.extend({
});
}
},
mounted() {
window.addEventListener('scroll', this.onScroll);
},
beforeDestroy() {
window.removeEventListener('scroll', this.onScroll);
},
methods: {
isScrollTop() {
return window.scrollY <= 8;
},
onNoteUpdated(i, note) {
Vue.set((this as any).notes, i, note);
},
init(promiseGenerator: () => Promise<any[]>) {
this.requestInitPromise = promiseGenerator;
this.resolveInitPromise();
},
resolveInitPromise() {
this.queue = [];
this.notes = [];
this.fetching = true;
const promise = this.requestInitPromise();
promise.then(notes => {
this.notes = notes;
this.requestInitPromise = null;
this.fetching = false;
}, e => {
this.fetching = false;
});
},
prepend(note, silent = false) {
//#region
const isMyNote = note.userId == (this as any).os.i.id;
const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
if ((this as any).os.i.clientSettings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
return;
}
}
if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) {
if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) {
return;
}
}
//#endregion
if (this.isScrollTop()) {
// Prepend the note
this.notes.unshift(note);
// 稿
if (this.notes.length >= displayLimit) {
this.notes = this.notes.slice(0, displayLimit);
}
} else {
this.queue.unshift(note);
}
},
append(note) {
this.notes.push(note);
},
tail() {
return this.notes[this.notes.length - 1];
},
releaseQueue() {
this.queue.forEach(n => this.prepend(n, true));
this.queue = [];
},
async loadMore() {
if (this.more == null) return;
if (this.moreFetching) return;
this.moreFetching = true;
await this.more();
this.moreFetching = false;
},
onScroll() {
if (this.isScrollTop()) {
this.releaseQueue();
}
if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
const current = window.scrollY + window.innerHeight;
if (current > document.body.offsetHeight - 8) this.loadMore();
}
}
}
});
@ -79,6 +207,13 @@ export default Vue.extend({
[data-fa]
margin-right 8px
> .newer-indicator
position -webkit-sticky
position sticky
z-index 100
height 3px
background $theme-color
> .init
padding 64px 0
text-align center

View file

@ -1,19 +1,12 @@
<template>
<div class="mk-timeline">
<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
<mk-friends-maker v-if="alone"/>
<mk-notes :notes="notes">
<div class="init" v-if="fetching">
%fa:spinner .pulse%%i18n:common.loading%
</div>
<div class="empty" v-if="!fetching && notes.length == 0">
<mk-notes ref="timeline" :more="existMore ? more : null">
<div slot="empty">
%fa:R comments%
%i18n:@empty%
</div>
<button v-if="canFetchMore" @click="more" :disabled="moreFetching" slot="tail">
<span v-if="!moreFetching">%i18n:@load-more%</span>
<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
</button>
</mk-notes>
</div>
</template>
@ -22,7 +15,6 @@
import Vue from 'vue';
const fetchLimit = 10;
const displayLimit = 30;
export default Vue.extend({
props: {
@ -37,8 +29,6 @@ export default Vue.extend({
return {
fetching: true,
moreFetching: false,
notes: [],
queue: [],
existMore: false,
connection: null,
connectionId: null
@ -48,10 +38,6 @@ export default Vue.extend({
computed: {
alone(): boolean {
return (this as any).os.i.followingCount == 0;
},
canFetchMore(): boolean {
return !this.moreFetching && !this.fetching && this.notes.length > 0 && this.existMore;
}
},
@ -63,8 +49,6 @@ export default Vue.extend({
this.connection.on('follow', this.onChangeFollowing);
this.connection.on('unfollow', this.onChangeFollowing);
window.addEventListener('scroll', this.onScroll);
this.fetch();
},
@ -73,102 +57,54 @@ export default Vue.extend({
this.connection.off('follow', this.onChangeFollowing);
this.connection.off('unfollow', this.onChangeFollowing);
(this as any).os.stream.dispose(this.connectionId);
window.removeEventListener('scroll', this.onScroll);
},
methods: {
isScrollTop() {
return window.scrollY <= 8;
},
fetch(cb?) {
this.queue = [];
this.fetching = true;
(this as any).api('notes/timeline', {
limit: fetchLimit + 1,
untilDate: this.date ? (this.date as any).getTime() : undefined,
includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
}
this.notes = notes;
this.fetching = false;
this.$emit('loaded');
if (cb) cb();
});
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('notes/timeline', {
limit: fetchLimit + 1,
includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
}
res(notes);
this.fetching = false;
this.$emit('loaded');
if (cb) cb();
}, rej);
}));
},
more() {
this.moreFetching = true;
(this as any).api('notes/timeline', {
limit: fetchLimit + 1,
untilId: this.notes[this.notes.length - 1].id,
untilId: (this.$refs.timeline as any).tail().id,
includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
} else {
this.existMore = false;
}
this.notes = this.notes.concat(notes);
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
},
prependNote(note) {
// Prepent a note
this.notes.unshift(note);
// 稿
if (this.notes.length >= displayLimit) {
this.notes = this.notes.slice(0, displayLimit);
}
},
releaseQueue() {
this.queue.forEach(n => this.prependNote(n));
this.queue = [];
},
onNote(note) {
//#region
const isMyNote = note.userId == (this as any).os.i.id;
const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
if ((this as any).os.i.clientSettings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
return;
}
}
if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) {
if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) {
return;
}
}
//#endregion
if (this.isScrollTop()) {
this.prependNote(note);
} else {
this.queue.unshift(note);
}
// Prepend a note
(this.$refs.timeline as any).prepend(note);
},
onChangeFollowing() {
this.fetch();
},
onScroll() {
if (this.isScrollTop()) {
this.releaseQueue();
}
}
}
});
@ -178,13 +114,6 @@ export default Vue.extend({
@import '~const.styl'
.mk-timeline
> .newer-indicator
position -webkit-sticky
position sticky
z-index 100
height 3px
background $theme-color
> .mk-friends-maker
margin-bottom 8px
</style>

View file

@ -1,17 +1,10 @@
<template>
<div class="mk-user-timeline">
<mk-notes :notes="notes">
<div class="init" v-if="fetching">
%fa:spinner .pulse%%i18n:common.loading%
</div>
<div class="empty" v-if="!fetching && notes.length == 0">
<mk-notes ref="timeline" :more="existMore ? more : null">
<div slot="empty">
%fa:R comments%
{{ withMedia ? '%i18n:!@no-notes-with-media%' : '%i18n:!@no-notes%' }}
</div>
<button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail">
<span v-if="!moreFetching">%i18n:@load-more%</span>
<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
</button>
</mk-notes>
</div>
</template>
@ -19,49 +12,53 @@
<script lang="ts">
import Vue from 'vue';
const limit = 10;
const fetchLimit = 10;
export default Vue.extend({
props: ['user', 'withMedia'],
data() {
return {
fetching: true,
notes: [],
existMore: false,
moreFetching: false
};
},
mounted() {
(this as any).api('users/notes', {
userId: this.user.id,
withMedia: this.withMedia,
limit: limit + 1
}).then(notes => {
if (notes.length == limit + 1) {
notes.pop();
this.existMore = true;
}
this.notes = notes;
this.fetching = false;
this.$emit('loaded');
});
this.fetch();
},
methods: {
fetch() {
this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('users/notes', {
userId: this.user.id,
withMedia: this.withMedia,
limit: fetchLimit + 1
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
}
res(notes);
this.fetching = false;
this.$emit('loaded');
}, rej);
}));
},
more() {
this.moreFetching = true;
(this as any).api('users/notes', {
userId: this.user.id,
withMedia: this.withMedia,
limit: limit + 1,
untilId: this.notes[this.notes.length - 1].id
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id
}).then(notes => {
if (notes.length == limit + 1) {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
} else {
this.existMore = false;
}
this.notes = this.notes.concat(notes);
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
}

View file

@ -11,6 +11,7 @@ import * as Router from 'koa-router';
import * as mount from 'koa-mount';
import * as compress from 'koa-compress';
import * as logger from 'koa-logger';
const slow = require('koa-slow');
import activityPub from './activitypub';
import webFinger from './webfinger';
@ -23,6 +24,11 @@ app.proxy = true;
if (process.env.NODE_ENV != 'production') {
// Logger
app.use(logger());
// Delay
app.use(slow({
delay: 1000
}));
}
// Compress response