summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2018-04-26 11:46:42 +0900
committersyuilo <syuilotan@yahoo.co.jp>2018-04-26 11:46:42 +0900
commitbc8a0083e262c724ef4d803a4f2a9f076377341c (patch)
tree449e1a28cbf49a6eaa4d03eab1b3165f7e437fe3
parentwip (diff)
downloadsharkey-bc8a0083e262c724ef4d803a4f2a9f076377341c.tar.gz
sharkey-bc8a0083e262c724ef4d803a4f2a9f076377341c.tar.bz2
sharkey-bc8a0083e262c724ef4d803a4f2a9f076377341c.zip
wip
-rw-r--r--package.json1
-rw-r--r--src/client/app/mobile/views/components/notes.vue147
-rw-r--r--src/client/app/mobile/views/components/timeline.vue117
-rw-r--r--src/client/app/mobile/views/components/user-timeline.vue55
-rw-r--r--src/server/index.ts6
5 files changed, 197 insertions, 129 deletions
diff --git a/package.json b/package.json
index 7d0adc3cb6..d37bbc040d 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index 999ab566ac..137e15c6de 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -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
diff --git a/src/client/app/mobile/views/components/timeline.vue b/src/client/app/mobile/views/components/timeline.vue
index f56667bed5..561034917e 100644
--- a/src/client/app/mobile/views/components/timeline.vue
+++ b/src/client/app/mobile/views/components/timeline.vue
@@ -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>
diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
index 40b3be035e..89ac4d2c66 100644
--- a/src/client/app/mobile/views/components/user-timeline.vue
+++ b/src/client/app/mobile/views/components/user-timeline.vue
@@ -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;
});
}
diff --git a/src/server/index.ts b/src/server/index.ts
index 2b5a910507..594f40c22f 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -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