summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2018-04-26 16:10:25 +0900
committerGitHub <noreply@github.com>2018-04-26 16:10:25 +0900
commit5d4b884528e0533e32b9c827ae8ccf64df0085dc (patch)
tree1f6a3238dfbf1f77da78d96e993f6d76cad73089 /src
parentRefactor (diff)
parentwip (diff)
downloadsharkey-5d4b884528e0533e32b9c827ae8ccf64df0085dc.tar.gz
sharkey-5d4b884528e0533e32b9c827ae8ccf64df0085dc.tar.bz2
sharkey-5d4b884528e0533e32b9c827ae8ccf64df0085dc.zip
Merge pull request #1550 from syuilo/user-list
User list
Diffstat (limited to 'src')
-rw-r--r--src/cafy-id.ts29
-rw-r--r--src/client/app/common/mios.ts1
-rw-r--r--src/client/app/common/scripts/streaming/user-list.ts17
-rw-r--r--src/client/app/desktop/api/update-banner.ts2
-rw-r--r--src/client/app/desktop/script.ts2
-rw-r--r--src/client/app/desktop/views/components/follow-button.vue6
-rw-r--r--src/client/app/desktop/views/components/index.ts2
-rw-r--r--src/client/app/desktop/views/components/mentions.vue6
-rw-r--r--src/client/app/desktop/views/components/notes.vue190
-rw-r--r--src/client/app/desktop/views/components/timeline.core.vue167
-rw-r--r--src/client/app/desktop/views/components/timeline.vue43
-rw-r--r--src/client/app/desktop/views/components/ui.header.account.vue11
-rw-r--r--src/client/app/desktop/views/components/user-list-timeline.vue93
-rw-r--r--src/client/app/desktop/views/components/user-lists-window.vue69
-rw-r--r--src/client/app/desktop/views/components/users-list.vue6
-rw-r--r--src/client/app/desktop/views/components/widget-container.vue2
-rw-r--r--src/client/app/desktop/views/pages/user-list.users.vue131
-rw-r--r--src/client/app/desktop/views/pages/user-list.vue71
-rw-r--r--src/client/app/desktop/views/pages/user/user.profile.vue51
-rw-r--r--src/client/app/desktop/views/pages/user/user.timeline.vue73
-rw-r--r--src/client/app/mobile/views/components/index.ts4
-rw-r--r--src/client/app/mobile/views/components/notes.vue169
-rw-r--r--src/client/app/mobile/views/components/timeline.vue190
-rw-r--r--src/client/app/mobile/views/components/user-list-timeline.vue93
-rw-r--r--src/client/app/mobile/views/components/user-timeline.vue55
-rw-r--r--src/client/app/mobile/views/components/users-list.vue6
-rw-r--r--src/client/app/mobile/views/pages/dashboard.vue196
-rw-r--r--src/client/app/mobile/views/pages/home.timeline.vue149
-rw-r--r--src/client/app/mobile/views/pages/home.vue304
-rw-r--r--src/client/app/mobile/views/pages/user.vue8
-rw-r--r--src/models/note-reaction.ts2
-rw-r--r--src/models/sw-subscription.ts1
-rw-r--r--src/models/user-list.ts67
-rw-r--r--src/models/user.ts22
-rw-r--r--src/publishers/stream.ts5
-rw-r--r--src/server/api/endpoints.ts29
-rw-r--r--src/server/api/endpoints/aggregation/posts.ts3
-rw-r--r--src/server/api/endpoints/aggregation/users.ts3
-rw-r--r--src/server/api/endpoints/aggregation/users/activity.ts7
-rw-r--r--src/server/api/endpoints/aggregation/users/followers.ts7
-rw-r--r--src/server/api/endpoints/aggregation/users/following.ts7
-rw-r--r--src/server/api/endpoints/aggregation/users/post.ts7
-rw-r--r--src/server/api/endpoints/aggregation/users/reaction.ts4
-rw-r--r--src/server/api/endpoints/app/create.ts2
-rw-r--r--src/server/api/endpoints/app/show.ts4
-rw-r--r--src/server/api/endpoints/channels.ts6
-rw-r--r--src/server/api/endpoints/channels/create.ts4
-rw-r--r--src/server/api/endpoints/channels/notes.ts12
-rw-r--r--src/server/api/endpoints/channels/show.ts8
-rw-r--r--src/server/api/endpoints/channels/unwatch.ts8
-rw-r--r--src/server/api/endpoints/channels/watch.ts8
-rw-r--r--src/server/api/endpoints/drive/files.ts13
-rw-r--r--src/server/api/endpoints/drive/files/create.ts9
-rw-r--r--src/server/api/endpoints/drive/files/find.ts8
-rw-r--r--src/server/api/endpoints/drive/files/show.ts8
-rw-r--r--src/server/api/endpoints/drive/files/update.ts10
-rw-r--r--src/server/api/endpoints/drive/files/upload_from_url.ts4
-rw-r--r--src/server/api/endpoints/drive/folders.ts13
-rw-r--r--src/server/api/endpoints/drive/folders/create.ts8
-rw-r--r--src/server/api/endpoints/drive/folders/find.ts8
-rw-r--r--src/server/api/endpoints/drive/folders/show.ts8
-rw-r--r--src/server/api/endpoints/drive/folders/update.ts10
-rw-r--r--src/server/api/endpoints/drive/stream.ts10
-rw-r--r--src/server/api/endpoints/following/create.ts4
-rw-r--r--src/server/api/endpoints/following/delete.ts4
-rw-r--r--src/server/api/endpoints/following/stalk.ts5
-rw-r--r--src/server/api/endpoints/following/unstalk.ts4
-rw-r--r--src/server/api/endpoints/i/authorized_apps.ts4
-rw-r--r--src/server/api/endpoints/i/change_password.ts4
-rw-r--r--src/server/api/endpoints/i/favorites.ts6
-rw-r--r--src/server/api/endpoints/i/notifications.ts8
-rw-r--r--src/server/api/endpoints/i/pin.ts8
-rw-r--r--src/server/api/endpoints/i/regenerate_token.ts4
-rw-r--r--src/server/api/endpoints/i/signin_history.ts10
-rw-r--r--src/server/api/endpoints/i/update.ts6
-rw-r--r--src/server/api/endpoints/i/update_client_setting.ts4
-rw-r--r--src/server/api/endpoints/i/update_home.ts2
-rw-r--r--src/server/api/endpoints/i/update_mobile_home.ts2
-rw-r--r--src/server/api/endpoints/messaging/history.ts4
-rw-r--r--src/server/api/endpoints/messaging/messages.ts8
-rw-r--r--src/server/api/endpoints/messaging/messages/create.ts10
-rw-r--r--src/server/api/endpoints/messaging/unread.ts4
-rw-r--r--src/server/api/endpoints/mute/create.ts8
-rw-r--r--src/server/api/endpoints/mute/delete.ts4
-rw-r--r--src/server/api/endpoints/mute/list.ts8
-rw-r--r--src/server/api/endpoints/my/apps.ts4
-rw-r--r--src/server/api/endpoints/notes.ts6
-rw-r--r--src/server/api/endpoints/notes/context.ts4
-rw-r--r--src/server/api/endpoints/notes/create.ts23
-rw-r--r--src/server/api/endpoints/notes/favorites/create.ts8
-rw-r--r--src/server/api/endpoints/notes/favorites/delete.ts8
-rw-r--r--src/server/api/endpoints/notes/global-timeline.ts6
-rw-r--r--src/server/api/endpoints/notes/local-timeline.ts6
-rw-r--r--src/server/api/endpoints/notes/mentions.ts6
-rw-r--r--src/server/api/endpoints/notes/polls/recommendation.ts4
-rw-r--r--src/server/api/endpoints/notes/polls/vote.ts8
-rw-r--r--src/server/api/endpoints/notes/reactions.ts4
-rw-r--r--src/server/api/endpoints/notes/reactions/create.ts4
-rw-r--r--src/server/api/endpoints/notes/reactions/delete.ts9
-rw-r--r--src/server/api/endpoints/notes/replies.ts4
-rw-r--r--src/server/api/endpoints/notes/reposts.ts8
-rw-r--r--src/server/api/endpoints/notes/search.ts10
-rw-r--r--src/server/api/endpoints/notes/show.ts4
-rw-r--r--src/server/api/endpoints/notes/timeline.ts6
-rw-r--r--src/server/api/endpoints/notes/user-list-timeline.ts179
-rw-r--r--src/server/api/endpoints/notifications/get_unread_count.ts4
-rw-r--r--src/server/api/endpoints/notifications/mark_as_read_all.ts4
-rw-r--r--src/server/api/endpoints/othello/games.ts6
-rw-r--r--src/server/api/endpoints/othello/games/show.ts4
-rw-r--r--src/server/api/endpoints/othello/match.ts4
-rw-r--r--src/server/api/endpoints/users.ts4
-rw-r--r--src/server/api/endpoints/users/followers.ts10
-rw-r--r--src/server/api/endpoints/users/following.ts6
-rw-r--r--src/server/api/endpoints/users/get_frequently_replied_users.ts4
-rw-r--r--src/server/api/endpoints/users/lists/create.ts25
-rw-r--r--src/server/api/endpoints/users/lists/list.ts13
-rw-r--r--src/server/api/endpoints/users/lists/push.ts51
-rw-r--r--src/server/api/endpoints/users/lists/show.ts23
-rw-r--r--src/server/api/endpoints/users/notes.ts8
-rw-r--r--src/server/api/endpoints/users/search_by_username.ts4
-rw-r--r--src/server/api/endpoints/users/show.ts62
-rw-r--r--src/server/api/stream/user-list.ts14
-rw-r--r--src/server/api/streaming.ts2
-rw-r--r--src/server/index.ts6
-rw-r--r--src/services/note/create.ts104
125 files changed, 2227 insertions, 1034 deletions
diff --git a/src/cafy-id.ts b/src/cafy-id.ts
new file mode 100644
index 0000000000..3faf5cd996
--- /dev/null
+++ b/src/cafy-id.ts
@@ -0,0 +1,29 @@
+import * as mongo from 'mongodb';
+import { Query } from 'cafy';
+
+export const isAnId = x => mongo.ObjectID.isValid(x);
+export const isNotAnId = x => !isAnId(x);
+
+/**
+ * ID
+ */
+export default class ID extends Query<mongo.ObjectID> {
+ constructor(...args) {
+ super(...args);
+
+ this.transform = v => {
+ if (isAnId(v) && !mongo.ObjectID.prototype.isPrototypeOf(v)) {
+ return new mongo.ObjectID(v);
+ } else {
+ return v;
+ }
+ };
+
+ this.pushValidator(v => {
+ if (!mongo.ObjectID.prototype.isPrototypeOf(v) && isNotAnId(v)) {
+ return new Error('must-be-an-id');
+ }
+ return true;
+ });
+ }
+}
diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts
index 463f763888..4e471cf96f 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/common/mios.ts
@@ -88,6 +88,7 @@ export default class MiOS extends EventEmitter {
propsData: props
}).$mount();
document.body.appendChild(w.$el);
+ return w;
}
/**
diff --git a/src/client/app/common/scripts/streaming/user-list.ts b/src/client/app/common/scripts/streaming/user-list.ts
new file mode 100644
index 0000000000..30a52b98dd
--- /dev/null
+++ b/src/client/app/common/scripts/streaming/user-list.ts
@@ -0,0 +1,17 @@
+import Stream from './stream';
+import MiOS from '../../mios';
+
+export class UserListStream extends Stream {
+ constructor(os: MiOS, me, listId) {
+ super(os, 'user-list', {
+ i: me.token,
+ listId
+ });
+
+ (this as any).on('_connected_', () => {
+ this.send({
+ i: me.token
+ });
+ });
+ }
+}
diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts
index bc3f783e35..feb1c33103 100644
--- a/src/client/app/desktop/api/update-banner.ts
+++ b/src/client/app/desktop/api/update-banner.ts
@@ -95,7 +95,7 @@ export default (os: OS) => {
multiple: false,
title: '%fa:image%バナーにする画像を選択'
});
-
+
return selectedFile
.then(cropImage)
.then(setBanner)
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index 3b0ed48cd0..2658a86b95 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -28,6 +28,7 @@ import MkUser from './views/pages/user/user.vue';
import MkFavorites from './views/pages/favorites.vue';
import MkSelectDrive from './views/pages/selectdrive.vue';
import MkDrive from './views/pages/drive.vue';
+import MkUserList from './views/pages/user-list.vue';
import MkHomeCustomize from './views/pages/home-customize.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkNote from './views/pages/note.vue';
@@ -55,6 +56,7 @@ init(async (launch) => {
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', component: MkDrive },
{ path: '/i/drive/folder/:folder', component: MkDrive },
+ { path: '/i/lists/:list', component: MkUserList },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
{ path: '/othello', component: MkOthello },
diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue
index 30e8cab76f..60c6129f61 100644
--- a/src/client/app/desktop/views/components/follow-button.vue
+++ b/src/client/app/desktop/views/components/follow-button.vue
@@ -19,6 +19,7 @@
<script lang="ts">
import Vue from 'vue';
+
export default Vue.extend({
props: {
user: {
@@ -30,6 +31,7 @@ export default Vue.extend({
default: 'compact'
}
},
+
data() {
return {
wait: false,
@@ -37,6 +39,7 @@ export default Vue.extend({
connectionId: null
};
},
+
mounted() {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
@@ -44,13 +47,14 @@ export default Vue.extend({
this.connection.on('follow', this.onFollow);
this.connection.on('unfollow', this.onUnfollow);
},
+
beforeDestroy() {
this.connection.off('follow', this.onFollow);
this.connection.off('unfollow', this.onUnfollow);
(this as any).os.stream.dispose(this.connectionId);
},
- methods: {
+ methods: {
onFollow(user) {
if (user.id == this.user.id) {
this.user.isFollowing = user.isFollowing;
diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts
index 4f61f43692..f58d0706df 100644
--- a/src/client/app/desktop/views/components/index.ts
+++ b/src/client/app/desktop/views/components/index.ts
@@ -28,6 +28,7 @@ import friendsMaker from './friends-maker.vue';
import followers from './followers.vue';
import following from './following.vue';
import usersList from './users-list.vue';
+import userListTimeline from './user-list-timeline.vue';
import widgetContainer from './widget-container.vue';
Vue.component('mk-ui', ui);
@@ -58,4 +59,5 @@ Vue.component('mk-friends-maker', friendsMaker);
Vue.component('mk-followers', followers);
Vue.component('mk-following', following);
Vue.component('mk-users-list', usersList);
+Vue.component('mk-user-list-timeline', userListTimeline);
Vue.component('mk-widget-container', widgetContainer);
diff --git a/src/client/app/desktop/views/components/mentions.vue b/src/client/app/desktop/views/components/mentions.vue
index fc3a7af75d..53d08a0eca 100644
--- a/src/client/app/desktop/views/components/mentions.vue
+++ b/src/client/app/desktop/views/components/mentions.vue
@@ -1,8 +1,8 @@
<template>
<div class="mk-mentions">
<header>
- <span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて</span>
- <span :data-is-active="mode == 'following'" @click="mode = 'following'">フォロー中</span>
+ <span :data-active="mode == 'all'" @click="mode = 'all'">すべて</span>
+ <span :data-active="mode == 'following'" @click="mode = 'following'">フォロー中</span>
</header>
<div class="fetching" v-if="fetching">
<mk-ellipsis-icon/>
@@ -98,7 +98,7 @@ export default Vue.extend({
font-size 18px
color #555
- &:not([data-is-active])
+ &:not([data-active])
color $theme-color
cursor pointer
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index 1a33a4240b..fa7a782b7b 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -1,5 +1,14 @@
<template>
<div class="mk-notes">
+ <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
+
+ <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
+
+ <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">
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
@@ -9,26 +18,48 @@
</p>
</template>
</transition-group>
- <footer>
- <slot name="footer"></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>
<script lang="ts">
import Vue from 'vue';
+import { url } from '../../../config';
+import getNoteSummary from '../../../../../renderers/get-note-summary';
+
import XNote from './notes.note.vue';
+const displayLimit = 30;
+
export default Vue.extend({
components: {
XNote
},
+
props: {
- notes: {
- type: Array,
- default: () => []
+ more: {
+ type: Function,
+ required: false
}
},
+
+ data() {
+ return {
+ requestInitPromise: null as () => Promise<any[]>,
+ notes: [],
+ queue: [],
+ unreadCount: 0,
+ fetching: true,
+ moreFetching: false
+ };
+ },
+
computed: {
_notes(): any[] {
return (this.notes as any).map(note => {
@@ -40,18 +71,146 @@ export default Vue.extend({
});
}
},
+
+ mounted() {
+ document.addEventListener('visibilitychange', this.onVisibilitychange, false);
+ window.addEventListener('scroll', this.onScroll);
+ },
+
+ beforeDestroy() {
+ document.removeEventListener('visibilitychange', this.onVisibilitychange);
+ window.removeEventListener('scroll', this.onScroll);
+ },
+
methods: {
+ isScrollTop() {
+ return window.scrollY <= 8;
+ },
+
focus() {
(this.$el as any).children[0].focus();
},
+
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 ((document.hidden || !this.isScrollTop()) && note.userId !== (this as any).os.i.id) {
+ this.unreadCount++;
+ document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
+ }
+
+ if (this.isScrollTop()) {
+ // Prepend the note
+ this.notes.unshift(note);
+
+ // サウンドを再生する
+ if ((this as any).os.isEnableSounds && !silent) {
+ const sound = new Audio(`${url}/assets/post.mp3`);
+ sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ sound.play();
+ }
+
+ // オーバーフローしたら古い投稿は捨てる
+ 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;
+ },
+
+ clearNotification() {
+ this.unreadCount = 0;
+ document.title = 'Misskey';
+ },
+
+ onVisibilitychange() {
+ if (!document.hidden) {
+ this.clearNotification();
+ }
+ },
+
+ onScroll() {
+ if (this.isScrollTop()) {
+ this.releaseQueue();
+ this.clearNotification();
+ }
+
+ if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
+ const current = window.scrollY + window.innerHeight;
+ if (current > document.body.offsetHeight - 8) this.loadMore();
+ }
}
}
});
</script>
<style lang="stylus" scoped>
+@import '~const.styl'
+
root(isDark)
.transition
.mk-notes-enter
@@ -78,24 +237,31 @@ root(isDark)
[data-fa]
margin-right 8px
+ > .newer-indicator
+ position -webkit-sticky
+ position sticky
+ z-index 100
+ height 3px
+ background $theme-color
+
> footer
- > *
+ > button
display block
margin 0
padding 16px
width 100%
text-align center
color #ccc
- border-top solid 1px #eaeaea
- border-bottom-left-radius 4px
- border-bottom-right-radius 4px
+ background isDark ? #282C37 : #fff
+ border-top solid 1px isDark ? #1c2023 : #eaeaea
+ border-bottom-left-radius 6px
+ border-bottom-right-radius 6px
- > button
&:hover
- background #f5f5f5
+ background isDark ? #2e3440 : #f5f5f5
&:active
- background #eee
+ background isDark ? #21242b : #eee
.mk-notes[data-darkmode]
root(true)
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index 719425c3c7..a137a57070 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -1,28 +1,23 @@
<template>
-<div class="mk-home-timeline">
- <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
+<div class="mk-timeline-core">
<mk-friends-maker v-if="src == 'home' && alone"/>
<div class="fetching" v-if="fetching">
<mk-ellipsis-icon/>
</div>
- <p class="empty" v-if="notes.length == 0 && !fetching">
- %fa:R comments%%i18n:@empty%
- </p>
- <mk-notes :notes="notes" ref="timeline">
- <button slot="footer" @click="more" :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>
+
+ <mk-notes ref="timeline" :more="canFetchMore ? more : null">
+ <p :class="$style.empty" slot="empty">
+ %fa:R comments%%i18n:@empty%
+ </p>
</mk-notes>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
-import { url } from '../../../config';
+import getNoteSummary from '../../../../../renderers/get-note-summary';
const fetchLimit = 10;
-const displayLimit = 30;
export default Vue.extend({
props: {
@@ -37,10 +32,9 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
existMore: false,
- notes: [],
- queue: [],
connection: null,
connectionId: null,
+ unreadCount: 0,
date: null
};
},
@@ -67,7 +61,7 @@ export default Vue.extend({
},
canFetchMore(): boolean {
- return !this.moreFetching && !this.fetching && this.notes.length > 0 && this.existMore;
+ return !this.moreFetching && !this.fetching && this.existMore;
}
},
@@ -82,7 +76,7 @@ export default Vue.extend({
}
document.addEventListener('keydown', this.onKeydown);
- window.addEventListener('scroll', this.onScroll);
+ document.addEventListener('visibilitychange', this.onVisibilitychange, false);
this.fetch();
},
@@ -96,33 +90,29 @@ export default Vue.extend({
this.stream.dispose(this.connectionId);
document.removeEventListener('keydown', this.onKeydown);
- window.removeEventListener('scroll', this.onScroll);
+ document.removeEventListener('visibilitychange', this.onVisibilitychange);
},
methods: {
- isScrollTop() {
- return window.scrollY <= 8;
- },
-
- fetch(cb?) {
- this.queue = [];
+ fetch() {
this.fetching = true;
- (this as any).api(this.endpoint, {
- limit: fetchLimit + 1,
- untilDate: this.date ? this.date.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(this.endpoint, {
+ limit: fetchLimit + 1,
+ untilDate: this.date ? this.date.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;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
},
more() {
@@ -132,7 +122,7 @@ export default Vue.extend({
(this as any).api(this.endpoint, {
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 => {
@@ -141,56 +131,19 @@ export default Vue.extend({
} 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, silent = false) {
- // サウンドを再生する
- if ((this as any).os.isEnableSounds && !silent) {
- const sound = new Audio(`${url}/assets/post.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
- sound.play();
- }
-
- // 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, true));
- 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 (document.hidden && note.userId !== (this as any).os.i.id) {
+ this.unreadCount++;
+ document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
}
- 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() {
@@ -206,14 +159,10 @@ export default Vue.extend({
this.fetch();
},
- onScroll() {
- if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
- const current = window.scrollY + window.innerHeight;
- if (current > document.body.offsetHeight - 8) this.more();
- }
-
- if (this.isScrollTop()) {
- this.releaseQueue();
+ onVisibilitychange() {
+ if (!document.hidden) {
+ this.unreadCount = 0;
+ document.title = 'Misskey';
}
},
@@ -223,7 +172,7 @@ export default Vue.extend({
this.focus();
}
}
- },
+ }
}
});
</script>
@@ -231,32 +180,28 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-home-timeline
- > .newer-indicator
- position -webkit-sticky
- position sticky
- z-index 100
- height 3px
- background $theme-color
-
+.mk-timeline-core
> .mk-friends-maker
border-bottom solid 1px #eee
> .fetching
padding 64px 0
- > .empty
- display block
- margin 0 auto
- padding 32px
- max-width 400px
- text-align center
- color #999
+</style>
- > [data-fa]
- display block
- margin-bottom 16px
- font-size 3em
- color #ccc
+<style lang="stylus" module>
+.empty
+ display block
+ margin 0 auto
+ padding 32px
+ max-width 400px
+ text-align center
+ color #999
+
+ > [data-fa]
+ display block
+ margin-bottom 16px
+ font-size 3em
+ color #ccc
</style>
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index 8035510a14..f5f13cbd56 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -1,19 +1,23 @@
<template>
<div class="mk-timeline">
<header>
- <span :data-is-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span>
- <span :data-is-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span>
- <span :data-is-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span>
+ <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span>
+ <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span>
+ <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span>
+ <span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
+ <button @click="chooseList" title="リスト">%fa:list%</button>
</header>
<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/>
<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/>
<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/>
+ <mk-user-list-timeline v-if="src == 'list'" ref="tl" key="list" :list="list"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import XCore from './timeline.core.vue';
+import MkUserListsWindow from './user-lists-window.vue';
export default Vue.extend({
components: {
@@ -22,7 +26,8 @@ export default Vue.extend({
data() {
return {
- src: 'home'
+ src: 'home',
+ list: null
};
},
@@ -35,6 +40,15 @@ export default Vue.extend({
methods: {
warp(date) {
(this.$refs.tl as any).warp(date);
+ },
+
+ chooseList() {
+ const w = (this as any).os.new(MkUserListsWindow);
+ w.$once('choosen', list => {
+ this.list = list;
+ this.src = 'list';
+ w.close();
+ });
}
}
});
@@ -55,6 +69,23 @@ root(isDark)
border-radius 6px 6px 0 0
box-shadow 0 1px rgba(0, 0, 0, 0.08)
+ > button
+ position absolute
+ z-index 2
+ top 0
+ right 0
+ padding 0
+ width 42px
+ font-size 0.9em
+ line-height 42px
+ color isDark ? #9baec8 : #ccc
+
+ &:hover
+ color isDark ? #b2c1d5 : #aaa
+
+ &:active
+ color isDark ? #b2c1d5 : #999
+
> span
display inline-block
padding 0 10px
@@ -62,7 +93,7 @@ root(isDark)
font-size 12px
user-select none
- &[data-is-active]
+ &[data-active]
color $theme-color
cursor default
font-weight bold
@@ -77,7 +108,7 @@ root(isDark)
height 2px
background $theme-color
- &:not([data-is-active])
+ &:not([data-active])
color isDark ? #9aa2a7 : #6f7477
cursor pointer
diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue
index 2d4d23933c..5148c5b967 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -16,6 +16,9 @@
<li>
<router-link to="/i/favorites">%fa:star%<span>%i18n:@favorites%</span>%fa:angle-right%</router-link>
</li>
+ <li @click="list">
+ <p>%fa:list%<span>%i18n:@lists%</span>%fa:angle-right%</p>
+ </li>
</ul>
<ul>
<li>
@@ -42,6 +45,7 @@
<script lang="ts">
import Vue from 'vue';
+import MkUserListsWindow from './user-lists-window.vue';
import MkSettingsWindow from './settings-window.vue';
import MkDriveWindow from './drive-window.vue';
import contains from '../../../common/scripts/contains';
@@ -80,6 +84,13 @@ export default Vue.extend({
this.close();
(this as any).os.new(MkDriveWindow);
},
+ list() {
+ this.close();
+ const w = (this as any).os.new(MkUserListsWindow);
+ w.$once('choosen', list => {
+ this.$router.push(`i/lists/${ list.id }`);
+ });
+ },
settings() {
this.close();
(this as any).os.new(MkSettingsWindow);
diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue
new file mode 100644
index 0000000000..ee983a969c
--- /dev/null
+++ b/src/client/app/desktop/views/components/user-list-timeline.vue
@@ -0,0 +1,93 @@
+<template>
+<div>
+ <mk-notes ref="timeline" :more="existMore ? more : null"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { UserListStream } from '../../../common/scripts/streaming/user-list';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ props: ['list'],
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null
+ };
+ },
+ watch: {
+ $route: 'init'
+ },
+ mounted() {
+ this.init();
+ },
+ beforeDestroy() {
+ this.connection.close();
+ },
+ methods: {
+ init() {
+ if (this.connection) this.connection.close();
+ this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id);
+ this.connection.on('note', this.onNote);
+ this.connection.on('userAdded', this.onUserAdded);
+ this.connection.on('userRemoved', this.onUserRemoved);
+
+ this.fetch();
+ },
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/user-list-timeline', {
+ listId: this.list.id,
+ 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');
+ }, rej);
+ }));
+ },
+ more() {
+ this.moreFetching = true;
+
+ (this as any).api('notes/list-timeline', {
+ listId: this.list.id,
+ limit: fetchLimit + 1,
+ 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();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+ },
+ onNote(note) {
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ },
+ onUserAdded() {
+ this.fetch();
+ },
+ onUserRemoved() {
+ this.fetch();
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
new file mode 100644
index 0000000000..d082610132
--- /dev/null
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -0,0 +1,69 @@
+<template>
+<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
+ <span slot="header">%fa:list% リスト</span>
+
+ <div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="_darkmode_">
+ <button class="ui" @click="add">リストを作成</button>
+ <a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a>
+ </div>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ lists: []
+ };
+ },
+ mounted() {
+ (this as any).api('users/lists/list').then(lists => {
+ this.fetching = false;
+ this.lists = lists;
+ });
+ },
+ methods: {
+ add() {
+ (this as any).apis.input({
+ title: 'リスト名',
+ }).then(async title => {
+ const list = await (this as any).api('users/lists/create', {
+ title
+ });
+
+ this.$emit('choosen', list);
+ });
+ },
+ choice(list) {
+ this.$emit('choosen', list);
+ },
+ close() {
+ (this as any).$refs.window.close();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+
+root(isDark)
+ padding 16px
+
+ > button
+ margin-bottom 16px
+
+ > a
+ display block
+ padding 16px
+ border solid 1px isDark ? #1c2023 : #eee
+ border-radius 4px
+
+[data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"][data-darkmode]
+ root(true)
+
+[data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"]:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue
index a08e76f573..e8f4c94d42 100644
--- a/src/client/app/desktop/views/components/users-list.vue
+++ b/src/client/app/desktop/views/components/users-list.vue
@@ -2,8 +2,8 @@
<div class="mk-users-list">
<nav>
<div>
- <span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span>
- <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span>
+ <span :data-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span>
+ <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span>
</div>
</nav>
<div class="users" v-if="!fetching && users.length != 0">
@@ -98,7 +98,7 @@ export default Vue.extend({
*
pointer-events none
- &[data-is-active]
+ &[data-active]
font-weight bold
color $theme-color
border-color $theme-color
diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
index c3fac1399d..926d7702b9 100644
--- a/src/client/app/desktop/views/components/widget-container.vue
+++ b/src/client/app/desktop/views/components/widget-container.vue
@@ -58,7 +58,7 @@ root(isDark)
box-shadow 0 1px rgba(0, 0, 0, 0.07)
> [data-fa]
- margin-right 4px
+ margin-right 6px
&:empty
display none
diff --git a/src/client/app/desktop/views/pages/user-list.users.vue b/src/client/app/desktop/views/pages/user-list.users.vue
new file mode 100644
index 0000000000..63070ed609
--- /dev/null
+++ b/src/client/app/desktop/views/pages/user-list.users.vue
@@ -0,0 +1,131 @@
+<template>
+<div>
+ <mk-widget-container>
+ <template slot="header">%fa:users% ユーザー</template>
+ <button slot="func" title="ユーザーを追加" @click="add">%fa:plus%</button>
+
+ <div data-id="d0b63759-a822-4556-a5ce-373ab966e08a">
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw% %i18n:common.loading%<mk-ellipsis/></p>
+ <template v-else-if="users.length != 0">
+ <div class="user" v-for="_user in users">
+ <router-link class="avatar-anchor" :to="_user | userPage">
+ <img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
+ </router-link>
+ <div class="body">
+ <router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
+ <p class="username">@{{ _user | acct }}</p>
+ </div>
+ </div>
+ </template>
+ <p class="empty" v-else>%i18n:@no-one%</p>
+ </div>
+ </mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ list: {
+ type: Object,
+ required: true
+ }
+ },
+ data() {
+ return {
+ fetching: true,
+ users: []
+ };
+ },
+ mounted() {
+ (this as any).api('users/show', {
+ userIds: this.list.userIds
+ }).then(users => {
+ this.users = users;
+ this.fetching = false;
+ });
+ },
+ methods: {
+ add() {
+ (this as any).apis.input({
+ title: 'ユーザー名',
+ }).then(async username => {
+ const user = await (this as any).api('users/show', {
+ username
+ });
+
+ (this as any).api('users/lists/push', {
+ listId: this.list.id,
+ userId: user.id
+ });
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ > .user
+ padding 16px
+ border-bottom solid 1px isDark ? #1c2023 : #eee
+
+ &:last-child
+ border-bottom none
+
+ &:after
+ content ""
+ display block
+ clear both
+
+ > .avatar-anchor
+ display block
+ float left
+ margin 0 12px 0 0
+
+ > .avatar
+ display block
+ width 42px
+ height 42px
+ margin 0
+ border-radius 8px
+ vertical-align bottom
+
+ > .body
+ float left
+ width calc(100% - 54px)
+
+ > .name
+ margin 0
+ font-size 16px
+ line-height 24px
+ color isDark ? #fff : #555
+
+ > .username
+ display block
+ margin 0
+ font-size 15px
+ line-height 16px
+ color isDark ? #606984 : #ccc
+
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > .fetching
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+[data-id="d0b63759-a822-4556-a5ce-373ab966e08a"][data-darkmode]
+ root(true)
+
+[data-id="d0b63759-a822-4556-a5ce-373ab966e08a"]:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/desktop/views/pages/user-list.vue b/src/client/app/desktop/views/pages/user-list.vue
new file mode 100644
index 0000000000..2241b84e5e
--- /dev/null
+++ b/src/client/app/desktop/views/pages/user-list.vue
@@ -0,0 +1,71 @@
+<template>
+<mk-ui>
+ <div v-if="!fetching" data-id="02010e15-cc48-4245-8636-16078a9b623c">
+ <div>
+ <div><h1>{{ list.title }}</h1></div>
+ <x-users :list="list"/>
+ </div>
+ <main>
+ <mk-user-list-timeline :list="list"/>
+ </main>
+ </div>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XUsers from './user-list.users.vue';
+
+export default Vue.extend({
+ components: {
+ XUsers
+ },
+ data() {
+ return {
+ fetching: true,
+ list: null
+ };
+ },
+ watch: {
+ $route: 'fetch'
+ },
+ mounted() {
+ this.fetch();
+ },
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this as any).api('users/lists/show', {
+ listId: this.$route.params.list
+ }).then(list => {
+ this.list = list;
+ this.fetching = false;
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+[data-id="02010e15-cc48-4245-8636-16078a9b623c"]
+ display flex
+ justify-content center
+ margin 0 auto
+ max-width 1200px
+
+ > main
+ > div > div
+ > *:not(:last-child)
+ margin-bottom 16px
+
+ > main
+ padding 16px
+ width calc(100% - 275px * 2)
+
+ > div
+ width 275px
+ margin 0
+ padding 16px 0 16px 16px
+
+</style>
diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue
index 774f300a38..64acbd86b3 100644
--- a/src/client/app/desktop/views/pages/user/user.profile.vue
+++ b/src/client/app/desktop/views/pages/user/user.profile.vue
@@ -3,15 +3,18 @@
<div class="friend-form" v-if="os.isSignedIn && os.i.id != user.id">
<mk-follow-button :user="user" size="big"/>
<p class="followed" v-if="user.isFollowed">%i18n:@follows-you%</p>
- <p class="stalk">
- <span v-if="user.isStalking">%i18n:@stalking% <a @click="unstalk">%i18n:@unstalk%</a></span>
- <span v-if="!user.isStalking"><a @click="stalk">%i18n:@stalk%</a></span>
- </p>
- <p class="mute">
- <span v-if="user.isMuted">%i18n:@muted% <a @click="unmute">%i18n:@unmute%</a></span>
- <span v-if="!user.isMuted"><a @click="mute">%i18n:@mute%</a></span>
+ <p class="stalk" v-if="user.isFollowing">
+ <span v-if="user.isStalking">%i18n:@stalking% <a @click="unstalk">%fa:meh% %i18n:@unstalk%</a></span>
+ <span v-if="!user.isStalking"><a @click="stalk">%fa:user-secret% %i18n:@stalk%</a></span>
</p>
</div>
+ <div class="action-form">
+ <button class="mute ui" @click="user.isMuted ? unmute() : mute()">
+ <span v-if="user.isMuted">%fa:eye% %i18n:@unmute%</span>
+ <span v-if="!user.isMuted">%fa:eye-slash% %i18n:@mute%</span>
+ </button>
+ <button class="mute ui" @click="list">%fa:list% リストに追加</button>
+ </div>
<div class="description" v-if="user.description">{{ user.description }}</div>
<div class="birthday" v-if="user.host === null && user.profile.birthday">
<p>%fa:birthday-cake%{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p>
@@ -32,6 +35,7 @@ import Vue from 'vue';
import * as age from 's-age';
import MkFollowingWindow from '../../components/following-window.vue';
import MkFollowersWindow from '../../components/followers-window.vue';
+import MkUserListsWindow from '../../components/user-lists-window.vue';
export default Vue.extend({
props: ['user'],
@@ -91,6 +95,21 @@ export default Vue.extend({
}, () => {
alert('error');
});
+ },
+
+ list() {
+ const w = (this as any).os.new(MkUserListsWindow);
+ w.$once('choosen', async list => {
+ w.close();
+ await (this as any).api('users/lists/push', {
+ listId: list.id,
+ userId: this.user.id
+ });
+ (this as any).apis.dialog({
+ title: 'Done!',
+ text: `${this.user.name}を${list.title}に追加しました。`
+ });
+ });
}
}
});
@@ -107,11 +126,9 @@ export default Vue.extend({
> .friend-form
padding 16px
+ text-align center
border-top solid 1px #eee
- > .mk-big-follow-button
- width 100%
-
> .followed
margin 12px 0 0 0
padding 0
@@ -122,6 +139,20 @@ export default Vue.extend({
background #eefaff
border-radius 4px
+ > .stalk
+ margin 12px 0 0 0
+
+ > .action-form
+ padding 16px
+ text-align center
+ border-top solid 1px #eee
+
+ > *
+ width 100%
+
+ &:not(:last-child)
+ margin-bottom 12px
+
> .description
padding 16px
color #555
diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue
index 55d6072a9d..9c9840c190 100644
--- a/src/client/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/client/app/desktop/views/pages/user/user.timeline.vue
@@ -1,42 +1,36 @@
<template>
<div class="timeline">
<header>
- <span :data-is-active="mode == 'default'" @click="mode = 'default'">投稿</span>
- <span :data-is-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span>
- <span :data-is-active="mode == 'with-media'" @click="mode = 'with-media'">メディア</span>
+ <span :data-active="mode == 'default'" @click="mode = 'default'">投稿</span>
+ <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span>
+ <span :data-active="mode == 'with-media'" @click="mode = 'with-media'">メディア</span>
</header>
<div class="loading" v-if="fetching">
<mk-ellipsis-icon/>
</div>
- <p class="empty" v-if="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
- <mk-notes ref="timeline" :notes="notes">
- <div slot="footer">
- <template v-if="!moreFetching">%fa:moon%</template>
- <template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
- </div>
+ <mk-notes ref="timeline" :more="existMore ? more : null">
+ <p class="empty" slot="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
</mk-notes>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
+
+const fetchLimit = 10;
+
export default Vue.extend({
props: ['user'],
data() {
return {
fetching: true,
moreFetching: false,
+ existMore: false,
mode: 'default',
unreadCount: 0,
- notes: [],
date: null
};
},
- computed: {
- empty(): boolean {
- return this.notes.length == 0;
- }
- },
watch: {
mode() {
this.fetch();
@@ -44,13 +38,11 @@ export default Vue.extend({
},
mounted() {
document.addEventListener('keydown', this.onDocumentKeydown);
- window.addEventListener('scroll', this.onScroll);
this.fetch(() => this.$emit('loaded'));
},
beforeDestroy() {
document.removeEventListener('keydown', this.onDocumentKeydown);
- window.removeEventListener('scroll', this.onScroll);
},
methods: {
onDocumentKeydown(e) {
@@ -61,36 +53,43 @@ export default Vue.extend({
}
},
fetch(cb?) {
- (this as any).api('users/notes', {
- userId: this.user.id,
- untilDate: this.date ? this.date.getTime() : undefined,
- includeReplies: this.mode == 'with-replies',
- withMedia: this.mode == 'with-media'
- }).then(notes => {
- this.notes = notes;
- this.fetching = false;
- if (cb) cb();
- });
+ this.fetching = true;
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('users/notes', {
+ userId: this.user.id,
+ limit: fetchLimit + 1,
+ untilDate: this.date ? this.date.getTime() : undefined,
+ includeReplies: this.mode == 'with-replies',
+ withMedia: this.mode == 'with-media'
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ if (cb) cb();
+ }, rej);
+ }));
},
more() {
- if (this.moreFetching || this.fetching || this.notes.length == 0) return;
this.moreFetching = true;
(this as any).api('users/notes', {
userId: this.user.id,
+ limit: fetchLimit + 1,
includeReplies: this.mode == 'with-replies',
withMedia: this.mode == 'with-media',
- untilId: this.notes[this.notes.length - 1].id
+ untilId: (this.$refs.timeline as any).tail().id
}).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
- this.notes = this.notes.concat(notes);
});
},
- onScroll() {
- const current = window.scrollY + window.innerHeight;
- if (current > document.body.offsetHeight - 16/*遊び*/) {
- this.more();
- }
- },
warp(date) {
this.date = date;
this.fetch();
@@ -115,7 +114,7 @@ export default Vue.extend({
font-size 18px
color #555
- &:not([data-is-active])
+ &:not([data-active])
color $theme-color
cursor pointer
diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts
index 9346700304..5ed8427b05 100644
--- a/src/client/app/mobile/views/components/index.ts
+++ b/src/client/app/mobile/views/components/index.ts
@@ -1,7 +1,6 @@
import Vue from 'vue';
import ui from './ui.vue';
-import timeline from './timeline.vue';
import note from './note.vue';
import notes from './notes.vue';
import mediaImage from './media-image.vue';
@@ -20,11 +19,11 @@ import notificationPreview from './notification-preview.vue';
import usersList from './users-list.vue';
import userPreview from './user-preview.vue';
import userTimeline from './user-timeline.vue';
+import userListTimeline from './user-list-timeline.vue';
import activity from './activity.vue';
import widgetContainer from './widget-container.vue';
Vue.component('mk-ui', ui);
-Vue.component('mk-timeline', timeline);
Vue.component('mk-note', note);
Vue.component('mk-notes', notes);
Vue.component('mk-media-image', mediaImage);
@@ -43,5 +42,6 @@ Vue.component('mk-notification-preview', notificationPreview);
Vue.component('mk-users-list', usersList);
Vue.component('mk-user-preview', userPreview);
Vue.component('mk-user-timeline', userTimeline);
+Vue.component('mk-user-list-timeline', userListTimeline);
Vue.component('mk-activity', activity);
Vue.component('mk-widget-container', widgetContainer);
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index 999ab566ac..703b51d678 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,22 +24,41 @@
</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>
<script lang="ts">
import Vue from 'vue';
+import getNoteSummary from '../../../../../renderers/get-note-summary';
+
+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: [],
+ unreadCount: 0,
+ fetching: true,
+ moreFetching: false
+ };
+ },
+
computed: {
_notes(): any[] {
return (this.notes as any).map(note => {
@@ -38,9 +70,127 @@ export default Vue.extend({
});
}
},
+
+ mounted() {
+ document.addEventListener('visibilitychange', this.onVisibilitychange, false);
+ window.addEventListener('scroll', this.onScroll);
+ },
+
+ beforeDestroy() {
+ document.removeEventListener('visibilitychange', this.onVisibilitychange);
+ 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 ((document.hidden || !this.isScrollTop()) && note.userId !== (this as any).os.i.id) {
+ this.unreadCount++;
+ document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
+ }
+
+ 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;
+ },
+
+ clearNotification() {
+ this.unreadCount = 0;
+ document.title = 'Misskey';
+ },
+
+ onVisibilitychange() {
+ if (!document.hidden) {
+ this.clearNotification();
+ }
+ },
+
+ onScroll() {
+ if (this.isScrollTop()) {
+ this.releaseQueue();
+ this.clearNotification();
+ }
+
+ 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 +229,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
deleted file mode 100644
index f56667bed5..0000000000
--- a/src/client/app/mobile/views/components/timeline.vue
+++ /dev/null
@@ -1,190 +0,0 @@
-<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">
- %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>
-
-<script lang="ts">
-import Vue from 'vue';
-
-const fetchLimit = 10;
-const displayLimit = 30;
-
-export default Vue.extend({
- props: {
- date: {
- type: Date,
- required: false,
- default: null
- }
- },
-
- data() {
- return {
- fetching: true,
- moreFetching: false,
- notes: [],
- queue: [],
- existMore: false,
- connection: null,
- connectionId: null
- };
- },
-
- computed: {
- alone(): boolean {
- return (this as any).os.i.followingCount == 0;
- },
-
- canFetchMore(): boolean {
- return !this.moreFetching && !this.fetching && this.notes.length > 0 && this.existMore;
- }
- },
-
- mounted() {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
-
- this.connection.on('note', this.onNote);
- this.connection.on('follow', this.onChangeFollowing);
- this.connection.on('unfollow', this.onChangeFollowing);
-
- window.addEventListener('scroll', this.onScroll);
-
- this.fetch();
- },
-
- beforeDestroy() {
- this.connection.off('note', this.onNote);
- 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();
- });
- },
-
- more() {
- this.moreFetching = true;
- (this as any).api('notes/timeline', {
- limit: fetchLimit + 1,
- untilId: this.notes[this.notes.length - 1].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);
- 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);
- }
- },
-
- onChangeFollowing() {
- this.fetch();
- },
-
- onScroll() {
- if (this.isScrollTop()) {
- this.releaseQueue();
- }
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-@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-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue
new file mode 100644
index 0000000000..ee983a969c
--- /dev/null
+++ b/src/client/app/mobile/views/components/user-list-timeline.vue
@@ -0,0 +1,93 @@
+<template>
+<div>
+ <mk-notes ref="timeline" :more="existMore ? more : null"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { UserListStream } from '../../../common/scripts/streaming/user-list';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ props: ['list'],
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null
+ };
+ },
+ watch: {
+ $route: 'init'
+ },
+ mounted() {
+ this.init();
+ },
+ beforeDestroy() {
+ this.connection.close();
+ },
+ methods: {
+ init() {
+ if (this.connection) this.connection.close();
+ this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id);
+ this.connection.on('note', this.onNote);
+ this.connection.on('userAdded', this.onUserAdded);
+ this.connection.on('userRemoved', this.onUserRemoved);
+
+ this.fetch();
+ },
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/user-list-timeline', {
+ listId: this.list.id,
+ 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');
+ }, rej);
+ }));
+ },
+ more() {
+ this.moreFetching = true;
+
+ (this as any).api('notes/list-timeline', {
+ listId: this.list.id,
+ limit: fetchLimit + 1,
+ 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();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+ },
+ onNote(note) {
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ },
+ onUserAdded() {
+ this.fetch();
+ },
+ onUserRemoved() {
+ this.fetch();
+ }
+ }
+});
+</script>
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/client/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue
index 8fa7a9cbe6..67a38a8955 100644
--- a/src/client/app/mobile/views/components/users-list.vue
+++ b/src/client/app/mobile/views/components/users-list.vue
@@ -1,8 +1,8 @@
<template>
<div class="mk-users-list">
<nav>
- <span :data-is-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span>
- <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span>
+ <span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span>
+ <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span>
</nav>
<div class="users" v-if="!fetching && users.length != 0">
<mk-user-preview v-for="u in users" :user="u" :key="u.id"/>
@@ -85,7 +85,7 @@ export default Vue.extend({
color #657786
border-bottom solid 2px transparent
- &[data-is-active]
+ &[data-active]
font-weight bold
color $theme-color
border-color $theme-color
diff --git a/src/client/app/mobile/views/pages/dashboard.vue b/src/client/app/mobile/views/pages/dashboard.vue
new file mode 100644
index 0000000000..14779da650
--- /dev/null
+++ b/src/client/app/mobile/views/pages/dashboard.vue
@@ -0,0 +1,196 @@
+<template>
+<mk-ui>
+ <span slot="header">%fa:home%ダッシュボード</span>
+ <template slot="func">
+ <button @click="customizing = !customizing">%fa:cog%</button>
+ </template>
+ <main>
+ <template v-if="customizing">
+ <header>
+ <select v-model="widgetAdderSelected">
+ <option value="profile">プロフィール</option>
+ <option value="calendar">カレンダー</option>
+ <option value="activity">アクティビティ</option>
+ <option value="rss">RSSリーダー</option>
+ <option value="photo-stream">フォトストリーム</option>
+ <option value="slideshow">スライドショー</option>
+ <option value="version">バージョン</option>
+ <option value="access-log">アクセスログ</option>
+ <option value="server">サーバー情報</option>
+ <option value="donation">寄付のお願い</option>
+ <option value="nav">ナビゲーション</option>
+ <option value="tips">ヒント</option>
+ </select>
+ <button @click="addWidget">追加</button>
+ <p><a @click="hint">カスタマイズのヒント</a></p>
+ </header>
+ <x-draggable
+ :list="widgets"
+ :options="{ handle: '.handle', animation: 150 }"
+ @sort="onWidgetSort"
+ >
+ <div v-for="widget in widgets" class="customize-container" :key="widget.id">
+ <header>
+ <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
+ </header>
+ <div @click="widgetFunc(widget.id)">
+ <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/>
+ </div>
+ </div>
+ </x-draggable>
+ </template>
+ <template v-else>
+ <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
+ </template>
+ </main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as XDraggable from 'vuedraggable';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+ components: {
+ XDraggable
+ },
+ data() {
+ return {
+ showNav: false,
+ widgets: [],
+ customizing: false,
+ widgetAdderSelected: null
+ };
+ },
+ created() {
+ if ((this as any).os.i.clientSettings.mobileHome == null) {
+ Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{
+ name: 'calendar',
+ id: 'a', data: {}
+ }, {
+ name: 'activity',
+ id: 'b', data: {}
+ }, {
+ name: 'rss',
+ id: 'c', data: {}
+ }, {
+ name: 'photo-stream',
+ id: 'd', data: {}
+ }, {
+ name: 'donation',
+ id: 'e', data: {}
+ }, {
+ name: 'nav',
+ id: 'f', data: {}
+ }, {
+ name: 'version',
+ id: 'g', data: {}
+ }]);
+ this.widgets = (this as any).os.i.clientSettings.mobileHome;
+ this.saveHome();
+ } else {
+ this.widgets = (this as any).os.i.clientSettings.mobileHome;
+ }
+
+ this.$watch('os.i.clientSettings', i => {
+ this.widgets = (this as any).os.i.clientSettings.mobileHome;
+ }, {
+ deep: true
+ });
+ },
+
+ mounted() {
+ document.title = 'Misskey';
+ document.documentElement.style.background = '#313a42';
+ },
+
+ methods: {
+ onHomeUpdated(data) {
+ if (data.home) {
+ (this as any).os.i.clientSettings.mobileHome = data.home;
+ this.widgets = data.home;
+ } else {
+ const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id);
+ if (w != null) {
+ w.data = data.data;
+ this.$refs[w.id][0].preventSave = true;
+ this.$refs[w.id][0].props = w.data;
+ this.widgets = (this as any).os.i.clientSettings.mobileHome;
+ }
+ }
+ },
+ hint() {
+ alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。');
+ },
+ widgetFunc(id) {
+ const w = this.$refs[id][0];
+ if (w.func) w.func();
+ },
+ onWidgetSort() {
+ this.saveHome();
+ },
+ addWidget() {
+ const widget = {
+ name: this.widgetAdderSelected,
+ id: uuid(),
+ data: {}
+ };
+
+ this.widgets.unshift(widget);
+ this.saveHome();
+ },
+ removeWidget(widget) {
+ this.widgets = this.widgets.filter(w => w.id != widget.id);
+ this.saveHome();
+ },
+ saveHome() {
+ (this as any).os.i.clientSettings.mobileHome = this.widgets;
+ (this as any).api('i/update_mobile_home', {
+ home: this.widgets
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+main
+ margin 0 auto
+ max-width 500px
+
+ @media (min-width 500px)
+ padding 8px
+
+ > header
+ padding 8px
+ background #fff
+
+ .widget
+ margin 8px
+
+ .customize-container
+ margin 8px
+ background #fff
+
+ > header
+ line-height 32px
+ background #eee
+
+ > .handle
+ padding 0 8px
+
+ > .remove
+ position absolute
+ top 0
+ right 0
+ padding 0 8px
+ line-height 32px
+
+ > div
+ padding 8px
+
+ > *
+ pointer-events none
+
+</style>
diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
new file mode 100644
index 0000000000..5f4bd6dcd8
--- /dev/null
+++ b/src/client/app/mobile/views/pages/home.timeline.vue
@@ -0,0 +1,149 @@
+<template>
+<div>
+ <mk-friends-maker v-if="src == 'home' && alone" style="margin-bottom:8px"/>
+
+ <mk-notes ref="timeline" :more="existMore ? more : null">
+ <div slot="empty">
+ %fa:R comments%
+ %i18n:@empty%
+ </div>
+ </mk-notes>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ props: {
+ src: {
+ type: String,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null,
+ connectionId: null,
+ unreadCount: 0,
+ date: null
+ };
+ },
+
+ computed: {
+ alone(): boolean {
+ return (this as any).os.i.followingCount == 0;
+ },
+
+ stream(): any {
+ return this.src == 'home'
+ ? (this as any).os.stream
+ : this.src == 'local'
+ ? (this as any).os.streams.localTimelineStream
+ : (this as any).os.streams.globalTimelineStream;
+ },
+
+ endpoint(): string {
+ return this.src == 'home'
+ ? 'notes/timeline'
+ : this.src == 'local'
+ ? 'notes/local-timeline'
+ : 'notes/global-timeline';
+ },
+
+ canFetchMore(): boolean {
+ return !this.moreFetching && !this.fetching && this.existMore;
+ }
+ },
+
+ mounted() {
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
+
+ this.connection.on('note', this.onNote);
+ if (this.src == 'home') {
+ this.connection.on('follow', this.onChangeFollowing);
+ this.connection.on('unfollow', this.onChangeFollowing);
+ }
+
+ this.fetch();
+ },
+
+ beforeDestroy() {
+ this.connection.off('note', this.onNote);
+ if (this.src == 'home') {
+ this.connection.off('follow', this.onChangeFollowing);
+ this.connection.off('unfollow', this.onChangeFollowing);
+ }
+ this.stream.dispose(this.connectionId);
+ },
+
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api(this.endpoint, {
+ limit: fetchLimit + 1,
+ untilDate: this.date ? this.date.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;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
+ },
+
+ more() {
+ if (!this.canFetchMore) return;
+
+ this.moreFetching = true;
+
+ (this as any).api(this.endpoint, {
+ limit: fetchLimit + 1,
+ 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();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+ },
+
+ onNote(note) {
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ },
+
+ onChangeFollowing() {
+ this.fetch();
+ },
+
+ focus() {
+ (this.$refs.timeline as any).focus();
+ },
+
+ warp(date) {
+ this.date = date;
+ this.fetch();
+ }
+ }
+});
+</script>
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index 3d94dd7ce6..92d34fa83b 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -1,59 +1,42 @@
<template>
<mk-ui>
- <span slot="header" @click="showTl = !showTl">
- <template v-if="showTl">%fa:home%%i18n:@timeline%</template>
- <template v-else>%fa:home%ウィジェット</template>
+ <span slot="header" @click="showNav = true">
+ <span>
+ <span v-if="src == 'home'">%fa:home%ホーム</span>
+ <span v-if="src == 'local'">%fa:R comments%ローカル</span>
+ <span v-if="src == 'global'">%fa:globe%グローバル</span>
+ <span v-if="src.startsWith('list')">%fa:list%{{ list.title }}</span>
+ </span>
<span style="margin-left:8px">
- <template v-if="showTl">%fa:angle-down%</template>
+ <template v-if="!showNav">%fa:angle-down%</template>
<template v-else>%fa:angle-up%</template>
</span>
</span>
+
<template slot="func">
- <button @click="fn" v-if="showTl">%fa:pencil-alt%</button>
- <button @click="customizing = !customizing" v-else>%fa:cog%</button>
+ <button @click="fn">%fa:pencil-alt%</button>
</template>
+
<main>
- <div class="tl">
- <mk-timeline @loaded="onLoaded" v-show="showTl"/>
+ <div class="nav" v-if="showNav">
+ <div class="bg" @click="showNav = false"></div>
+ <div class="body">
+ <div>
+ <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span>
+ <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span>
+ <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span>
+ <template v-if="lists">
+ <span v-for="l in lists" :data-active="src == 'list:' + l.id" @click="src = 'list:' + l.id; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
+ </template>
+ </div>
+ </div>
</div>
- <div class="widgets" v-show="!showTl">
- <template v-if="customizing">
- <header>
- <select v-model="widgetAdderSelected">
- <option value="profile">プロフィール</option>
- <option value="calendar">カレンダー</option>
- <option value="activity">アクティビティ</option>
- <option value="rss">RSSリーダー</option>
- <option value="photo-stream">フォトストリーム</option>
- <option value="slideshow">スライドショー</option>
- <option value="version">バージョン</option>
- <option value="access-log">アクセスログ</option>
- <option value="server">サーバー情報</option>
- <option value="donation">寄付のお願い</option>
- <option value="nav">ナビゲーション</option>
- <option value="tips">ヒント</option>
- </select>
- <button @click="addWidget">追加</button>
- <p><a @click="hint">カスタマイズのヒント</a></p>
- </header>
- <x-draggable
- :list="widgets"
- :options="{ handle: '.handle', animation: 150 }"
- @sort="onWidgetSort"
- >
- <div v-for="widget in widgets" class="customize-container" :key="widget.id">
- <header>
- <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
- </header>
- <div @click="widgetFunc(widget.id)">
- <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/>
- </div>
- </div>
- </x-draggable>
- </template>
- <template v-else>
- <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
- </template>
+
+ <div class="tl">
+ <x-tl v-if="src == 'home'" ref="tl" key="home" src="home" @loaded="onLoaded"/>
+ <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/>
+ <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
+ <mk-user-list-timeline v-if="src.startsWith('list:')" ref="tl" key="list" :list="list"/>
</div>
</main>
</mk-ui>
@@ -61,144 +44,53 @@
<script lang="ts">
import Vue from 'vue';
-import * as XDraggable from 'vuedraggable';
-import * as uuid from 'uuid';
import Progress from '../../../common/scripts/loading';
-import getNoteSummary from '../../../../../renderers/get-note-summary';
+import XTl from './home.timeline.vue';
export default Vue.extend({
components: {
- XDraggable
+ XTl
},
+
data() {
return {
- connection: null,
- connectionId: null,
- unreadCount: 0,
- showTl: true,
- widgets: [],
- customizing: false,
- widgetAdderSelected: null
+ src: 'home',
+ list: null,
+ lists: null,
+ showNav: false
};
},
- created() {
- if ((this as any).os.i.clientSettings.mobileHome == null) {
- Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{
- name: 'calendar',
- id: 'a', data: {}
- }, {
- name: 'activity',
- id: 'b', data: {}
- }, {
- name: 'rss',
- id: 'c', data: {}
- }, {
- name: 'photo-stream',
- id: 'd', data: {}
- }, {
- name: 'donation',
- id: 'e', data: {}
- }, {
- name: 'nav',
- id: 'f', data: {}
- }, {
- name: 'version',
- id: 'g', data: {}
- }]);
- this.widgets = (this as any).os.i.clientSettings.mobileHome;
- this.saveHome();
- } else {
- this.widgets = (this as any).os.i.clientSettings.mobileHome;
- }
- this.$watch('os.i.clientSettings', i => {
- this.widgets = (this as any).os.i.clientSettings.mobileHome;
- }, {
- deep: true
- });
+ watch: {
+ src() {
+ this.showNav = false;
+ },
+
+ showNav(v) {
+ if (v && this.lists === null) {
+ (this as any).api('users/lists/list').then(lists => {
+ this.lists = lists;
+ });
+ }
+ }
},
+
mounted() {
document.title = 'Misskey';
document.documentElement.style.background = '#313a42';
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
-
- this.connection.on('note', this.onStreamNote);
- this.connection.on('mobile_home_updated', this.onHomeUpdated);
- document.addEventListener('visibilitychange', this.onVisibilitychange, false);
-
Progress.start();
},
- beforeDestroy() {
- this.connection.off('note', this.onStreamNote);
- this.connection.off('mobile_home_updated', this.onHomeUpdated);
- (this as any).os.stream.dispose(this.connectionId);
- document.removeEventListener('visibilitychange', this.onVisibilitychange);
- },
+
methods: {
fn() {
(this as any).apis.post();
},
+
onLoaded() {
Progress.done();
},
- onStreamNote(note) {
- if (document.hidden && note.userId !== (this as any).os.i.id) {
- this.unreadCount++;
- document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
- }
- },
- onVisibilitychange() {
- if (!document.hidden) {
- this.unreadCount = 0;
- document.title = 'Misskey';
- }
- },
- onHomeUpdated(data) {
- if (data.home) {
- (this as any).os.i.clientSettings.mobileHome = data.home;
- this.widgets = data.home;
- } else {
- const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id);
- if (w != null) {
- w.data = data.data;
- this.$refs[w.id][0].preventSave = true;
- this.$refs[w.id][0].props = w.data;
- this.widgets = (this as any).os.i.clientSettings.mobileHome;
- }
- }
- },
- hint() {
- alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。');
- },
- widgetFunc(id) {
- const w = this.$refs[id][0];
- if (w.func) w.func();
- },
- onWidgetSort() {
- this.saveHome();
- },
- addWidget() {
- const widget = {
- name: this.widgetAdderSelected,
- id: uuid(),
- data: {}
- };
- this.widgets.unshift(widget);
- this.saveHome();
- },
- removeWidget(widget) {
- this.widgets = this.widgets.filter(w => w.id != widget.id);
- this.saveHome();
- },
- saveHome() {
- (this as any).os.i.clientSettings.mobileHome = this.widgets;
- (this as any).api('i/update_mobile_home', {
- home: this.widgets
- });
- },
warp() {
}
@@ -207,53 +99,75 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
+@import '~const.styl'
+
main
+ > .nav
+ > .bg
+ position fixed
+ z-index 10000
+ top 0
+ left 0
+ width 100%
+ height 100%
+ background rgba(#000, 0.5)
- > .tl
- > .mk-timeline
- max-width 600px
+ > .body
+ position fixed
+ z-index 10001
+ top 56px
+ left 0
+ right 0
+ width 300px
margin 0 auto
- padding 8px
-
- @media (min-width 500px)
- padding 16px
-
- > .widgets
- margin 0 auto
- max-width 500px
+ background #fff
+ border-radius 8px
+ box-shadow 0 0 16px rgba(0, 0, 0, 0.1)
- @media (min-width 500px)
- padding 8px
+ $balloon-size = 16px
- > header
- padding 8px
- background #fff
+ &:before
+ content ""
+ display block
+ position absolute
+ top -($balloon-size * 2)
+ left s('calc(50% - %s)', $balloon-size)
+ border-top solid $balloon-size transparent
+ border-left solid $balloon-size transparent
+ border-right solid $balloon-size transparent
+ border-bottom solid $balloon-size $border-color
- .widget
- margin 8px
+ &:after
+ content ""
+ display block
+ position absolute
+ top -($balloon-size * 2) + 1.5px
+ left s('calc(50% - %s)', $balloon-size)
+ border-top solid $balloon-size transparent
+ border-left solid $balloon-size transparent
+ border-right solid $balloon-size transparent
+ border-bottom solid $balloon-size #fff
- .customize-container
- margin 8px
- background #fff
+ > div
+ padding 8px 0
- > header
- line-height 32px
- background #eee
+ > *
+ display block
+ padding 8px 16px
- > .handle
- padding 0 8px
+ &[data-active]
+ color $theme-color-foreground
+ background $theme-color
- > .remove
- position absolute
- top 0
- right 0
- padding 0 8px
- line-height 32px
+ &:not([data-active]):hover
+ background #eee
- > div
- padding 8px
+ > .tl
+ max-width 600px
+ margin 0 auto
+ padding 8px
- > *
- pointer-events none
+ @media (min-width 500px)
+ padding 16px
</style>
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 3ff9057f73..73b8e24315 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -45,9 +45,9 @@
</header>
<nav>
<div class="nav-container">
- <a :data-is-active="page == 'home'" @click="page = 'home'">%i18n:@overview%</a>
- <a :data-is-active="page == 'notes'" @click="page = 'notes'">%i18n:@timeline%</a>
- <a :data-is-active="page == 'media'" @click="page = 'media'">%i18n:@media%</a>
+ <a :data-active="page == 'home'" @click="page = 'home'">%i18n:@overview%</a>
+ <a :data-active="page == 'notes'" @click="page = 'notes'">%i18n:@timeline%</a>
+ <a :data-active="page == 'media'" @click="page = 'media'">%i18n:@media%</a>
</div>
</nav>
<div class="body">
@@ -256,7 +256,7 @@ main
color #657786
border-bottom solid 2px transparent
- &[data-is-active]
+ &[data-active]
font-weight bold
color $theme-color
border-color $theme-color
diff --git a/src/models/note-reaction.ts b/src/models/note-reaction.ts
index 7891ebdf17..f78b0d9d01 100644
--- a/src/models/note-reaction.ts
+++ b/src/models/note-reaction.ts
@@ -1,5 +1,5 @@
import * as mongo from 'mongodb';
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import deepcopy = require('deepcopy');
import db from '../db/mongodb';
import Reaction from './note-reaction';
diff --git a/src/models/sw-subscription.ts b/src/models/sw-subscription.ts
index 621ac8a9b6..a38edd3a50 100644
--- a/src/models/sw-subscription.ts
+++ b/src/models/sw-subscription.ts
@@ -38,4 +38,3 @@ export async function deleteSwSubscription(swSubscription: string | mongo.Object
_id: s._id
});
}
-
diff --git a/src/models/user-list.ts b/src/models/user-list.ts
new file mode 100644
index 0000000000..7100fced7e
--- /dev/null
+++ b/src/models/user-list.ts
@@ -0,0 +1,67 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import db from '../db/mongodb';
+
+const UserList = db.get<IUserList>('userList');
+export default UserList;
+
+export interface IUserList {
+ _id: mongo.ObjectID;
+ createdAt: Date;
+ title: string;
+ userId: mongo.ObjectID;
+ userIds: mongo.ObjectID[];
+}
+
+/**
+ * UserListを物理削除します
+ */
+export async function deleteUserList(userList: string | mongo.ObjectID | IUserList) {
+ let u: IUserList;
+
+ // Populate
+ if (mongo.ObjectID.prototype.isPrototypeOf(userList)) {
+ u = await UserList.findOne({
+ _id: userList
+ });
+ } else if (typeof userList === 'string') {
+ u = await UserList.findOne({
+ _id: new mongo.ObjectID(userList)
+ });
+ } else {
+ u = userList as IUserList;
+ }
+
+ if (u == null) return;
+
+ // このUserListを削除
+ await UserList.remove({
+ _id: u._id
+ });
+}
+
+export const pack = (
+ userList: string | mongo.ObjectID | IUserList
+) => new Promise<any>(async (resolve, reject) => {
+ let _userList: any;
+
+ if (mongo.ObjectID.prototype.isPrototypeOf(userList)) {
+ _userList = await UserList.findOne({
+ _id: userList
+ });
+ } else if (typeof userList === 'string') {
+ _userList = await UserList.findOne({
+ _id: new mongo.ObjectID(userList)
+ });
+ } else {
+ _userList = deepcopy(userList);
+ }
+
+ if (!_userList) throw `invalid userList arg ${userList}`;
+
+ // Rename _id to id
+ _userList.id = _userList._id;
+ delete _userList._id;
+
+ resolve(_userList);
+});
diff --git a/src/models/user.ts b/src/models/user.ts
index ca1ca28937..0621b6e736 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -1,5 +1,6 @@
import * as mongo from 'mongodb';
import deepcopy = require('deepcopy');
+import sequential = require('promise-sequential');
import rap from '@prezzemolo/rap';
import db from '../db/mongodb';
import Note, { pack as packNote, deleteNote } from './note';
@@ -20,6 +21,7 @@ import FollowingLog, { deleteFollowingLog } from './following-log';
import FollowedLog, { deleteFollowedLog } from './followed-log';
import SwSubscription, { deleteSwSubscription } from './sw-subscription';
import Notification, { deleteNotification } from './notification';
+import UserList, { deleteUserList } from './user-list';
const User = db.get<IUser>('users');
@@ -166,9 +168,9 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
).map(x => deleteAccessToken(x)));
// このユーザーのNoteをすべて削除
- await Promise.all((
- await Note.find({ userId: u._id })
- ).map(x => deleteNote(x)));
+ //await sequential((
+ // await Note.find({ userId: u._id })
+ //).map(x => () => deleteNote(x)));
// このユーザーのNoteReactionをすべて削除
await Promise.all((
@@ -260,6 +262,20 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
await Notification.find({ notifierId: u._id })
).map(x => deleteNotification(x)));
+ // このユーザーのUserListをすべて削除
+ await Promise.all((
+ await UserList.find({ userId: u._id })
+ ).map(x => deleteUserList(x)));
+
+ // このユーザーが入っているすべてのUserListからこのユーザーを削除
+ await Promise.all((
+ await UserList.find({ userIds: u._id })
+ ).map(x =>
+ UserList.update({ _id: x._id }, {
+ $pull: { userIds: u._id }
+ })
+ ));
+
// このユーザーを削除
await User.remove({
_id: u._id
diff --git a/src/publishers/stream.ts b/src/publishers/stream.ts
index 2ecbfa0dd8..dcc03e39f1 100644
--- a/src/publishers/stream.ts
+++ b/src/publishers/stream.ts
@@ -25,6 +25,10 @@ class MisskeyEvent {
this.publish(`note-stream:${noteId}`, type, typeof value === 'undefined' ? null : value);
}
+ public publishUserListStream(listId: ID, type: string, value?: any): void {
+ this.publish(`user-list-stream:${listId}`, type, typeof value === 'undefined' ? null : value);
+ }
+
public publishMessagingStream(userId: ID, otherpartyId: ID, type: string, value?: any): void {
this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
}
@@ -69,6 +73,7 @@ export default ev.publishUserStream.bind(ev);
export const publishLocalTimelineStream = ev.publishLocalTimelineStream.bind(ev);
export const publishGlobalTimelineStream = ev.publishGlobalTimelineStream.bind(ev);
export const publishDriveStream = ev.publishDriveStream.bind(ev);
+export const publishUserListStream = ev.publishUserListStream.bind(ev);
export const publishNoteStream = ev.publishNoteStream.bind(ev);
export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev);
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index 3686918147..734b8273f1 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -415,6 +415,27 @@ const endpoints: Endpoint[] = [
},
{
+ name: 'users/lists/show',
+ withCredential: true,
+ kind: 'account-read'
+ },
+ {
+ name: 'users/lists/create',
+ withCredential: true,
+ kind: 'account-write'
+ },
+ {
+ name: 'users/lists/push',
+ withCredential: true,
+ kind: 'account-write'
+ },
+ {
+ name: 'users/lists/list',
+ withCredential: true,
+ kind: 'account-read'
+ },
+
+ {
name: 'following/create',
withCredential: true,
limit: {
@@ -504,6 +525,14 @@ const endpoints: Endpoint[] = [
}
},
{
+ name: 'notes/user-list-timeline',
+ withCredential: true,
+ limit: {
+ duration: ms('10minutes'),
+ max: 100
+ }
+ },
+ {
name: 'notes/mentions',
withCredential: true,
limit: {
diff --git a/src/server/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts
index cc2a48b53d..17bead2808 100644
--- a/src/server/api/endpoints/aggregation/posts.ts
+++ b/src/server/api/endpoints/aggregation/posts.ts
@@ -6,9 +6,6 @@ import Note from '../../../../models/note';
/**
* Aggregate notes
- *
- * @param {any} params
- * @return {Promise<any>}
*/
module.exports = params => new Promise(async (res, rej) => {
// Get 'limit' parameter
diff --git a/src/server/api/endpoints/aggregation/users.ts b/src/server/api/endpoints/aggregation/users.ts
index 19776ed297..b0a7632f24 100644
--- a/src/server/api/endpoints/aggregation/users.ts
+++ b/src/server/api/endpoints/aggregation/users.ts
@@ -6,9 +6,6 @@ import User from '../../../../models/user';
/**
* Aggregate users
- *
- * @param {any} params
- * @return {Promise<any>}
*/
module.exports = params => new Promise(async (res, rej) => {
// Get 'limit' parameter
diff --git a/src/server/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts
index 318cce77a5..d36e07a441 100644
--- a/src/server/api/endpoints/aggregation/users/activity.ts
+++ b/src/server/api/endpoints/aggregation/users/activity.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import User from '../../../../../models/user';
import Note from '../../../../../models/note';
@@ -9,9 +9,6 @@ import Note from '../../../../../models/note';
/**
* Aggregate activity of a user
- *
- * @param {any} params
- * @return {Promise<any>}
*/
module.exports = (params) => new Promise(async (res, rej) => {
// Get 'limit' parameter
@@ -19,7 +16,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Lookup user
diff --git a/src/server/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts
index 7ccb2a3066..a6dd29e735 100644
--- a/src/server/api/endpoints/aggregation/users/followers.ts
+++ b/src/server/api/endpoints/aggregation/users/followers.ts
@@ -1,19 +1,16 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import User from '../../../../../models/user';
import FollowedLog from '../../../../../models/followed-log';
/**
* Aggregate followers of a user
- *
- * @param {any} params
- * @return {Promise<any>}
*/
module.exports = (params) => new Promise(async (res, rej) => {
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Lookup user
diff --git a/src/server/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts
index 45e246495b..7336f392fe 100644
--- a/src/server/api/endpoints/aggregation/users/following.ts
+++ b/src/server/api/endpoints/aggregation/users/following.ts
@@ -1,19 +1,16 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import User from '../../../../../models/user';
import FollowingLog from '../../../../../models/following-log';
/**
* Aggregate following of a user
- *
- * @param {any} params
- * @return {Promise<any>}
*/
module.exports = (params) => new Promise(async (res, rej) => {
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Lookup user
diff --git a/src/server/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts
index e6170d83e2..c5a5e5ffca 100644
--- a/src/server/api/endpoints/aggregation/users/post.ts
+++ b/src/server/api/endpoints/aggregation/users/post.ts
@@ -1,19 +1,16 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import User from '../../../../../models/user';
import Note from '../../../../../models/note';
/**
* Aggregate note of a user
- *
- * @param {any} params
- * @return {Promise<any>}
*/
module.exports = (params) => new Promise(async (res, rej) => {
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Lookup user
diff --git a/src/server/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts
index 881c7ea693..f1664823cd 100644
--- a/src/server/api/endpoints/aggregation/users/reaction.ts
+++ b/src/server/api/endpoints/aggregation/users/reaction.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import User from '../../../../../models/user';
import Reaction from '../../../../../models/note-reaction';
@@ -13,7 +13,7 @@ import Reaction from '../../../../../models/note-reaction';
*/
module.exports = (params) => new Promise(async (res, rej) => {
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Lookup user
diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts
index 4a55d33f2d..f403429261 100644
--- a/src/server/api/endpoints/app/create.ts
+++ b/src/server/api/endpoints/app/create.ts
@@ -79,7 +79,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
if (descriptionErr) return rej('invalid description param');
// Get 'permission' parameter
- const [permission, permissionErr] = $(params.permission).array('string').unique().$;
+ const [permission, permissionErr] = $(params.permission).array($().string()).unique().$;
if (permissionErr) return rej('invalid permission param');
// Get 'callbackUrl' parameter
diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts
index 99a2093b68..92a03b9838 100644
--- a/src/server/api/endpoints/app/show.ts
+++ b/src/server/api/endpoints/app/show.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import App, { pack } from '../../../../models/app';
/**
@@ -41,7 +41,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
const isSecure = user != null && app == null;
// Get 'appId' parameter
- const [appId, appIdErr] = $(params.appId).optional.id().$;
+ const [appId, appIdErr] = $(params.appId).optional.type(ID).$;
if (appIdErr) return rej('invalid appId param');
// Get 'nameId' parameter
diff --git a/src/server/api/endpoints/channels.ts b/src/server/api/endpoints/channels.ts
index 582e6ba43b..b68107ed7d 100644
--- a/src/server/api/endpoints/channels.ts
+++ b/src/server/api/endpoints/channels.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../cafy-id';
import Channel, { pack } from '../../../models/channel';
/**
@@ -17,11 +17,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
diff --git a/src/server/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts
index 0f0f558c8a..a737fcb152 100644
--- a/src/server/api/endpoints/channels/create.ts
+++ b/src/server/api/endpoints/channels/create.ts
@@ -8,10 +8,6 @@ import { pack } from '../../../../models/channel';
/**
* Create a channel
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = async (params, user) => new Promise(async (res, rej) => {
// Get 'title' parameter
diff --git a/src/server/api/endpoints/channels/notes.ts b/src/server/api/endpoints/channels/notes.ts
index d636aa0d10..73a69c6d2a 100644
--- a/src/server/api/endpoints/channels/notes.ts
+++ b/src/server/api/endpoints/channels/notes.ts
@@ -1,16 +1,12 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import { default as Channel, IChannel } from '../../../../models/channel';
import Note, { pack } from '../../../../models/note';
/**
* Show a notes of a channel
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'limit' parameter
@@ -18,11 +14,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
@@ -31,7 +27,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
}
// Get 'channelId' parameter
- const [channelId, channelIdErr] = $(params.channelId).id().$;
+ const [channelId, channelIdErr] = $(params.channelId).type(ID).$;
if (channelIdErr) return rej('invalid channelId param');
// Fetch channel
diff --git a/src/server/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts
index 3ce9ce4745..3f468937ed 100644
--- a/src/server/api/endpoints/channels/show.ts
+++ b/src/server/api/endpoints/channels/show.ts
@@ -1,19 +1,15 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Channel, { IChannel, pack } from '../../../../models/channel';
/**
* Show a channel
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'channelId' parameter
- const [channelId, channelIdErr] = $(params.channelId).id().$;
+ const [channelId, channelIdErr] = $(params.channelId).type(ID).$;
if (channelIdErr) return rej('invalid channelId param');
// Fetch channel
diff --git a/src/server/api/endpoints/channels/unwatch.ts b/src/server/api/endpoints/channels/unwatch.ts
index 8220b90b68..6ada3c9e1b 100644
--- a/src/server/api/endpoints/channels/unwatch.ts
+++ b/src/server/api/endpoints/channels/unwatch.ts
@@ -1,20 +1,16 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Channel from '../../../../models/channel';
import Watching from '../../../../models/channel-watching';
/**
* Unwatch a channel
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'channelId' parameter
- const [channelId, channelIdErr] = $(params.channelId).id().$;
+ const [channelId, channelIdErr] = $(params.channelId).type(ID).$;
if (channelIdErr) return rej('invalid channelId param');
//#region Fetch channel
diff --git a/src/server/api/endpoints/channels/watch.ts b/src/server/api/endpoints/channels/watch.ts
index 6906282a54..7880c34652 100644
--- a/src/server/api/endpoints/channels/watch.ts
+++ b/src/server/api/endpoints/channels/watch.ts
@@ -1,20 +1,16 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Channel from '../../../../models/channel';
import Watching from '../../../../models/channel-watching';
/**
* Watch a channel
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'channelId' parameter
- const [channelId, channelIdErr] = $(params.channelId).id().$;
+ const [channelId, channelIdErr] = $(params.channelId).type(ID).$;
if (channelIdErr) return rej('invalid channelId param');
//#region Fetch channel
diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts
index 63d69d145a..7f78ef9daa 100644
--- a/src/server/api/endpoints/drive/files.ts
+++ b/src/server/api/endpoints/drive/files.ts
@@ -1,16 +1,11 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import DriveFile, { pack } from '../../../../models/drive-file';
/**
* Get drive files
- *
- * @param {any} params
- * @param {any} user
- * @param {any} app
- * @return {Promise<any>}
*/
module.exports = async (params, user, app) => {
// Get 'limit' parameter
@@ -18,11 +13,11 @@ module.exports = async (params, user, app) => {
if (limitErr) throw 'invalid limit param';
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) throw 'invalid sinceId param';
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) throw 'invalid untilId param';
// Check if both of sinceId and untilId is specified
@@ -31,7 +26,7 @@ module.exports = async (params, user, app) => {
}
// Get 'folderId' parameter
- const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+ const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$;
if (folderIdErr) throw 'invalid folderId param';
// Get 'type' parameter
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index df0bd0a0d3..3d5048732d 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -1,17 +1,12 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import { validateFileName, pack } from '../../../../../models/drive-file';
import create from '../../../../../services/drive/add-file';
/**
* Create a file
- *
- * @param {any} file
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = async (file, params, user): Promise<any> => {
if (file == null) {
@@ -34,7 +29,7 @@ module.exports = async (file, params, user): Promise<any> => {
}
// Get 'folderId' parameter
- const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+ const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$;
if (folderIdErr) throw 'invalid folderId param';
try {
diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts
index 0ab6e5d3e3..5d49577983 100644
--- a/src/server/api/endpoints/drive/files/find.ts
+++ b/src/server/api/endpoints/drive/files/find.ts
@@ -1,15 +1,11 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import DriveFile, { pack } from '../../../../../models/drive-file';
/**
* Find a file(s)
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'name' parameter
@@ -17,7 +13,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (nameErr) return rej('invalid name param');
// Get 'folderId' parameter
- const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+ const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$;
if (folderIdErr) return rej('invalid folderId param');
// Issue query
diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts
index 3398f24541..93c3a63031 100644
--- a/src/server/api/endpoints/drive/files/show.ts
+++ b/src/server/api/endpoints/drive/files/show.ts
@@ -1,19 +1,15 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import DriveFile, { pack } from '../../../../../models/drive-file';
/**
* Show a file
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = async (params, user) => {
// Get 'fileId' parameter
- const [fileId, fileIdErr] = $(params.fileId).id().$;
+ const [fileId, fileIdErr] = $(params.fileId).type(ID).$;
if (fileIdErr) throw 'invalid fileId param';
// Fetch file
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index c783ad8b3b..3ac157b530 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -1,21 +1,17 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import DriveFolder from '../../../../../models/drive-folder';
import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file';
import { publishDriveStream } from '../../../../../publishers/stream';
/**
* Update a file
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'fileId' parameter
- const [fileId, fileIdErr] = $(params.fileId).id().$;
+ const [fileId, fileIdErr] = $(params.fileId).type(ID).$;
if (fileIdErr) return rej('invalid fileId param');
// Fetch file
@@ -35,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (name) file.filename = name;
// Get 'folderId' parameter
- const [folderId, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+ const [folderId, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$;
if (folderIdErr) return rej('invalid folderId param');
if (folderId !== undefined) {
diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts
index 8a426c0efc..cfae1ae192 100644
--- a/src/server/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/server/api/endpoints/drive/files/upload_from_url.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import { pack } from '../../../../../models/drive-file';
import uploadFromUrl from '../../../../../services/drive/upload-from-url';
@@ -15,7 +15,7 @@ module.exports = async (params, user): Promise<any> => {
if (urlErr) throw 'invalid url param';
// Get 'folderId' parameter
- const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+ const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$;
if (folderIdErr) throw 'invalid folderId param';
return pack(await uploadFromUrl(url, user, folderId));
diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts
index 489e47912e..cba33c4286 100644
--- a/src/server/api/endpoints/drive/folders.ts
+++ b/src/server/api/endpoints/drive/folders.ts
@@ -1,16 +1,11 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import DriveFolder, { pack } from '../../../../models/drive-folder';
/**
* Get drive folders
- *
- * @param {any} params
- * @param {any} user
- * @param {any} app
- * @return {Promise<any>}
*/
module.exports = (params, user, app) => new Promise(async (res, rej) => {
// Get 'limit' parameter
@@ -18,11 +13,11 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
@@ -31,7 +26,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
}
// Get 'folderId' parameter
- const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+ const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$;
if (folderIdErr) return rej('invalid folderId param');
// Construct query
diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts
index f34d0019d7..65425537a2 100644
--- a/src/server/api/endpoints/drive/folders/create.ts
+++ b/src/server/api/endpoints/drive/folders/create.ts
@@ -1,16 +1,12 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
import { publishDriveStream } from '../../../../../publishers/stream';
/**
* Create drive folder
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'name' parameter
@@ -18,7 +14,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (nameErr) return rej('invalid name param');
// Get 'parentId' parameter
- const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$;
+ const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.type(ID).$;
if (parentIdErr) return rej('invalid parentId param');
// If the parent folder is specified
diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts
index 04dc38f87f..d6277f1978 100644
--- a/src/server/api/endpoints/drive/folders/find.ts
+++ b/src/server/api/endpoints/drive/folders/find.ts
@@ -1,15 +1,11 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import DriveFolder, { pack } from '../../../../../models/drive-folder';
/**
* Find a folder(s)
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'name' parameter
@@ -17,7 +13,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (nameErr) return rej('invalid name param');
// Get 'parentId' parameter
- const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$;
+ const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.type(ID).$;
if (parentIdErr) return rej('invalid parentId param');
// Issue query
diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts
index b432f5a50a..c703209fef 100644
--- a/src/server/api/endpoints/drive/folders/show.ts
+++ b/src/server/api/endpoints/drive/folders/show.ts
@@ -1,19 +1,15 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import DriveFolder, { pack } from '../../../../../models/drive-folder';
/**
* Show a folder
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'folderId' parameter
- const [folderId, folderIdErr] = $(params.folderId).id().$;
+ const [folderId, folderIdErr] = $(params.folderId).type(ID).$;
if (folderIdErr) return rej('invalid folderId param');
// Get folder
diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts
index dd7e8f5c86..d8da67fac8 100644
--- a/src/server/api/endpoints/drive/folders/update.ts
+++ b/src/server/api/endpoints/drive/folders/update.ts
@@ -1,20 +1,16 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
import { publishDriveStream } from '../../../../../publishers/stream';
/**
* Update a folder
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'folderId' parameter
- const [folderId, folderIdErr] = $(params.folderId).id().$;
+ const [folderId, folderIdErr] = $(params.folderId).type(ID).$;
if (folderIdErr) return rej('invalid folderId param');
// Fetch folder
@@ -34,7 +30,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (name) folder.name = name;
// Get 'parentId' parameter
- const [parentId, parentIdErr] = $(params.parentId).optional.nullable.id().$;
+ const [parentId, parentIdErr] = $(params.parentId).optional.nullable.type(ID).$;
if (parentIdErr) return rej('invalid parentId param');
if (parentId !== undefined) {
if (parentId === null) {
diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts
index 02313aa37b..00d89582b6 100644
--- a/src/server/api/endpoints/drive/stream.ts
+++ b/src/server/api/endpoints/drive/stream.ts
@@ -1,15 +1,11 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import DriveFile, { pack } from '../../../../models/drive-file';
/**
* Get drive stream
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'limit' parameter
@@ -17,11 +13,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index 27e5eb31db..43f902852e 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import User from '../../../../models/user';
import Following from '../../../../models/following';
import create from '../../../../services/following/create';
@@ -13,7 +13,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const follower = user;
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// 自分自身
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index ca0703ca22..99722ccf91 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import User from '../../../../models/user';
import Following from '../../../../models/following';
import deleteFollowing from '../../../../services/following/delete';
@@ -13,7 +13,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const follower = user;
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Check if the followee is yourself
diff --git a/src/server/api/endpoints/following/stalk.ts b/src/server/api/endpoints/following/stalk.ts
index fc8be4924d..1dfbc4df98 100644
--- a/src/server/api/endpoints/following/stalk.ts
+++ b/src/server/api/endpoints/following/stalk.ts
@@ -1,6 +1,5 @@
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Following from '../../../../models/following';
-import { isLocalUser } from '../../../../models/user';
/**
* Stalk a user
@@ -9,7 +8,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const follower = user;
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Fetch following
diff --git a/src/server/api/endpoints/following/unstalk.ts b/src/server/api/endpoints/following/unstalk.ts
index d7593bcd00..0d91ffeac8 100644
--- a/src/server/api/endpoints/following/unstalk.ts
+++ b/src/server/api/endpoints/following/unstalk.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Following from '../../../../models/following';
/**
@@ -8,7 +8,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const follower = user;
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Fetch following
diff --git a/src/server/api/endpoints/i/authorized_apps.ts b/src/server/api/endpoints/i/authorized_apps.ts
index 82fd2d2516..fd12b3dec0 100644
--- a/src/server/api/endpoints/i/authorized_apps.ts
+++ b/src/server/api/endpoints/i/authorized_apps.ts
@@ -7,10 +7,6 @@ import { pack } from '../../../../models/app';
/**
* Get authorized apps of my account
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'limit' parameter
diff --git a/src/server/api/endpoints/i/change_password.ts b/src/server/api/endpoints/i/change_password.ts
index 57415083f1..a24e9f0be1 100644
--- a/src/server/api/endpoints/i/change_password.ts
+++ b/src/server/api/endpoints/i/change_password.ts
@@ -7,10 +7,6 @@ import User from '../../../../models/user';
/**
* Change password
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = async (params, user) => new Promise(async (res, rej) => {
// Get 'currentPasword' parameter
diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts
index f390ef9ec7..a2c472ad17 100644
--- a/src/server/api/endpoints/i/favorites.ts
+++ b/src/server/api/endpoints/i/favorites.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Favorite, { pack } from '../../../../models/favorite';
/**
@@ -13,11 +13,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index 69a8910898..14ade7b023 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Notification from '../../../../models/notification';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/notification';
@@ -22,7 +22,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (markAsReadErr) return rej('invalid markAsRead param');
// Get 'type' parameter
- const [type, typeErr] = $(params.type).optional.array('string').unique().$;
+ const [type, typeErr] = $(params.type).optional.array($().string()).unique().$;
if (typeErr) return rej('invalid type param');
// Get 'limit' parameter
@@ -30,11 +30,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts
index 909a6fdbde..761e41bbea 100644
--- a/src/server/api/endpoints/i/pin.ts
+++ b/src/server/api/endpoints/i/pin.ts
@@ -1,21 +1,17 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import User from '../../../../models/user';
import Note from '../../../../models/note';
import { pack } from '../../../../models/user';
/**
* Pin note
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = async (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
- const [noteId, noteIdErr] = $(params.noteId).id().$;
+ const [noteId, noteIdErr] = $(params.noteId).type(ID).$;
if (noteIdErr) return rej('invalid noteId param');
// Fetch pinee
diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts
index f9e92c1797..945ddbdee4 100644
--- a/src/server/api/endpoints/i/regenerate_token.ts
+++ b/src/server/api/endpoints/i/regenerate_token.ts
@@ -9,10 +9,6 @@ import generateUserToken from '../../common/generate-native-user-token';
/**
* Regenerate native token
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = async (params, user) => new Promise(async (res, rej) => {
// Get 'password' parameter
diff --git a/src/server/api/endpoints/i/signin_history.ts b/src/server/api/endpoints/i/signin_history.ts
index 931b9e2252..77beca9fd6 100644
--- a/src/server/api/endpoints/i/signin_history.ts
+++ b/src/server/api/endpoints/i/signin_history.ts
@@ -1,15 +1,11 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Signin, { pack } from '../../../../models/signin';
/**
* Get signin history of my account
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'limit' parameter
@@ -17,11 +13,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index f3c9d777b5..7505e73387 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
import event from '../../../../publishers/stream';
@@ -32,12 +32,12 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
if (birthday !== undefined) user.profile.birthday = birthday;
// Get 'avatarId' parameter
- const [avatarId, avatarIdErr] = $(params.avatarId).optional.id().$;
+ const [avatarId, avatarIdErr] = $(params.avatarId).optional.type(ID).$;
if (avatarIdErr) return rej('invalid avatarId param');
if (avatarId) user.avatarId = avatarId;
// Get 'bannerId' parameter
- const [bannerId, bannerIdErr] = $(params.bannerId).optional.id().$;
+ const [bannerId, bannerIdErr] = $(params.bannerId).optional.type(ID).$;
if (bannerIdErr) return rej('invalid bannerId param');
if (bannerId) user.bannerId = bannerId;
diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts
index b0d5db5ec2..f753c8bcc4 100644
--- a/src/server/api/endpoints/i/update_client_setting.ts
+++ b/src/server/api/endpoints/i/update_client_setting.ts
@@ -7,10 +7,6 @@ import event from '../../../../publishers/stream';
/**
* Update myself
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = async (params, user) => new Promise(async (res, rej) => {
// Get 'name' parameter
diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts
index ce7661ede0..4b8ba25069 100644
--- a/src/server/api/endpoints/i/update_home.ts
+++ b/src/server/api/endpoints/i/update_home.ts
@@ -8,7 +8,7 @@ import event from '../../../../publishers/stream';
module.exports = async (params, user) => new Promise(async (res, rej) => {
// Get 'home' parameter
const [home, homeErr] = $(params.home).optional.array().each(
- $().strict.object()
+ $().object(true)
.have('name', $().string())
.have('id', $().string())
.have('place', $().string())
diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts
index b710e2f330..c3ecea7178 100644
--- a/src/server/api/endpoints/i/update_mobile_home.ts
+++ b/src/server/api/endpoints/i/update_mobile_home.ts
@@ -8,7 +8,7 @@ import event from '../../../../publishers/stream';
module.exports = async (params, user) => new Promise(async (res, rej) => {
// Get 'home' parameter
const [home, homeErr] = $(params.home).optional.array().each(
- $().strict.object()
+ $().object(true)
.have('name', $().string())
.have('id', $().string())
.have('data', $().object())).$;
diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts
index e42d34f21a..654bf5c198 100644
--- a/src/server/api/endpoints/messaging/history.ts
+++ b/src/server/api/endpoints/messaging/history.ts
@@ -8,10 +8,6 @@ import { pack } from '../../../../models/messaging-message';
/**
* Show messaging history
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'limit' parameter
diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts
index 092eab0562..f28699cb88 100644
--- a/src/server/api/endpoints/messaging/messages.ts
+++ b/src/server/api/endpoints/messaging/messages.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Message from '../../../../models/messaging-message';
import User from '../../../../models/user';
import { pack } from '../../../../models/messaging-message';
@@ -16,7 +16,7 @@ import read from '../../common/read-messaging-message';
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'userId' parameter
- const [recipientId, recipientIdErr] = $(params.userId).id().$;
+ const [recipientId, recipientIdErr] = $(params.userId).type(ID).$;
if (recipientIdErr) return rej('invalid userId param');
// Fetch recipient
@@ -41,11 +41,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index 0483b602b2..cce326be6e 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import Message from '../../../../../models/messaging-message';
import { isValidText } from '../../../../../models/messaging-message';
import History from '../../../../../models/messaging-history';
@@ -16,14 +16,10 @@ import config from '../../../../../config';
/**
* Create a message
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'userId' parameter
- const [recipientId, recipientIdErr] = $(params.userId).id().$;
+ const [recipientId, recipientIdErr] = $(params.userId).type(ID).$;
if (recipientIdErr) return rej('invalid userId param');
// Myself
@@ -49,7 +45,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (textErr) return rej('invalid text');
// Get 'fileId' parameter
- const [fileId, fileIdErr] = $(params.fileId).optional.id().$;
+ const [fileId, fileIdErr] = $(params.fileId).optional.type(ID).$;
if (fileIdErr) return rej('invalid fileId param');
let file = null;
diff --git a/src/server/api/endpoints/messaging/unread.ts b/src/server/api/endpoints/messaging/unread.ts
index 30d59dd8bd..1d83af501d 100644
--- a/src/server/api/endpoints/messaging/unread.ts
+++ b/src/server/api/endpoints/messaging/unread.ts
@@ -6,10 +6,6 @@ import Mute from '../../../../models/mute';
/**
* Get count of unread messages
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
const mute = await Mute.find({
diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
index 26ae612cab..0d59ecc118 100644
--- a/src/server/api/endpoints/mute/create.ts
+++ b/src/server/api/endpoints/mute/create.ts
@@ -1,22 +1,18 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import User from '../../../../models/user';
import Mute from '../../../../models/mute';
/**
* Mute a user
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
const muter = user;
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// 自分自身
diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts
index 6f617416c8..3a37de9a21 100644
--- a/src/server/api/endpoints/mute/delete.ts
+++ b/src/server/api/endpoints/mute/delete.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import User from '../../../../models/user';
import Mute from '../../../../models/mute';
@@ -12,7 +12,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const muter = user;
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Check if the mutee is yourself
diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts
index 0b8262d6c5..f35bf7d168 100644
--- a/src/server/api/endpoints/mute/list.ts
+++ b/src/server/api/endpoints/mute/list.ts
@@ -1,17 +1,13 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/user';
import { getFriendIds } from '../../common/get-friends';
/**
* Get muted users of a user
- *
- * @param {any} params
- * @param {any} me
- * @return {Promise<any>}
*/
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'iknow' parameter
@@ -23,7 +19,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'cursor' parameter
- const [cursor = null, cursorErr] = $(params.cursor).optional.id().$;
+ const [cursor = null, cursorErr] = $(params.cursor).optional.type(ID).$;
if (cursorErr) return rej('invalid cursor param');
// Construct query
diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts
index 2a3f8bcd7a..eb7ece70e9 100644
--- a/src/server/api/endpoints/my/apps.ts
+++ b/src/server/api/endpoints/my/apps.ts
@@ -6,10 +6,6 @@ import App, { pack } from '../../../../models/app';
/**
* Get my apps
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'limit' parameter
diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
index a70ac0588f..bf4d5bc66f 100644
--- a/src/server/api/endpoints/notes.ts
+++ b/src/server/api/endpoints/notes.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../cafy-id';
import Note, { pack } from '../../../models/note';
/**
@@ -33,11 +33,11 @@ module.exports = (params) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
diff --git a/src/server/api/endpoints/notes/context.ts b/src/server/api/endpoints/notes/context.ts
index 2caf742d26..309fc26447 100644
--- a/src/server/api/endpoints/notes/context.ts
+++ b/src/server/api/endpoints/notes/context.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note, { pack } from '../../../../models/note';
/**
@@ -13,7 +13,7 @@ import Note, { pack } from '../../../../models/note';
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
- const [noteId, noteIdErr] = $(params.noteId).id().$;
+ const [noteId, noteIdErr] = $(params.noteId).type(ID).$;
if (noteIdErr) return rej('invalid noteId param');
// Get 'limit' parameter
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index ea1f41aae2..1824a16c24 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
import Channel, { IChannel } from '../../../../models/channel';
@@ -11,11 +11,6 @@ import { IApp } from '../../../../models/app';
/**
* Create a note
- *
- * @param {any} params
- * @param {any} user
- * @param {any} app
- * @return {Promise<any>}
*/
module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => {
// Get 'visibility' parameter
@@ -35,11 +30,11 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
if (viaMobileErr) return rej('invalid viaMobile');
// Get 'tags' parameter
- const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$;
+ const [tags = [], tagsErr] = $(params.tags).optional.array($().string().range(1, 32)).unique().$;
if (tagsErr) return rej('invalid tags');
// Get 'geo' parameter
- const [geo, geoErr] = $(params.geo).optional.nullable.strict.object()
+ const [geo, geoErr] = $(params.geo).optional.nullable.object(true)
.have('coordinates', $().array().length(2)
.item(0, $().number().range(-180, 180))
.item(1, $().number().range(-90, 90)))
@@ -52,7 +47,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
if (geoErr) return rej('invalid geo');
// Get 'mediaIds' parameter
- const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$;
+ const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array($().type(ID)).unique().range(1, 4).$;
if (mediaIdsErr) return rej('invalid mediaIds');
let files = [];
@@ -79,7 +74,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
}
// Get 'renoteId' parameter
- const [renoteId, renoteIdErr] = $(params.renoteId).optional.id().$;
+ const [renoteId, renoteIdErr] = $(params.renoteId).optional.type(ID).$;
if (renoteIdErr) return rej('invalid renoteId');
let renote: INote = null;
@@ -100,7 +95,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
}
// Get 'replyId' parameter
- const [replyId, replyIdErr] = $(params.replyId).optional.id().$;
+ const [replyId, replyIdErr] = $(params.replyId).optional.type(ID).$;
if (replyIdErr) return rej('invalid replyId');
let reply: INote = null;
@@ -121,7 +116,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
}
// Get 'channelId' parameter
- const [channelId, channelIdErr] = $(params.channelId).optional.id().$;
+ const [channelId, channelIdErr] = $(params.channelId).optional.type(ID).$;
if (channelIdErr) return rej('invalid channelId');
let channel: IChannel = null;
@@ -162,8 +157,8 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
}
// Get 'poll' parameter
- const [poll, pollErr] = $(params.poll).optional.strict.object()
- .have('choices', $().array('string')
+ const [poll, pollErr] = $(params.poll).optional.object(true)
+ .have('choices', $().array($().string())
.unique()
.range(2, 10)
.each(c => c.length > 0 && c.length < 50))
diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts
index c8e7f52426..e4c4adb9bb 100644
--- a/src/server/api/endpoints/notes/favorites/create.ts
+++ b/src/server/api/endpoints/notes/favorites/create.ts
@@ -1,20 +1,16 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import Favorite from '../../../../../models/favorite';
import Note from '../../../../../models/note';
/**
* Favorite a note
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
- const [noteId, noteIdErr] = $(params.noteId).id().$;
+ const [noteId, noteIdErr] = $(params.noteId).type(ID).$;
if (noteIdErr) return rej('invalid noteId param');
// Get favoritee
diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts
index 92aceb343b..3c4d9a1111 100644
--- a/src/server/api/endpoints/notes/favorites/delete.ts
+++ b/src/server/api/endpoints/notes/favorites/delete.ts
@@ -1,20 +1,16 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import Favorite from '../../../../../models/favorite';
import Note from '../../../../../models/note';
/**
* Unfavorite a note
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
- const [noteId, noteIdErr] = $(params.noteId).id().$;
+ const [noteId, noteIdErr] = $(params.noteId).type(ID).$;
if (noteIdErr) return rej('invalid noteId param');
// Get favoritee
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
index 07e138ec54..e2a94d8a3e 100644
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note from '../../../../models/note';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note';
@@ -15,11 +15,11 @@ module.exports = async (params, user, app) => {
if (limitErr) throw 'invalid limit param';
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) throw 'invalid sinceId param';
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) throw 'invalid untilId param';
// Get 'sinceDate' parameter
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
index d63528c3cd..dda83311ac 100644
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note from '../../../../models/note';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note';
@@ -15,11 +15,11 @@ module.exports = async (params, user, app) => {
if (limitErr) throw 'invalid limit param';
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) throw 'invalid sinceId param';
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) throw 'invalid untilId param';
// Get 'sinceDate' parameter
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index 2d95606b3f..815cf271a2 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note from '../../../../models/note';
import { getFriendIds } from '../../common/get-friends';
import { pack } from '../../../../models/note';
@@ -24,11 +24,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
diff --git a/src/server/api/endpoints/notes/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts
index cb530ea2cf..24b0a4c803 100644
--- a/src/server/api/endpoints/notes/polls/recommendation.ts
+++ b/src/server/api/endpoints/notes/polls/recommendation.ts
@@ -7,10 +7,6 @@ import Note, { pack } from '../../../../../models/note';
/**
* Get recommended polls
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'limit' parameter
diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts
index 03d94da60d..2669c39085 100644
--- a/src/server/api/endpoints/notes/polls/vote.ts
+++ b/src/server/api/endpoints/notes/polls/vote.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import Vote from '../../../../../models/poll-vote';
import Note from '../../../../../models/note';
import Watching from '../../../../../models/note-watching';
@@ -11,14 +11,10 @@ import notify from '../../../../../publishers/notify';
/**
* Vote poll of a note
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
- const [noteId, noteIdErr] = $(params.noteId).id().$;
+ const [noteId, noteIdErr] = $(params.noteId).type(ID).$;
if (noteIdErr) return rej('invalid noteId param');
// Get votee
diff --git a/src/server/api/endpoints/notes/reactions.ts b/src/server/api/endpoints/notes/reactions.ts
index bbff97bb0a..68ffbacd46 100644
--- a/src/server/api/endpoints/notes/reactions.ts
+++ b/src/server/api/endpoints/notes/reactions.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note from '../../../../models/note';
import Reaction, { pack } from '../../../../models/note-reaction';
@@ -14,7 +14,7 @@ import Reaction, { pack } from '../../../../models/note-reaction';
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
- const [noteId, noteIdErr] = $(params.noteId).id().$;
+ const [noteId, noteIdErr] = $(params.noteId).type(ID).$;
if (noteIdErr) return rej('invalid noteId param');
// Get 'limit' parameter
diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts
index 9e217cc3e0..1c21252604 100644
--- a/src/server/api/endpoints/notes/reactions/create.ts
+++ b/src/server/api/endpoints/notes/reactions/create.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import Note from '../../../../../models/note';
import create from '../../../../../services/note/reaction/create';
import { validateReaction } from '../../../../../models/note-reaction';
@@ -11,7 +11,7 @@ import { validateReaction } from '../../../../../models/note-reaction';
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
- const [noteId, noteIdErr] = $(params.noteId).id().$;
+ const [noteId, noteIdErr] = $(params.noteId).type(ID).$;
if (noteIdErr) return rej('invalid noteId param');
// Get 'reaction' parameter
diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts
index b5d738b8ff..be3c1b214d 100644
--- a/src/server/api/endpoints/notes/reactions/delete.ts
+++ b/src/server/api/endpoints/notes/reactions/delete.ts
@@ -1,21 +1,16 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import Reaction from '../../../../../models/note-reaction';
import Note from '../../../../../models/note';
-// import event from '../../../publishers/stream';
/**
* Unreact to a note
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
- const [noteId, noteIdErr] = $(params.noteId).id().$;
+ const [noteId, noteIdErr] = $(params.noteId).type(ID).$;
if (noteIdErr) return rej('invalid noteId param');
// Fetch unreactee
diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts
index 88d9ff329a..31f1bb941a 100644
--- a/src/server/api/endpoints/notes/replies.ts
+++ b/src/server/api/endpoints/notes/replies.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note, { pack } from '../../../../models/note';
/**
@@ -13,7 +13,7 @@ import Note, { pack } from '../../../../models/note';
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
- const [noteId, noteIdErr] = $(params.noteId).id().$;
+ const [noteId, noteIdErr] = $(params.noteId).type(ID).$;
if (noteIdErr) return rej('invalid noteId param');
// Get 'limit' parameter
diff --git a/src/server/api/endpoints/notes/reposts.ts b/src/server/api/endpoints/notes/reposts.ts
index 9dfc2c3cb5..fe98931380 100644
--- a/src/server/api/endpoints/notes/reposts.ts
+++ b/src/server/api/endpoints/notes/reposts.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note, { pack } from '../../../../models/note';
/**
@@ -13,7 +13,7 @@ import Note, { pack } from '../../../../models/note';
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
- const [noteId, noteIdErr] = $(params.noteId).id().$;
+ const [noteId, noteIdErr] = $(params.noteId).type(ID).$;
if (noteIdErr) return rej('invalid noteId param');
// Get 'limit' parameter
@@ -21,11 +21,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts
index 3ff3fbbafa..021f620aa2 100644
--- a/src/server/api/endpoints/notes/search.ts
+++ b/src/server/api/endpoints/notes/search.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
const escapeRegexp = require('escape-regexp');
import Note from '../../../../models/note';
import User from '../../../../models/user';
@@ -22,19 +22,19 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
if (textError) return rej('invalid text param');
// Get 'includeUserIds' parameter
- const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array('id').$;
+ const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array($().type(ID)).$;
if (includeUserIdsErr) return rej('invalid includeUserIds param');
// Get 'excludeUserIds' parameter
- const [excludeUserIds = [], excludeUserIdsErr] = $(params.excludeUserIds).optional.array('id').$;
+ const [excludeUserIds = [], excludeUserIdsErr] = $(params.excludeUserIds).optional.array($().type(ID)).$;
if (excludeUserIdsErr) return rej('invalid excludeUserIds param');
// Get 'includeUserUsernames' parameter
- const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array('string').$;
+ const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array($().string()).$;
if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param');
// Get 'excludeUserUsernames' parameter
- const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.excludeUserUsernames).optional.array('string').$;
+ const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.excludeUserUsernames).optional.array($().string()).$;
if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param');
// Get 'following' parameter
diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts
index 67cdc3038b..266e0687e9 100644
--- a/src/server/api/endpoints/notes/show.ts
+++ b/src/server/api/endpoints/notes/show.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note, { pack } from '../../../../models/note';
/**
@@ -13,7 +13,7 @@ import Note, { pack } from '../../../../models/note';
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
- const [noteId, noteIdErr] = $(params.noteId).id().$;
+ const [noteId, noteIdErr] = $(params.noteId).type(ID).$;
if (noteIdErr) return rej('invalid noteId param');
// Get note
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index de30afea57..476d64158c 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note from '../../../../models/note';
import Mute from '../../../../models/mute';
import ChannelWatching from '../../../../models/channel-watching';
@@ -17,11 +17,11 @@ module.exports = async (params, user, app) => {
if (limitErr) throw 'invalid limit param';
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) throw 'invalid sinceId param';
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) throw 'invalid untilId param';
// Get 'sinceDate' parameter
diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts
new file mode 100644
index 0000000000..bb94fa0ab9
--- /dev/null
+++ b/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -0,0 +1,179 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy'; import ID from '../../../../cafy-id';
+import Note from '../../../../models/note';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/note';
+import UserList from '../../../../models/user-list';
+
+/**
+ * Get timeline of a user list
+ */
+module.exports = async (params, user, app) => {
+ // Get 'limit' parameter
+ const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
+ if (limitErr) throw 'invalid limit param';
+
+ // Get 'sinceId' parameter
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
+ if (sinceIdErr) throw 'invalid sinceId param';
+
+ // Get 'untilId' parameter
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
+ if (untilIdErr) throw 'invalid untilId param';
+
+ // Get 'sinceDate' parameter
+ const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
+ if (sinceDateErr) throw 'invalid sinceDate param';
+
+ // Get 'untilDate' parameter
+ const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
+ if (untilDateErr) throw 'invalid untilDate param';
+
+ // Check if only one of sinceId, untilId, sinceDate, untilDate specified
+ if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+ throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
+ }
+
+ // Get 'includeMyRenotes' parameter
+ const [includeMyRenotes = true, includeMyRenotesErr] = $(params.includeMyRenotes).optional.boolean().$;
+ if (includeMyRenotesErr) throw 'invalid includeMyRenotes param';
+
+ // Get 'includeRenotedMyNotes' parameter
+ const [includeRenotedMyNotes = true, includeRenotedMyNotesErr] = $(params.includeRenotedMyNotes).optional.boolean().$;
+ if (includeRenotedMyNotesErr) throw 'invalid includeRenotedMyNotes param';
+
+ // Get 'listId' parameter
+ const [listId, listIdErr] = $(params.listId).type(ID).$;
+ if (listIdErr) throw 'invalid listId param';
+
+ const [list, mutedUserIds] = await Promise.all([
+ // リストを取得
+ // Fetch the list
+ UserList.findOne({
+ _id: listId,
+ userId: user._id
+ }),
+
+ // ミュートしているユーザーを取得
+ Mute.find({
+ muterId: user._id
+ }).then(ms => ms.map(m => m.muteeId))
+ ]);
+
+ if (list.userIds.length == 0) {
+ return [];
+ }
+
+ //#region Construct query
+ const sort = {
+ _id: -1
+ };
+
+ const listQuery = list.userIds.map(u => ({
+ userId: u,
+
+ // リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める)
+ $or: [{
+ // リプライでない
+ replyId: null
+ }, { // または
+ // リプライだが返信先が投稿者自身の投稿
+ $expr: {
+ $eq: ['$_reply.userId', '$userId']
+ }
+ }, { // または
+ // リプライだが返信先が自分(フォロワー)の投稿
+ '_reply.userId': user._id
+ }, { // または
+ // 自分(フォロワー)が送信したリプライ
+ userId: user._id
+ }]
+ }));
+
+ const query = {
+ $and: [{
+ // リストに入っている人のタイムラインへの投稿
+ $or: listQuery,
+
+ // mute
+ userId: {
+ $nin: mutedUserIds
+ },
+ '_reply.userId': {
+ $nin: mutedUserIds
+ },
+ '_renote.userId': {
+ $nin: mutedUserIds
+ },
+ }]
+ } as any;
+
+ // MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。
+ // つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
+ // for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
+
+ if (includeMyRenotes === false) {
+ query.$and.push({
+ $or: [{
+ userId: { $ne: user._id }
+ }, {
+ renoteId: null
+ }, {
+ text: { $ne: null }
+ }, {
+ mediaIds: { $ne: [] }
+ }, {
+ poll: { $ne: null }
+ }]
+ });
+ }
+
+ if (includeRenotedMyNotes === false) {
+ query.$and.push({
+ $or: [{
+ '_renote.userId': { $ne: user._id }
+ }, {
+ renoteId: null
+ }, {
+ text: { $ne: null }
+ }, {
+ mediaIds: { $ne: [] }
+ }, {
+ poll: { $ne: null }
+ }]
+ });
+ }
+
+ if (sinceId) {
+ sort._id = 1;
+ query._id = {
+ $gt: sinceId
+ };
+ } else if (untilId) {
+ query._id = {
+ $lt: untilId
+ };
+ } else if (sinceDate) {
+ sort._id = 1;
+ query.createdAt = {
+ $gt: new Date(sinceDate)
+ };
+ } else if (untilDate) {
+ query.createdAt = {
+ $lt: new Date(untilDate)
+ };
+ }
+ //#endregion
+
+ // Issue query
+ const timeline = await Note
+ .find(query, {
+ limit: limit,
+ sort: sort
+ });
+
+ // Serialize
+ return await Promise.all(timeline.map(note => pack(note, user)));
+};
diff --git a/src/server/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts
index 283ecd63b1..600a80d194 100644
--- a/src/server/api/endpoints/notifications/get_unread_count.ts
+++ b/src/server/api/endpoints/notifications/get_unread_count.ts
@@ -6,10 +6,6 @@ import Mute from '../../../../models/mute';
/**
* Get count of unread notifications
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
const mute = await Mute.find({
diff --git a/src/server/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts
index 01c9145837..dce3cb4663 100644
--- a/src/server/api/endpoints/notifications/mark_as_read_all.ts
+++ b/src/server/api/endpoints/notifications/mark_as_read_all.ts
@@ -6,10 +6,6 @@ import event from '../../../../publishers/stream';
/**
* Mark as read all notifications
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Update documents
diff --git a/src/server/api/endpoints/othello/games.ts b/src/server/api/endpoints/othello/games.ts
index d05c1c2585..3b23b60637 100644
--- a/src/server/api/endpoints/othello/games.ts
+++ b/src/server/api/endpoints/othello/games.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import OthelloGame, { pack } from '../../../../models/othello-game';
module.exports = (params, user) => new Promise(async (res, rej) => {
@@ -11,11 +11,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
diff --git a/src/server/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts
index dd886936d4..d76c6556a2 100644
--- a/src/server/api/endpoints/othello/games/show.ts
+++ b/src/server/api/endpoints/othello/games/show.ts
@@ -1,10 +1,10 @@
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
import OthelloGame, { pack } from '../../../../../models/othello-game';
import Othello from '../../../../../othello/core';
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'gameId' parameter
- const [gameId, gameIdErr] = $(params.gameId).id().$;
+ const [gameId, gameIdErr] = $(params.gameId).type(ID).$;
if (gameIdErr) return rej('invalid gameId param');
const game = await OthelloGame.findOne({ _id: gameId });
diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts
index d9274f8f9c..b73b64437b 100644
--- a/src/server/api/endpoints/othello/match.ts
+++ b/src/server/api/endpoints/othello/match.ts
@@ -1,4 +1,4 @@
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Matching, { pack as packMatching } from '../../../../models/othello-matching';
import OthelloGame, { pack as packGame } from '../../../../models/othello-game';
import User from '../../../../models/user';
@@ -7,7 +7,7 @@ import { eighteight } from '../../../../othello/maps';
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'userId' parameter
- const [childId, childIdErr] = $(params.userId).id().$;
+ const [childId, childIdErr] = $(params.userId).type(ID).$;
if (childIdErr) return rej('invalid userId param');
// Myself
diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts
index ae33e8af0c..5b389d452f 100644
--- a/src/server/api/endpoints/users.ts
+++ b/src/server/api/endpoints/users.ts
@@ -6,10 +6,6 @@ import User, { pack } from '../../../models/user';
/**
* Lists all users
- *
- * @param {any} params
- * @param {any} me
- * @return {Promise<any>}
*/
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'limit' parameter
diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts
index 5f03326be8..940b5ed9bc 100644
--- a/src/server/api/endpoints/users/followers.ts
+++ b/src/server/api/endpoints/users/followers.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import User from '../../../../models/user';
import Following from '../../../../models/following';
import { pack } from '../../../../models/user';
@@ -9,14 +9,10 @@ import { getFriendIds } from '../../common/get-friends';
/**
* Get followers of a user
- *
- * @param {any} params
- * @param {any} me
- * @return {Promise<any>}
*/
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Get 'iknow' parameter
@@ -28,7 +24,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'cursor' parameter
- const [cursor = null, cursorErr] = $(params.cursor).optional.id().$;
+ const [cursor = null, cursorErr] = $(params.cursor).optional.type(ID).$;
if (cursorErr) return rej('invalid cursor param');
// Lookup user
diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
index 9fb135b24d..63a73a2e27 100644
--- a/src/server/api/endpoints/users/following.ts
+++ b/src/server/api/endpoints/users/following.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import User from '../../../../models/user';
import Following from '../../../../models/following';
import { pack } from '../../../../models/user';
@@ -16,7 +16,7 @@ import { getFriendIds } from '../../common/get-friends';
*/
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Get 'iknow' parameter
@@ -28,7 +28,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'cursor' parameter
- const [cursor = null, cursorErr] = $(params.cursor).optional.id().$;
+ const [cursor = null, cursorErr] = $(params.cursor).optional.type(ID).$;
if (cursorErr) return rej('invalid cursor param');
// Lookup user
diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts
index 7a98f44e98..4c00620a52 100644
--- a/src/server/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts
@@ -1,13 +1,13 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note from '../../../../models/note';
import User, { pack } from '../../../../models/user';
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).id().$;
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Get 'limit' parameter
diff --git a/src/server/api/endpoints/users/lists/create.ts b/src/server/api/endpoints/users/lists/create.ts
new file mode 100644
index 0000000000..6ae510f52b
--- /dev/null
+++ b/src/server/api/endpoints/users/lists/create.ts
@@ -0,0 +1,25 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import UserList, { pack } from '../../../../../models/user-list';
+
+/**
+ * Create a user list
+ */
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+ // Get 'title' parameter
+ const [title, titleErr] = $(params.title).string().range(1, 100).$;
+ if (titleErr) return rej('invalid title param');
+
+ // insert
+ const userList = await UserList.insert({
+ createdAt: new Date(),
+ userId: user._id,
+ title: title,
+ userIds: []
+ });
+
+ // Response
+ res(await pack(userList));
+});
diff --git a/src/server/api/endpoints/users/lists/list.ts b/src/server/api/endpoints/users/lists/list.ts
new file mode 100644
index 0000000000..d19339a1f5
--- /dev/null
+++ b/src/server/api/endpoints/users/lists/list.ts
@@ -0,0 +1,13 @@
+import UserList, { pack } from '../../../../../models/user-list';
+
+/**
+ * Add a user to a user list
+ */
+module.exports = async (params, me) => new Promise(async (res, rej) => {
+ // Fetch lists
+ const userLists = await UserList.find({
+ userId: me._id,
+ });
+
+ res(await Promise.all(userLists.map(x => pack(x))));
+});
diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts
new file mode 100644
index 0000000000..467c08efd4
--- /dev/null
+++ b/src/server/api/endpoints/users/lists/push.ts
@@ -0,0 +1,51 @@
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
+import UserList from '../../../../../models/user-list';
+import User, { pack as packUser } from '../../../../../models/user';
+import { publishUserListStream } from '../../../../../publishers/stream';
+
+/**
+ * Add a user to a user list
+ */
+module.exports = async (params, me) => new Promise(async (res, rej) => {
+ // Get 'listId' parameter
+ const [listId, listIdErr] = $(params.listId).type(ID).$;
+ if (listIdErr) return rej('invalid listId param');
+
+ // Fetch the list
+ const userList = await UserList.findOne({
+ _id: listId,
+ userId: me._id,
+ });
+
+ if (userList == null) {
+ return rej('list not found');
+ }
+
+ // Get 'userId' parameter
+ const [userId, userIdErr] = $(params.userId).type(ID).$;
+ if (userIdErr) return rej('invalid userId param');
+
+ // Fetch the user
+ const user = await User.findOne({
+ _id: userId
+ });
+
+ if (user == null) {
+ return rej('user not found');
+ }
+
+ if (userList.userIds.map(id => id.toHexString()).includes(user._id.toHexString())) {
+ return rej('the user already added');
+ }
+
+ // Push the user
+ await UserList.update({ _id: userList._id }, {
+ $push: {
+ userIds: user._id
+ }
+ });
+
+ res();
+
+ publishUserListStream(userList._id, 'userAdded', await packUser(user));
+});
diff --git a/src/server/api/endpoints/users/lists/show.ts b/src/server/api/endpoints/users/lists/show.ts
new file mode 100644
index 0000000000..61e0f0463f
--- /dev/null
+++ b/src/server/api/endpoints/users/lists/show.ts
@@ -0,0 +1,23 @@
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
+import UserList, { pack } from '../../../../../models/user-list';
+
+/**
+ * Show a user list
+ */
+module.exports = async (params, me) => new Promise(async (res, rej) => {
+ // Get 'listId' parameter
+ const [listId, listIdErr] = $(params.listId).type(ID).$;
+ if (listIdErr) return rej('invalid listId param');
+
+ // Fetch the list
+ const userList = await UserList.findOne({
+ _id: listId,
+ userId: me._id,
+ });
+
+ if (userList == null) {
+ return rej('list not found');
+ }
+
+ res(await pack(userList));
+});
diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts
index bd4247c79c..dafa18bcc9 100644
--- a/src/server/api/endpoints/users/notes.ts
+++ b/src/server/api/endpoints/users/notes.ts
@@ -1,7 +1,7 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import getHostLower from '../../common/get-host-lower';
import Note, { pack } from '../../../../models/note';
import User from '../../../../models/user';
@@ -11,7 +11,7 @@ import User from '../../../../models/user';
*/
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).optional.id().$;
+ const [userId, userIdErr] = $(params.userId).optional.type(ID).$;
if (userIdErr) return rej('invalid userId param');
// Get 'username' parameter
@@ -43,11 +43,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+ const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
- const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+ const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$;
if (untilIdErr) return rej('invalid untilId param');
// Get 'sinceDate' parameter
diff --git a/src/server/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts
index 5f6ececff9..91d9ad1f3a 100644
--- a/src/server/api/endpoints/users/search_by_username.ts
+++ b/src/server/api/endpoints/users/search_by_username.ts
@@ -6,10 +6,6 @@ import User, { pack } from '../../../../models/user';
/**
* Search a user by username
- *
- * @param {any} params
- * @param {any} me
- * @return {Promise<any>}
*/
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'query' parameter
diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index 7e7f5dc488..141565ece6 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -1,22 +1,26 @@
/**
* Module dependencies
*/
-import $ from 'cafy';
+import $ from 'cafy'; import ID from '../../../../cafy-id';
import User, { pack } from '../../../../models/user';
import resolveRemoteUser from '../../../../remote/resolve-user';
const cursorOption = { fields: { data: false } };
/**
- * Show a user
+ * Show user(s)
*/
module.exports = (params, me) => new Promise(async (res, rej) => {
let user;
// Get 'userId' parameter
- const [userId, userIdErr] = $(params.userId).optional.id().$;
+ const [userId, userIdErr] = $(params.userId).optional.type(ID).$;
if (userIdErr) return rej('invalid userId param');
+ // Get 'userIds' parameter
+ const [userIds, userIdsErr] = $(params.userIds).optional.array($().type(ID)).$;
+ if (userIdsErr) return rej('invalid userIds param');
+
// Get 'username' parameter
const [username, usernameErr] = $(params.username).optional.string().$;
if (usernameErr) return rej('invalid username param');
@@ -25,32 +29,40 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
const [host, hostErr] = $(params.host).nullable.optional.string().$;
if (hostErr) return rej('invalid host param');
- if (userId === undefined && typeof username !== 'string') {
- return rej('userId or pair of username and host is required');
- }
+ if (userIds) {
+ const users = await User.find({
+ _id: {
+ $in: userIds
+ }
+ });
- // Lookup user
- if (typeof host === 'string') {
- try {
- user = await resolveRemoteUser(username, host, cursorOption);
- } catch (e) {
- console.warn(`failed to resolve remote user: ${e}`);
- return rej('failed to resolve remote user');
- }
+ res(await Promise.all(users.map(u => pack(u, me, {
+ detail: true
+ }))));
} else {
- const q = userId !== undefined
- ? { _id: userId }
- : { usernameLower: username.toLowerCase(), host: null };
+ // Lookup user
+ if (typeof host === 'string') {
+ try {
+ user = await resolveRemoteUser(username, host, cursorOption);
+ } catch (e) {
+ console.warn(`failed to resolve remote user: ${e}`);
+ return rej('failed to resolve remote user');
+ }
+ } else {
+ const q = userId !== undefined
+ ? { _id: userId }
+ : { usernameLower: username.toLowerCase(), host: null };
- user = await User.findOne(q, cursorOption);
+ user = await User.findOne(q, cursorOption);
- if (user === null) {
- return rej('user not found');
+ if (user === null) {
+ return rej('user not found');
+ }
}
- }
- // Send response
- res(await pack(user, me, {
- detail: true
- }));
+ // Send response
+ res(await pack(user, me, {
+ detail: true
+ }));
+ }
});
diff --git a/src/server/api/stream/user-list.ts b/src/server/api/stream/user-list.ts
new file mode 100644
index 0000000000..ba03b97860
--- /dev/null
+++ b/src/server/api/stream/user-list.ts
@@ -0,0 +1,14 @@
+import * as websocket from 'websocket';
+import * as redis from 'redis';
+import { ParsedUrlQuery } from 'querystring';
+
+export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
+ const q = request.resourceURL.query as ParsedUrlQuery;
+ const listId = q.listId as string;
+
+ // Subscribe stream
+ subscriber.subscribe(`misskey:user-list-stream:${listId}`);
+ subscriber.on('message', (_, data) => {
+ connection.send(data);
+ });
+}
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index ce13253649..e4884ed7c4 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -6,6 +6,7 @@ import config from '../../config';
import homeStream from './stream/home';
import localTimelineStream from './stream/local-timeline';
import globalTimelineStream from './stream/global-timeline';
+import userListStream from './stream/user-list';
import driveStream from './stream/drive';
import messagingStream from './stream/messaging';
import messagingIndexStream from './stream/messaging-index';
@@ -70,6 +71,7 @@ module.exports = (server: http.Server) => {
request.resourceURL.pathname === '/' ? homeStream :
request.resourceURL.pathname === '/local-timeline' ? localTimelineStream :
request.resourceURL.pathname === '/global-timeline' ? globalTimelineStream :
+ request.resourceURL.pathname === '/user-list' ? userListStream :
request.resourceURL.pathname === '/drive' ? driveStream :
request.resourceURL.pathname === '/messaging' ? messagingStream :
request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream :
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
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index e5ad96898f..4808edfda4 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -1,6 +1,6 @@
import Note, { pack, INote } from '../../models/note';
import User, { isLocalUser, IUser, isRemoteUser } from '../../models/user';
-import stream, { publishLocalTimelineStream, publishGlobalTimelineStream } from '../../publishers/stream';
+import stream, { publishLocalTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../publishers/stream';
import Following from '../../models/following';
import { deliver } from '../../queue';
import renderNote from '../../remote/activitypub/renderer/note';
@@ -16,6 +16,7 @@ import pushSw from '../../publishers/push-sw';
import event from '../../publishers/stream';
import parse from '../../text/parse';
import { IApp } from '../../models/app';
+import UserList from '../../models/user-list';
export default async (user: IUser, data: {
createdAt?: Date;
@@ -110,60 +111,73 @@ export default async (user: IUser, data: {
// タイムラインへの投稿
if (note.channelId == null) {
- if (isLocalUser(user)) {
- // Publish event to myself's stream
- stream(note.userId, 'note', noteObj);
+ if (!silent) {
+ if (isLocalUser(user)) {
+ // Publish event to myself's stream
+ stream(note.userId, 'note', noteObj);
- // Publish note to local timeline stream
- publishLocalTimelineStream(noteObj);
- }
+ // Publish note to local timeline stream
+ publishLocalTimelineStream(noteObj);
+ }
- // Publish note to global timeline stream
- publishGlobalTimelineStream(noteObj);
+ // Publish note to global timeline stream
+ publishGlobalTimelineStream(noteObj);
- // Fetch all followers
- const followers = await Following.find({
- followeeId: note.userId
- });
+ // フォロワーに配信
+ Following.find({
+ followeeId: note.userId
+ }).then(followers => {
+ followers.map(async following => {
+ const follower = following._follower;
- if (!silent) {
- const render = async () => {
- const content = data.renote && data.text == null
- ? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
- : renderCreate(await renderNote(note));
- return packAp(content);
- };
+ if (isLocalUser(follower)) {
+ // ストーキングしていない場合
+ if (!following.stalk) {
+ // この投稿が返信ならスキップ
+ if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) return;
+ }
- // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
- if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
- deliver(user, await render(), data.reply._user.inbox);
- }
+ // Publish event to followers stream
+ stream(following.followerId, 'note', noteObj);
+ } else {
+ //#region AP配送
+ // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
+ if (isLocalUser(user)) {
+ deliver(user, await render(), follower.inbox);
+ }
+ //#endergion
+ }
+ });
+ });
- // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
- if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) {
- deliver(user, await render(), data.renote._user.inbox);
- }
+ // リストに配信
+ UserList.find({
+ userIds: note.userId
+ }).then(lists => {
+ lists.forEach(list => {
+ publishUserListStream(list._id, 'note', noteObj);
+ });
+ });
+ }
- Promise.all(followers.map(async following => {
- const follower = following._follower;
+ //#region AP配送
+ const render = async () => {
+ const content = data.renote && data.text == null
+ ? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
+ : renderCreate(await renderNote(note));
+ return packAp(content);
+ };
- if (isLocalUser(follower)) {
- // ストーキングしていない場合
- if (!following.stalk) {
- // この投稿が返信ならスキップ
- if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) return;
- }
+ // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
+ if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
+ deliver(user, await render(), data.reply._user.inbox);
+ }
- // Publish event to followers stream
- stream(following.followerId, 'note', noteObj);
- } else {
- // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
- if (isLocalUser(user)) {
- deliver(user, await render(), follower.inbox);
- }
- }
- }));
+ // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
+ if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) {
+ deliver(user, await render(), data.renote._user.inbox);
}
+ //#endergion
}
// チャンネルへの投稿