Compare commits

..

32 Commits

Author SHA1 Message Date
syuilo
a57a1d809b v5074 2018-04-21 19:06:59 +09:00
syuilo
29a121f5b2 Darken 2018-04-21 19:05:55 +09:00
syuilo
4cfb360d44 Fix #1529 2018-04-21 19:02:12 +09:00
syuilo
441796f845 Add search syntax 2018-04-21 18:59:16 +09:00
syuilo
3c80f0eaca Fix #1526 2018-04-21 18:25:25 +09:00
syuilo
86e1b792c2 Salt 2018-04-21 16:40:16 +09:00
syuilo
decb257136 Revert "wait"
This reverts commit d670591713.
2018-04-21 16:32:15 +09:00
syuilo
d670591713 wait 2018-04-21 16:27:47 +09:00
syuilo
dee2b77bdc oops 2018-04-21 16:21:12 +09:00
syuilo
6119312a74 Fix bug 2018-04-21 16:18:58 +09:00
syuilo
b77a8af6e3 v5064 2018-04-21 16:07:05 +09:00
syuilo
c9e9ecd699 Update README.md 2018-04-21 16:03:58 +09:00
syuilo
4a0f9d8280 Add timeline animation
Co-Authored-By: tamaina <tamaina@hotmail.co.jp>
2018-04-21 16:01:12 +09:00
syuilo
797b5d2311 Darken 2018-04-21 15:46:46 +09:00
syuilo
b14d7b45ae Better timeline rendering
Co-Authored-By: tamaina <tamaina@hotmail.co.jp>
2018-04-21 15:37:02 +09:00
syuilo
d721a143a8 Darken 2018-04-21 14:19:21 +09:00
syuilo
fde9c783ae Fix username rendering
Co-Authored-By: tamaina <tamaina@hotmail.co.jp>
2018-04-21 13:54:51 +09:00
syuilo
c80b288db3 Better renote annotation rendering
Co-Authored-By: tamaina <tamaina@hotmail.co.jp>
2018-04-21 13:48:56 +09:00
syuilo
93898b7a5f 🎨 2018-04-21 12:09:04 +09:00
syuilo
a0a2335c16 Darken 2018-04-21 11:57:46 +09:00
syuilo
716b41107b Darken 2018-04-21 11:44:55 +09:00
syuilo
a9a7a89b8b #1506 2018-04-21 11:42:38 +09:00
syuilo
4bf85a0e6b Darken 2018-04-21 10:10:04 +09:00
syuilo
dd509333a3 v5051 2018-04-20 13:39:08 +09:00
syuilo
8ccfbbf24c Darken 2018-04-20 13:38:28 +09:00
syuilo
4953842ff1 ✌️ 2018-04-20 13:31:43 +09:00
syuilo
8a8d97b8c7 🎨 2018-04-20 13:03:51 +09:00
syuilo
5916fcca6a Improve usability 2018-04-20 12:55:18 +09:00
syuilo
2105e4964b ✌️ 2018-04-20 12:38:31 +09:00
syuilo
8ed30d1ff3 ✌️ 2018-04-20 12:18:50 +09:00
syuilo
6f546efdb7 Darken 2018-04-20 12:13:33 +09:00
syuilo
c4f62296a4 Fix bug 2018-04-20 08:05:57 +09:00
41 changed files with 822 additions and 250 deletions

View File

@@ -44,9 +44,9 @@ If you want to translate Misskey, please see [Translation guide](./docs/translat
:mortar_board: Notable contributors
----------------------------------------------------------------
| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] | ![rinsuki][rinsuki-icon] |
|:-:|:-:|:-:|:-:|:-:|
| [syuilo][syuilo-link]<br>Owner | [Aya Morisawa][ayamorisawa-link]<br>Collaborator | [otofune][otofune-link]<br>Collaborator | [akihikodaki][akihikodaki-link] | [rinsuki][rinsuki-link] |
| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] | ![tamaina][tamaina-icon] | ![rinsuki][rinsuki-icon] |
|:-:|:-:|:-:|:-:|:-:|:-:|
| [syuilo][syuilo-link]<br>Owner | [Aya Morisawa][ayamorisawa-link]<br>Collaborator | [otofune][otofune-link]<br>Collaborator | [akihikodaki][akihikodaki-link] | [tamaina][tamaina-link] | [rinsuki][rinsuki-link] |
[List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
@@ -92,6 +92,8 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
[akihikodaki-icon]: https://avatars2.githubusercontent.com/u/17036990?s=70&v=4
[rinsuki-link]: https://github.com/rinsuki
[rinsuki-icon]: https://avatars0.githubusercontent.com/u/6533808?s=70&v=4
[tamaina-link]: https://github.com/tamaina
[tamaina-icon]: https://avatars1.githubusercontent.com/u/7973572?s=70&v=4
[mirro-san-link]: https://github.com/mirro-san
[mirro-san-icon]: https://avatars1.githubusercontent.com/u/17948612?s=70&v=4

View File

@@ -98,7 +98,9 @@ common/views/components/nav.vue:
feedback: "Feedback"
common/views/components/note-menu.vue:
favorite: "Favorite this note"
pin: "Pin to profile page"
remote: "Show on origin"
common/views/components/poll.vue:
vote-to: "Vote for '{}'"
@@ -367,6 +369,7 @@ desktop/views/components/settings.profile.vue:
desktop/views/components/ui.header.account.vue:
profile: "Your profile"
drive: "Drive"
favorites: "Favorites"
customize: "Customize"
settings: "Settings"
signout: "Sign out"

View File

@@ -98,7 +98,9 @@ common/views/components/nav.vue:
feedback: "フィードバック"
common/views/components/note-menu.vue:
favorite: "Favorite this note"
pin: "Épingler sur votre profile"
remote: "投稿元で見る"
common/views/components/poll.vue:
vote-to: "Voter pour '{}'"
@@ -367,6 +369,7 @@ desktop/views/components/settings.profile.vue:
desktop/views/components/ui.header.account.vue:
profile: "Votre profil"
drive: "Drive"
favorites: "Favorites"
customize: "Modifications"
settings: "Réglages"
signout: "Déconnexion"

View File

@@ -98,7 +98,9 @@ common/views/components/nav.vue:
feedback: "フィードバック"
common/views/components/note-menu.vue:
favorite: "お気に入り"
pin: "ピン留め"
remote: "投稿元で見る"
common/views/components/poll.vue:
vote-to: "「{}」に投票する"
@@ -367,6 +369,7 @@ desktop/views/components/settings.profile.vue:
desktop/views/components/ui.header.account.vue:
profile: "プロフィール"
drive: "ドライブ"
favorites: "お気に入り"
customize: "カスタマイズ"
settings: "設定"
signout: "サインアウト"

View File

@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "0.0.5042",
"version": "0.0.5074",
"codename": "nighthike",
"license": "MIT",
"description": "A miniblog-based SNS",
@@ -208,6 +208,7 @@
"vue-router": "3.0.1",
"vue-template-compiler": "2.5.16",
"vuedraggable": "2.16.0",
"vuex": "^3.0.1",
"web-push": "3.3.0",
"webfinger.js": "2.6.6",
"webpack": "4.6.0",

View File

@@ -11,14 +11,12 @@
'use strict';
// Chromeで確認したことなのですが、constやletを用いたとしても
// グローバルなスコープで定数/変数を定義するとwindowのプロパティ
// としてそれがアクセスできるようになる訳ではありませんが、普通に
// コンソールから定数/変数名を入力するとアクセスできてしまいます。
// ブロック内に入れてスコープを非グローバル化するとそれが防げます
// (Chrome以外のブラウザでは検証していません)
{
if (localStorage.getItem('shouldFlush') == 'true') refresh();
(function() {
// キャッシュ削除要求があれば従う
if (localStorage.getItem('shouldFlush') == 'true') {
refresh();
return;
}
// Get the current url information
const url = new URL(location.href);
@@ -80,11 +78,16 @@
const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug)
|| ENV != 'production';
// Get salt query
const salt = localStorage.getItem('salt')
? '?salt=' + localStorage.getItem('salt')
: '';
// Load an app script
// Note: 'async' make it possible to load the script asyncly.
// 'defer' make it possible to run the script when the dom loaded.
const script = document.createElement('script');
script.setAttribute('src', `/assets/${app}.${ver}.${lang}.${raw ? 'raw' : 'min'}.js`);
script.setAttribute('src', `/assets/${app}.${ver}.${lang}.${raw ? 'raw' : 'min'}.js${salt}`);
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
head.appendChild(script);
@@ -120,6 +123,9 @@
function refresh() {
localStorage.setItem('shouldFlush', 'false');
// Random
localStorage.setItem('salt', Math.random().toString());
// Clear cache (serive worker)
try {
navigator.serviceWorker.controller.postMessage('clear');
@@ -134,4 +140,4 @@
// Force reload
location.reload(true);
}
}
})();

View File

@@ -21,7 +21,9 @@ const defaultSettings = {
showMaps: true,
showPostFormOnTopOfTl: false,
gradientWindowHeader: false,
showReplyTarget: true
showReplyTarget: true,
showMyRenotes: true,
showRenotedMyNotes: true
};
//#region api requests

View File

@@ -0,0 +1,67 @@
<template>
<div class="mk-google">
<input type="search" v-model="query" :placeholder="q">
<button @click="search">検索</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: ['q'],
data() {
return {
query: null
};
},
mounted() {
this.query = this.q;
},
methods: {
search() {
window.open(`https://www.google.com/?#q=${this.query}`, '_blank');
}
}
});
</script>
<style lang="stylus" scoped>
root(isDark)
display flex
margin 8px 0
> input
flex-shrink 1
padding 10px
width 100%
height 40px
font-family sans-serif
font-size 16px
color isDark ? #dee4e8 : #55595c
background isDark ? #191b22 : #fff
border solid 1px isDark ? #495156 : #dadada
border-radius 4px 0 0 4px
&:hover
border-color isDark ? #777c86 : #b0b0b0
> button
flex-shrink 0
padding 0 16px
border solid 1px isDark ? #495156 : #dadada
border-left none
border-radius 0 4px 4px 0
&:hover
background-color isDark ? #2e3440 : #eee
&:active
box-shadow 0 2px 4px rgba(#000, 0.15) inset
.mk-google[data-darkmode]
root(true)
.mk-google:not([data-darkmode])
root(false)
</style>

View File

@@ -4,6 +4,7 @@ import parse from '../../../../../text/parse';
import getAcct from '../../../../../acct/render';
import { url } from '../../../config';
import MkUrl from './url.vue';
import MkGoogle from './google.vue';
const flatten = list => list.reduce(
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
@@ -145,6 +146,13 @@ export default Vue.component('mk-note-html', {
const emoji = emojilib.lib[token.emoji];
return createElement('span', emoji ? emoji.char : token.content);
case 'search':
return createElement(MkGoogle, {
props: {
q: token.query
}
});
default:
console.log('unknown ast type:', token.type);
}

View File

@@ -2,6 +2,7 @@
<div class="mk-note-menu">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ compact }" ref="popover">
<button @click="favorite">%i18n:@favorite%</button>
<button v-if="note.userId == os.i.id" @click="pin">%i18n:@pin%</button>
<a v-if="note.uri" :href="note.uri" target="_blank">%i18n:@remote%</a>
</div>
@@ -58,6 +59,14 @@ export default Vue.extend({
});
},
favorite() {
(this as any).api('notes/favorites/create', {
noteId: this.note.id
}).then(() => {
this.$destroy();
});
},
close() {
(this.$refs.backdrop as any).style.pointerEvents = 'none';
anime({
@@ -142,6 +151,7 @@ $border-color = rgba(27, 31, 35, 0.15)
> a
display block
padding 8px 16px
width 100%
&:hover
color $theme-color-foreground

View File

@@ -69,7 +69,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
.mk-poll-editor
root(isDark)
padding 8px
> .caution
@@ -102,6 +102,8 @@ export default Vue.extend({
padding 6px 8px
width 300px
font-size 14px
color isDark ? #fff : #000
background isDark ? #191b22 : #fff
border solid 1px rgba($theme-color, 0.1)
border-radius 4px
@@ -139,4 +141,10 @@ export default Vue.extend({
&:active
color darken($theme-color, 30%)
.mk-poll-editor[data-darkmode]
root(true)
.mk-poll-editor:not([data-darkmode])
root(false)
</style>

View File

@@ -68,7 +68,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
.mk-poll
root(isDark)
> ul
display block
@@ -81,7 +81,8 @@ export default Vue.extend({
margin 4px 0
padding 4px 8px
width 100%
border solid 1px #eee
color isDark ? #fff : #000
border solid 1px isDark ? #5e636f : #eee
border-radius 4px
overflow hidden
cursor pointer
@@ -108,6 +109,8 @@ export default Vue.extend({
margin-left 4px
> p
color isDark ? #a3aebf : #000
a
color inherit
@@ -121,4 +124,10 @@ export default Vue.extend({
&:active
background transparent
.mk-poll[data-darkmode]
root(true)
.mk-poll:not([data-darkmode])
root(false)
</style>

View File

@@ -65,16 +65,16 @@ export default Vue.extend({
iframe
width 100%
.mk-url-preview
root(isDark)
display block
font-size 16px
border solid 1px #eee
border solid 1px isDark ? #58606b : #eee
border-radius 4px
overflow hidden
&:hover
text-decoration none
border-color #ddd
border-color isDark ? #7d8590 : #ddd
> article > header > h1
text-decoration underline
@@ -99,11 +99,11 @@ iframe
> h1
margin 0
font-size 1em
color #555
color isDark ? #d6dae0 : #555
> p
margin 0
color #777
color isDark ? #a4aab3 : #777
font-size 0.8em
> footer
@@ -120,7 +120,7 @@ iframe
> p
display inline-block
margin 0
color #666
color isDark ? #b0b4bf : #666
font-size 0.8em
line-height 16px
vertical-align top
@@ -139,4 +139,10 @@ iframe
> article
padding 8px
.mk-url-preview[data-darkmode]
root(true)
.mk-url-preview:not([data-darkmode])
root(false)
</style>

View File

@@ -25,6 +25,7 @@ import updateBanner from './api/update-banner';
import MkIndex from './views/pages/index.vue';
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 MkHomeCustomize from './views/pages/home-customize.vue';
@@ -50,6 +51,7 @@ init(async (launch) => {
routes: [
{ path: '/', name: 'index', component: MkIndex },
{ path: '/i/customize-home', component: MkHomeCustomize },
{ path: '/i/favorites', component: MkFavorites },
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', component: MkDrive },
{ path: '/i/drive/folder/:folder', component: MkDrive },

View File

@@ -47,6 +47,23 @@ html
&[data-darkmode]
background #191B22
&, *
&::-webkit-scrollbar-track
background-color #282C37
&::-webkit-scrollbar
width 6px
height 6px
&::-webkit-scrollbar-thumb
background-color #454954
&:hover
background-color #535660
&:active
background-color $theme-color
body
display flex
flex-direction column

View File

@@ -219,7 +219,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
.mk-home
root(isDark)
display block
&[data-customize]
@@ -249,7 +249,8 @@ export default Vue.extend({
left 0
width 100%
height 48px
background #f7f7f7
color isDark ? #fff : #000
background isDark ? #313543 : #f7f7f7
box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
> a
@@ -289,7 +290,7 @@ export default Vue.extend({
line-height 48px
&.trash
border-left solid 1px #ddd
border-left solid 1px isDark ? #1c2023 : #ddd
> div
width 100%
@@ -329,7 +330,7 @@ export default Vue.extend({
.mk-post-form
margin-bottom 16px
border solid 1px #e5e5e5
border solid 1px rgba(#000, 0.075)
border-radius 4px
> *:not(.main)
@@ -357,4 +358,10 @@ export default Vue.extend({
max-width 700px
margin 0 auto
.mk-home[data-darkmode]
root(true)
.mk-home:not([data-darkmode])
root(false)
</style>

View File

@@ -4,15 +4,13 @@
<x-sub :note="p.reply"/>
</div>
<div class="renote" v-if="isRenote">
<p>
<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
</router-link>
%fa:retweet%
<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
<span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span>
</p>
<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
</router-link>
%fa:retweet%
<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
<span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span>
<mk-time :time="note.createdAt"/>
</div>
<article>
@@ -324,36 +322,44 @@ root(isDark)
border-radius 4px
> .renote
display flex
align-items baseline
padding 16px 32px
line-height 28px
color #9dbb00
background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
> p
margin 0
padding 16px 32px
line-height 28px
.avatar-anchor
display inline-block
.avatar-anchor
display inline-block
.avatar
vertical-align bottom
width 28px
height 28px
margin 0 8px 0 0
border-radius 6px
.avatar
vertical-align bottom
width 28px
height 28px
margin 0 8px 0 0
border-radius 6px
[data-fa]
margin-right 4px
[data-fa]
margin-right 4px
> span
flex-shrink 0
.name
font-weight bold
&:last-of-type
margin-right 8px
.name
overflow hidden
flex-shrink 1
text-overflow ellipsis
white-space nowrap
font-weight bold
> .mk-time
position absolute
top 16px
right 32px
display block
margin-left auto
flex-shrink 0
font-size 0.9em
line-height 28px
& + article
padding-top 8px
@@ -421,12 +427,14 @@ root(isDark)
margin 0 .5em 0 0
padding 1px 6px
font-size 12px
color #aaa
border solid 1px #ddd
color isDark ? #758188 :#aaa
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
> .username
margin 0 .5em 0 0
overflow hidden
text-overflow ellipsis
color isDark ? #606984 : #ccc
> .info
@@ -539,7 +547,7 @@ root(isDark)
> .mk-note-preview
padding 16px
border dashed 1px #c0dac6
border dashed 1px isDark ? #4e945e : #c0dac6
border-radius 8px
> footer

View File

@@ -1,12 +1,14 @@
<template>
<div class="mk-notes">
<template v-for="(note, i) in _notes">
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
<p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
<span>%fa:angle-up%{{ note._datetext }}</span>
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
<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)"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
<span>%fa:angle-up%{{ note._datetext }}</span>
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
</transition-group>
<footer>
<slot name="footer"></slot>
</footer>
@@ -51,21 +53,30 @@ export default Vue.extend({
<style lang="stylus" scoped>
root(isDark)
> .date
display block
margin 0
line-height 32px
font-size 14px
text-align center
color isDark ? #666b79 : #aaa
background isDark ? #242731 : #fdfdfd
border-bottom solid 1px isDark ? #1c2023 : #eaeaea
.transition
.mk-notes-enter
.mk-notes-leave-to
opacity 0
transform translateY(-30px)
span
margin 0 16px
> *
transition transform .3s ease, opacity .3s ease
[data-fa]
margin-right 8px
> .date
display block
margin 0
line-height 32px
font-size 14px
text-align center
color isDark ? #666b79 : #aaa
background isDark ? #242731 : #fdfdfd
border-bottom solid 1px isDark ? #1c2023 : #eaeaea
span
margin 0 16px
[data-fa]
margin-right 8px
> footer
> *

View File

@@ -302,7 +302,7 @@ root(isDark)
min-width 100%
min-height calc(16px + 12px + 12px)
font-size 16px
color #333
color isDark ? #fff : #333
background isDark ? #191d23 : #fff
outline none
border solid 1px rgba($theme-color, 0.1)
@@ -392,7 +392,7 @@ root(isDark)
cursor pointer
> .mk-poll-editor
background lighten($theme-color, 98%)
background isDark ? #181b23 : lighten($theme-color, 98%)
border solid 1px rgba($theme-color, 0.1)
border-top none
border-radius 0 0 4px 4px

View File

@@ -45,6 +45,8 @@
</div>
<mk-switch v-model="os.i.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
<mk-switch v-model="os.i.clientSettings.showReplyTarget" @change="onChangeShowReplyTarget" text="リプライ先を表示する"/>
<mk-switch v-model="os.i.clientSettings.showMyRenotes" @change="onChangeShowMyRenotes" text="自分の行ったRenoteをタイムラインに表示する"/>
<mk-switch v-model="os.i.clientSettings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="Renoteされた自分の投稿をタイムラインに表示する"/>
<mk-switch v-model="os.i.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
<span>位置情報が添付された投稿のマップを自動的に展開します</span>
</mk-switch>
@@ -319,6 +321,18 @@ export default Vue.extend({
value: v
});
},
onChangeShowMyRenotes(v) {
(this as any).api('i/update_client_setting', {
name: 'showMyRenotes',
value: v
});
},
onChangeShowRenotedMyNotes(v) {
(this as any).api('i/update_client_setting', {
name: 'showRenotedMyNotes',
value: v
});
},
onChangeShowMaps(v) {
(this as any).api('i/update_client_setting', {
name: 'showMaps',

View File

@@ -1,5 +1,6 @@
<template>
<div class="mk-home-timeline">
<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
<mk-friends-maker v-if="src == 'home' && alone"/>
<div class="fetching" v-if="fetching">
<mk-ellipsis-icon/>
@@ -20,6 +21,9 @@
import Vue from 'vue';
import { url } from '../../../config';
const fetchLimit = 10;
const displayLimit = 30;
export default Vue.extend({
props: {
src: {
@@ -34,6 +38,7 @@ export default Vue.extend({
moreFetching: false,
existMore: false,
notes: [],
queue: [],
connection: null,
connectionId: null,
date: null
@@ -59,6 +64,10 @@ export default Vue.extend({
: this.src == 'local'
? 'notes/local-timeline'
: 'notes/global-timeline';
},
canFetchMore(): boolean {
return !this.moreFetching && !this.fetching && this.notes.length > 0 && this.existMore;
}
},
@@ -72,6 +81,9 @@ export default Vue.extend({
this.connection.on('unfollow', this.onChangeFollowing);
}
document.addEventListener('keydown', this.onKeydown);
window.addEventListener('scroll', this.onScroll);
this.fetch();
},
@@ -82,17 +94,27 @@ export default Vue.extend({
this.connection.off('unfollow', this.onChangeFollowing);
}
this.stream.dispose(this.connectionId);
document.removeEventListener('keydown', this.onKeydown);
window.removeEventListener('scroll', this.onScroll);
},
methods: {
isScrollTop() {
return window.scrollY <= 8;
},
fetch(cb?) {
this.queue = [];
this.fetching = true;
(this as any).api(this.endpoint, {
limit: 11,
untilDate: this.date ? this.date.getTime() : undefined
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 == 11) {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
}
@@ -104,13 +126,17 @@ export default Vue.extend({
},
more() {
if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return;
if (!this.canFetchMore) return;
this.moreFetching = true;
(this as any).api(this.endpoint, {
limit: 11,
untilId: this.notes[this.notes.length - 1].id
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 == 11) {
if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
this.existMore = false;
@@ -120,18 +146,51 @@ export default Vue.extend({
});
},
onNote(note) {
prependNote(note, silent = false) {
// サウンドを再生する
if ((this as any).os.isEnableSounds) {
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);
const isTop = window.scrollY > 8;
if (isTop) this.notes.pop();
// オーバーフローしたら古い投稿は捨てる
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 ((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() {
@@ -145,13 +204,41 @@ export default Vue.extend({
warp(date) {
this.date = date;
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();
}
},
onKeydown(e) {
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
if (e.which == 84) { // t
this.focus();
}
}
},
}
});
</script>
<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-friends-maker
border-bottom solid 1px #eee

View File

@@ -27,35 +27,12 @@ export default Vue.extend({
},
mounted() {
document.addEventListener('keydown', this.onKeydown);
window.addEventListener('scroll', this.onScroll);
(this.$refs.tl as any).$once('loaded', () => {
this.$emit('loaded');
});
},
beforeDestroy() {
document.removeEventListener('keydown', this.onKeydown);
window.removeEventListener('scroll', this.onScroll);
},
methods: {
onScroll() {
if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
const current = window.scrollY + window.innerHeight;
if (current > document.body.offsetHeight - 8) (this.$refs.tl as any).more();
}
},
onKeydown(e) {
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
if (e.which == 84) { // t
(this.$refs.tl as any).focus();
}
}
},
warp(date) {
(this.$refs.tl as any).warp(date);
}

View File

@@ -13,6 +13,9 @@
<li @click="drive">
<p>%fa:cloud%<span>%i18n:@drive%</span>%fa:angle-right%</p>
</li>
<li>
<router-link to="/i/favorites">%fa:star%<span>%i18n:@favorites%</span>%fa:angle-right%</router-link>
</li>
</ul>
<ul>
<li>
@@ -24,7 +27,7 @@
</ul>
<ul>
<li @click="signout">
<p>%fa:power-off%<span>%i18n:@signout%</span></p>
<p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p>
</li>
</ul>
<ul>
@@ -209,7 +212,7 @@ root(isDark)
pointer-events none
> span:first-child
padding-left 16px
padding-left 22px
> [data-fa]:first-child
margin-right 6px
@@ -233,6 +236,16 @@ root(isDark)
&:active
background darken($theme-color, 10%)
&.signout
$color = #e64137
&:hover, &:active
background $color
color #fff
&:active
background darken($color, 10%)
.zoom-in-top-enter-active,
.zoom-in-top-leave-active {
transform-origin: center -16px;

View File

@@ -43,6 +43,8 @@ export default Vue.extend({
XClock,
},
mounted() {
this.$store.commit('setUiHeaderHeight', 48);
if ((this as any).os.isSignedIn) {
const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000
const isHisasiburi = ago >= 3600;

View File

@@ -141,7 +141,7 @@ root(isDark)
> .description
padding 0 16px
font-size 0.7em
color #555
color isDark ? #9ea4ad : #555
> .status
padding 8px 16px

View File

@@ -0,0 +1,73 @@
<template>
<mk-ui>
<main v-if="!fetching">
<template v-for="favorite in favorites">
<mk-note-detail :note="favorite.note" :key="favorite.note.id"/>
</template>
<a v-if="existMore" @click="more">さらに読み込む</a>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
export default Vue.extend({
data() {
return {
fetching: true,
favorites: [],
existMore: false,
moreFetching: false
};
},
created() {
this.fetch();
},
methods: {
fetch() {
Progress.start();
this.fetching = true;
(this as any).api('i/favorites', {
limit: 11
}).then(favorites => {
if (favorites.length == 11) {
this.existMore = true;
favorites.pop();
}
this.favorites = favorites;
this.fetching = false;
Progress.done();
});
},
more() {
this.moreFetching = true;
(this as any).api('i/favorites', {
limit: 11,
maxId: this.favorites[this.favorites.length - 1].id
}).then(favorites => {
if (favorites.length == 11) {
this.existMore = true;
favorites.pop();
} else {
this.existMore = false;
}
this.favorites = this.favorites.concat(favorites);
this.moreFetching = false;
});
}
}
});
</script>
<style lang="stylus" scoped>
main
margin 0 auto
padding 16px
max-width 700px
</style>

View File

@@ -2,6 +2,7 @@
<div class="mkw-messaging">
<mk-widget-container :show-header="props.design == 0">
<template slot="header">%fa:comments%%i18n:@title%</template>
<button slot="func" @click="add">%fa:plus%</button>
<mk-messaging ref="index" compact @navigate="navigate"/>
</mk-widget-container>
@@ -11,6 +12,7 @@
<script lang="ts">
import define from '../../../common/define-widget';
import MkMessagingRoomWindow from '../components/messaging-room-window.vue';
import MkMessagingWindow from '../components/messaging-window.vue';
export default define({
name: 'messaging',
@@ -24,6 +26,9 @@ export default define({
user: user
});
},
add() {
(this as any).os.new(MkMessagingWindow);
},
func() {
if (this.props.design == 1) {
this.props.design = 0;
@@ -37,7 +42,7 @@ export default define({
<style lang="stylus" scoped>
.mkw-messaging
> .mk-messaging
.mk-messaging
max-height 250px
overflow auto

View File

@@ -67,7 +67,7 @@ root(isDark)
> .poll
padding 16px
font-size 12px
color #555
color isDark ? #9ea4ad : #555
> p
margin 0 0 8px 0

View File

@@ -91,7 +91,7 @@ root(isDark)
> .banner
height 100px
background-color #f5f5f5
background-color isDark ? #303e4a : #f5f5f5
background-size cover
background-position center
cursor pointer

View File

@@ -3,6 +3,7 @@
*/
import Vue from 'vue';
import Vuex from 'vuex';
import VueRouter from 'vue-router';
import VModal from 'vue-js-modal';
import * as TreeView from 'vue-json-tree-view';
@@ -23,6 +24,7 @@ switch (lang) {
default: elementLocale = ElementLocaleEn; break;
}
Vue.use(Vuex);
Vue.use(VueRouter);
Vue.use(VModal);
Vue.use(TreeView);
@@ -39,6 +41,17 @@ require('./common/views/widgets');
// Register global filters
require('./common/views/filters');
const store = new Vuex.Store({
state: {
uiHeaderHeight: 0
},
mutations: {
setUiHeaderHeight(state, height) {
state.uiHeaderHeight = height;
}
}
});
Vue.mixin({
destroyed(this: any) {
if (this.$el.parentNode) {
@@ -145,6 +158,7 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
});
const app = new Vue({
store,
router,
created() {
this.$watch('os.i', i => {

View File

@@ -4,15 +4,13 @@
<x-sub :note="p.reply"/>
</div>
<div class="renote" v-if="isRenote">
<p>
<router-link class="avatar-anchor" :to="note.user | userPage">
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
</router-link>
%fa:retweet%
<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
<span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span>
</p>
<router-link class="avatar-anchor" :to="note.user | userPage">
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
</router-link>
%fa:retweet%
<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
<span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span>
<mk-time :time="note.createdAt"/>
</div>
<article>
@@ -251,42 +249,47 @@ export default Vue.extend({
font-size 16px
> .renote
display flex
align-items baseline
padding 8px 16px
line-height 28px
color #9dbb00
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
> p
margin 0
padding 8px 16px
line-height 28px
@media (min-width 500px)
padding 16px
@media (min-width 500px)
padding 16px
.avatar-anchor
display inline-block
.avatar-anchor
display inline-block
.avatar
vertical-align bottom
width 28px
height 28px
margin 0 8px 0 0
border-radius 6px
.avatar
vertical-align bottom
width 28px
height 28px
margin 0 8px 0 0
border-radius 6px
[data-fa]
margin-right 4px
[data-fa]
margin-right 4px
> span
flex-shrink 0
.name
font-weight bold
&:last-of-type
margin-right 8px
.name
overflow hidden
flex-shrink 1
text-overflow ellipsis
white-space nowrap
font-weight bold
> .mk-time
position absolute
top 8px
right 16px
display block
margin-left auto
flex-shrink 0
font-size 0.9em
line-height 28px
@media (min-width 500px)
top 16px
& + article
padding-top 8px
@@ -368,6 +371,8 @@ export default Vue.extend({
> .username
margin 0 0.5em 0 0
overflow hidden
text-overflow ellipsis
color #ccc
> .info

View File

@@ -2,13 +2,15 @@
<div class="mk-notes">
<slot name="head"></slot>
<slot></slot>
<template v-for="(note, i) in _notes">
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
<p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
<span>%fa:angle-up%{{ note._datetext }}</span>
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
<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)"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
<span>%fa:angle-up%{{ note._datetext }}</span>
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
</transition-group>
<footer>
<slot name="tail"></slot>
</footer>
@@ -52,6 +54,31 @@ export default Vue.extend({
border-radius 8px
box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
.transition
.mk-notes-enter
.mk-notes-leave-to
opacity 0
transform translateY(-30px)
> *
transition transform .3s ease, opacity .3s ease
> .date
display block
margin 0
line-height 32px
text-align center
font-size 0.9em
color #aaa
background #fdfdfd
border-bottom solid 1px #eaeaea
span
margin 0 16px
[data-fa]
margin-right 8px
> .init
padding 64px 0
text-align center
@@ -73,22 +100,6 @@ export default Vue.extend({
font-size 3em
color #ccc
> .date
display block
margin 0
line-height 32px
text-align center
font-size 0.9em
color #aaa
background #fdfdfd
border-bottom solid 1px #eaeaea
span
margin 0 16px
[data-fa]
margin-right 8px
> footer
text-align center
border-top solid 1px #eaeaea

View File

@@ -1,5 +1,6 @@
<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">
@@ -9,7 +10,7 @@
%fa:R comments%
%i18n:@empty%
</div>
<button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail">
<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>
@@ -20,7 +21,8 @@
<script lang="ts">
import Vue from 'vue';
const limit = 10;
const fetchLimit = 10;
const displayLimit = 30;
export default Vue.extend({
props: {
@@ -30,21 +32,29 @@ export default Vue.extend({
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();
@@ -53,22 +63,35 @@ export default Vue.extend({
this.connection.on('follow', this.onChangeFollowing);
this.connection.on('unfollow', this.onChangeFollowing);
this.fetch();
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: limit + 1,
untilDate: this.date ? (this.date as any).getTime() : undefined
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 == limit + 1) {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
}
@@ -78,13 +101,16 @@ this.fetch();
if (cb) cb();
});
},
more() {
this.moreFetching = true;
(this as any).api('notes/timeline', {
limit: limit + 1,
untilId: this.notes[this.notes.length - 1].id
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 == limit + 1) {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
} else {
@@ -94,20 +120,71 @@ this.fetch();
this.moreFetching = false;
});
},
onNote(note) {
prependNote(note) {
// Prepent a note
this.notes.unshift(note);
const isTop = window.scrollY > 8;
if (isTop) this.notes.pop();
// オーバーフローしたら古い投稿は捨てる
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>
.mk-friends-maker
margin-bottom 8px
@import '~const.styl'
.mk-timeline
> .newer-indicator
position -webkit-sticky
position sticky
z-index 100
height 3px
background $theme-color
> .mk-friends-maker
margin-bottom 8px
</style>

View File

@@ -32,6 +32,8 @@ export default Vue.extend({
};
},
mounted() {
this.$store.commit('setUiHeaderHeight', 48);
if ((this as any).os.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
@@ -150,6 +152,9 @@ export default Vue.extend({
width 100%
box-shadow 0 1px 0 rgba(#000, 0.075)
&, *
user-select none
> .main
color rgba(#fff, 0.9)

View File

@@ -1,7 +1,10 @@
import * as mongo from 'mongodb';
import deepcopy = require('deepcopy');
import db from '../db/mongodb';
import { pack as packNote } from './note';
const Favorite = db.get<IFavorite>('favorites');
Favorite.createIndex(['userId', 'noteId'], { unique: true });
export default Favorite;
export type IFavorite = {
@@ -37,3 +40,35 @@ export async function deleteFavorite(favorite: string | mongo.ObjectID | IFavori
_id: f._id
});
}
/**
* Pack a favorite for API response
*/
export const pack = (
favorite: any,
me: any
) => new Promise<any>(async (resolve, reject) => {
let _favorite: any;
// Populate the favorite if 'favorite' is ID
if (mongo.ObjectID.prototype.isPrototypeOf(favorite)) {
_favorite = await Favorite.findOne({
_id: favorite
});
} else if (typeof favorite === 'string') {
_favorite = await Favorite.findOne({
_id: new mongo.ObjectID(favorite)
});
} else {
_favorite = deepcopy(favorite);
}
// Rename _id to id
_favorite.id = _favorite._id;
delete _favorite._id;
// Populate note
_favorite.note = await packNote(_favorite.noteId, me);
resolve(_favorite);
});

View File

@@ -233,6 +233,12 @@ const endpoints: Endpoint[] = [
kind: 'notification-read'
},
{
name: 'i/favorites',
withCredential: true,
kind: 'favorites-read'
},
{
name: 'othello/match',
withCredential: true

View File

@@ -2,43 +2,52 @@
* Module dependencies
*/
import $ from 'cafy';
import Favorite from '../../../../models/favorite';
import { pack } from '../../../../models/note';
import Favorite, { pack } from '../../../../models/favorite';
/**
* Get followers of a user
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
* Get favorited notes
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param');
// Get 'offset' parameter
const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
if (offsetErr) return rej('invalid offset param');
// Get 'sinceId' parameter
const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'sort' parameter
const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$;
if (sortError) return rej('invalid sort param');
// Get 'untilId' parameter
const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
if (sinceId && untilId) {
return rej('cannot set sinceId and untilId');
}
const query = {
userId: user._id
} as any;
const sort = {
_id: -1
};
if (sinceId) {
sort._id = 1;
query._id = {
$gt: sinceId
};
} else if (untilId) {
query._id = {
$lt: untilId
};
}
// Get favorites
const favorites = await Favorite
.find({
userId: user._id
}, {
limit: limit,
skip: offset,
sort: {
_id: sort == 'asc' ? 1 : -1
}
});
.find(query, { limit, sort });
// Serialize
res(await Promise.all(favorites.map(async favorite =>
await pack(favorite.noteId)
)));
res(await Promise.all(favorites.map(favorite => pack(favorite, user))));
});

View File

@@ -37,6 +37,14 @@ module.exports = async (params, user, app) => {
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';
const [followings, watchingChannelIds, mutedUserIds] = await Promise.all([
// フォローを取得
// Fetch following
@@ -84,38 +92,76 @@ module.exports = async (params, user, app) => {
});
const query = {
$or: [{
$and: [{
// フォローしている人のタイムラインへの投稿
$or: followQuery
}, {
// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
$or: [{
channelId: {
$exists: false
}
$and: [{
$or: [{
$and: [{
// フォローしている人のタイムラインへの投稿
$or: followQuery
}, {
channelId: null
// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
$or: [{
channelId: {
$exists: false
}
}, {
channelId: null
}]
}]
}]
}, {
// Watchしているチャンネルへの投稿
channelId: {
$in: watchingChannelIds
}
}],
// mute
userId: {
$nin: mutedUserIds
},
'_reply.userId': {
$nin: mutedUserIds
},
'_renote.userId': {
$nin: mutedUserIds
},
}, {
// Watchしているチャンネルへの投稿
channelId: {
$in: watchingChannelIds
}
}],
// 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 = {

View File

@@ -61,7 +61,7 @@ router.get('/manifest.json', async ctx => {
router.use('/docs', docs.routes());
// URL preview endpoint
router.get('url', require('./url-preview'));
router.get('/url', require('./url-preview'));
// Render base html for all requests
router.get('*', async ctx => {

View File

@@ -54,9 +54,9 @@ const handlers = {
document.body.appendChild(blockquote);
},
title({ document }, { title }) {
title({ document }, { content }) {
const h1 = document.createElement('h1');
h1.textContent = title;
h1.textContent = content;
document.body.appendChild(h1);
},
@@ -75,6 +75,13 @@ const handlers = {
a.href = url;
a.textContent = url;
document.body.appendChild(a);
},
search({ document }, { content, query }) {
const a = document.createElement('a');
a.href = `https://www.google.com/?#q=${query}`;
a.textContent = content;
document.body.appendChild(a);
}
};

View File

@@ -0,0 +1,13 @@
/**
* Search
*/
module.exports = text => {
const match = text.match(/^(.+?) 検索(\n|$)/);
if (!match) return null;
return {
type: 'search',
content: match[0],
query: match[1]
};
};