Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a57a1d809b | ||
![]() |
29a121f5b2 | ||
![]() |
4cfb360d44 | ||
![]() |
441796f845 | ||
![]() |
3c80f0eaca | ||
![]() |
86e1b792c2 | ||
![]() |
decb257136 | ||
![]() |
d670591713 | ||
![]() |
dee2b77bdc | ||
![]() |
6119312a74 | ||
![]() |
b77a8af6e3 | ||
![]() |
c9e9ecd699 | ||
![]() |
4a0f9d8280 | ||
![]() |
797b5d2311 | ||
![]() |
b14d7b45ae | ||
![]() |
d721a143a8 | ||
![]() |
fde9c783ae | ||
![]() |
c80b288db3 | ||
![]() |
93898b7a5f | ||
![]() |
a0a2335c16 | ||
![]() |
716b41107b | ||
![]() |
a9a7a89b8b | ||
![]() |
4bf85a0e6b | ||
![]() |
dd509333a3 | ||
![]() |
8ccfbbf24c | ||
![]() |
4953842ff1 | ||
![]() |
8a8d97b8c7 | ||
![]() |
5916fcca6a | ||
![]() |
2105e4964b | ||
![]() |
8ed30d1ff3 | ||
![]() |
6f546efdb7 | ||
![]() |
c4f62296a4 |
@@ -44,9 +44,9 @@ If you want to translate Misskey, please see [Translation guide](./docs/translat
|
|||||||
|
|
||||||
:mortar_board: Notable contributors
|
:mortar_board: Notable contributors
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] | ![rinsuki][rinsuki-icon] |
|
| ![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] | [rinsuki][rinsuki-link] |
|
| [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)
|
[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
|
[akihikodaki-icon]: https://avatars2.githubusercontent.com/u/17036990?s=70&v=4
|
||||||
[rinsuki-link]: https://github.com/rinsuki
|
[rinsuki-link]: https://github.com/rinsuki
|
||||||
[rinsuki-icon]: https://avatars0.githubusercontent.com/u/6533808?s=70&v=4
|
[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-link]: https://github.com/mirro-san
|
||||||
[mirro-san-icon]: https://avatars1.githubusercontent.com/u/17948612?s=70&v=4
|
[mirro-san-icon]: https://avatars1.githubusercontent.com/u/17948612?s=70&v=4
|
||||||
|
@@ -98,7 +98,9 @@ common/views/components/nav.vue:
|
|||||||
feedback: "Feedback"
|
feedback: "Feedback"
|
||||||
|
|
||||||
common/views/components/note-menu.vue:
|
common/views/components/note-menu.vue:
|
||||||
|
favorite: "Favorite this note"
|
||||||
pin: "Pin to profile page"
|
pin: "Pin to profile page"
|
||||||
|
remote: "Show on origin"
|
||||||
|
|
||||||
common/views/components/poll.vue:
|
common/views/components/poll.vue:
|
||||||
vote-to: "Vote for '{}'"
|
vote-to: "Vote for '{}'"
|
||||||
@@ -367,6 +369,7 @@ desktop/views/components/settings.profile.vue:
|
|||||||
desktop/views/components/ui.header.account.vue:
|
desktop/views/components/ui.header.account.vue:
|
||||||
profile: "Your profile"
|
profile: "Your profile"
|
||||||
drive: "Drive"
|
drive: "Drive"
|
||||||
|
favorites: "Favorites"
|
||||||
customize: "Customize"
|
customize: "Customize"
|
||||||
settings: "Settings"
|
settings: "Settings"
|
||||||
signout: "Sign out"
|
signout: "Sign out"
|
||||||
|
@@ -98,7 +98,9 @@ common/views/components/nav.vue:
|
|||||||
feedback: "フィードバック"
|
feedback: "フィードバック"
|
||||||
|
|
||||||
common/views/components/note-menu.vue:
|
common/views/components/note-menu.vue:
|
||||||
|
favorite: "Favorite this note"
|
||||||
pin: "Épingler sur votre profile"
|
pin: "Épingler sur votre profile"
|
||||||
|
remote: "投稿元で見る"
|
||||||
|
|
||||||
common/views/components/poll.vue:
|
common/views/components/poll.vue:
|
||||||
vote-to: "Voter pour '{}'"
|
vote-to: "Voter pour '{}'"
|
||||||
@@ -367,6 +369,7 @@ desktop/views/components/settings.profile.vue:
|
|||||||
desktop/views/components/ui.header.account.vue:
|
desktop/views/components/ui.header.account.vue:
|
||||||
profile: "Votre profil"
|
profile: "Votre profil"
|
||||||
drive: "Drive"
|
drive: "Drive"
|
||||||
|
favorites: "Favorites"
|
||||||
customize: "Modifications"
|
customize: "Modifications"
|
||||||
settings: "Réglages"
|
settings: "Réglages"
|
||||||
signout: "Déconnexion"
|
signout: "Déconnexion"
|
||||||
|
@@ -98,7 +98,9 @@ common/views/components/nav.vue:
|
|||||||
feedback: "フィードバック"
|
feedback: "フィードバック"
|
||||||
|
|
||||||
common/views/components/note-menu.vue:
|
common/views/components/note-menu.vue:
|
||||||
|
favorite: "お気に入り"
|
||||||
pin: "ピン留め"
|
pin: "ピン留め"
|
||||||
|
remote: "投稿元で見る"
|
||||||
|
|
||||||
common/views/components/poll.vue:
|
common/views/components/poll.vue:
|
||||||
vote-to: "「{}」に投票する"
|
vote-to: "「{}」に投票する"
|
||||||
@@ -367,6 +369,7 @@ desktop/views/components/settings.profile.vue:
|
|||||||
desktop/views/components/ui.header.account.vue:
|
desktop/views/components/ui.header.account.vue:
|
||||||
profile: "プロフィール"
|
profile: "プロフィール"
|
||||||
drive: "ドライブ"
|
drive: "ドライブ"
|
||||||
|
favorites: "お気に入り"
|
||||||
customize: "カスタマイズ"
|
customize: "カスタマイズ"
|
||||||
settings: "設定"
|
settings: "設定"
|
||||||
signout: "サインアウト"
|
signout: "サインアウト"
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "0.0.5042",
|
"version": "0.0.5074",
|
||||||
"codename": "nighthike",
|
"codename": "nighthike",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "A miniblog-based SNS",
|
"description": "A miniblog-based SNS",
|
||||||
@@ -208,6 +208,7 @@
|
|||||||
"vue-router": "3.0.1",
|
"vue-router": "3.0.1",
|
||||||
"vue-template-compiler": "2.5.16",
|
"vue-template-compiler": "2.5.16",
|
||||||
"vuedraggable": "2.16.0",
|
"vuedraggable": "2.16.0",
|
||||||
|
"vuex": "^3.0.1",
|
||||||
"web-push": "3.3.0",
|
"web-push": "3.3.0",
|
||||||
"webfinger.js": "2.6.6",
|
"webfinger.js": "2.6.6",
|
||||||
"webpack": "4.6.0",
|
"webpack": "4.6.0",
|
||||||
|
@@ -11,14 +11,12 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Chromeで確認したことなのですが、constやletを用いたとしても
|
(function() {
|
||||||
// グローバルなスコープで定数/変数を定義するとwindowのプロパティ
|
// キャッシュ削除要求があれば従う
|
||||||
// としてそれがアクセスできるようになる訳ではありませんが、普通に
|
if (localStorage.getItem('shouldFlush') == 'true') {
|
||||||
// コンソールから定数/変数名を入力するとアクセスできてしまいます。
|
refresh();
|
||||||
// ブロック内に入れてスコープを非グローバル化するとそれが防げます
|
return;
|
||||||
// (Chrome以外のブラウザでは検証していません)
|
}
|
||||||
{
|
|
||||||
if (localStorage.getItem('shouldFlush') == 'true') refresh();
|
|
||||||
|
|
||||||
// Get the current url information
|
// Get the current url information
|
||||||
const url = new URL(location.href);
|
const url = new URL(location.href);
|
||||||
@@ -80,11 +78,16 @@
|
|||||||
const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug)
|
const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug)
|
||||||
|| ENV != 'production';
|
|| ENV != 'production';
|
||||||
|
|
||||||
|
// Get salt query
|
||||||
|
const salt = localStorage.getItem('salt')
|
||||||
|
? '?salt=' + localStorage.getItem('salt')
|
||||||
|
: '';
|
||||||
|
|
||||||
// Load an app script
|
// Load an app script
|
||||||
// Note: 'async' make it possible to load the script asyncly.
|
// Note: 'async' make it possible to load the script asyncly.
|
||||||
// 'defer' make it possible to run the script when the dom loaded.
|
// 'defer' make it possible to run the script when the dom loaded.
|
||||||
const script = document.createElement('script');
|
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('async', 'true');
|
||||||
script.setAttribute('defer', 'true');
|
script.setAttribute('defer', 'true');
|
||||||
head.appendChild(script);
|
head.appendChild(script);
|
||||||
@@ -120,6 +123,9 @@
|
|||||||
function refresh() {
|
function refresh() {
|
||||||
localStorage.setItem('shouldFlush', 'false');
|
localStorage.setItem('shouldFlush', 'false');
|
||||||
|
|
||||||
|
// Random
|
||||||
|
localStorage.setItem('salt', Math.random().toString());
|
||||||
|
|
||||||
// Clear cache (serive worker)
|
// Clear cache (serive worker)
|
||||||
try {
|
try {
|
||||||
navigator.serviceWorker.controller.postMessage('clear');
|
navigator.serviceWorker.controller.postMessage('clear');
|
||||||
@@ -134,4 +140,4 @@
|
|||||||
// Force reload
|
// Force reload
|
||||||
location.reload(true);
|
location.reload(true);
|
||||||
}
|
}
|
||||||
}
|
})();
|
||||||
|
@@ -21,7 +21,9 @@ const defaultSettings = {
|
|||||||
showMaps: true,
|
showMaps: true,
|
||||||
showPostFormOnTopOfTl: false,
|
showPostFormOnTopOfTl: false,
|
||||||
gradientWindowHeader: false,
|
gradientWindowHeader: false,
|
||||||
showReplyTarget: true
|
showReplyTarget: true,
|
||||||
|
showMyRenotes: true,
|
||||||
|
showRenotedMyNotes: true
|
||||||
};
|
};
|
||||||
|
|
||||||
//#region api requests
|
//#region api requests
|
||||||
|
67
src/client/app/common/views/components/google.vue
Normal file
67
src/client/app/common/views/components/google.vue
Normal 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>
|
@@ -4,6 +4,7 @@ import parse from '../../../../../text/parse';
|
|||||||
import getAcct from '../../../../../acct/render';
|
import getAcct from '../../../../../acct/render';
|
||||||
import { url } from '../../../config';
|
import { url } from '../../../config';
|
||||||
import MkUrl from './url.vue';
|
import MkUrl from './url.vue';
|
||||||
|
import MkGoogle from './google.vue';
|
||||||
|
|
||||||
const flatten = list => list.reduce(
|
const flatten = list => list.reduce(
|
||||||
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
|
(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];
|
const emoji = emojilib.lib[token.emoji];
|
||||||
return createElement('span', emoji ? emoji.char : token.content);
|
return createElement('span', emoji ? emoji.char : token.content);
|
||||||
|
|
||||||
|
case 'search':
|
||||||
|
return createElement(MkGoogle, {
|
||||||
|
props: {
|
||||||
|
q: token.query
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('unknown ast type:', token.type);
|
console.log('unknown ast type:', token.type);
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
<div class="mk-note-menu">
|
<div class="mk-note-menu">
|
||||||
<div class="backdrop" ref="backdrop" @click="close"></div>
|
<div class="backdrop" ref="backdrop" @click="close"></div>
|
||||||
<div class="popover" :class="{ compact }" ref="popover">
|
<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>
|
<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>
|
<a v-if="note.uri" :href="note.uri" target="_blank">%i18n:@remote%</a>
|
||||||
</div>
|
</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() {
|
close() {
|
||||||
(this.$refs.backdrop as any).style.pointerEvents = 'none';
|
(this.$refs.backdrop as any).style.pointerEvents = 'none';
|
||||||
anime({
|
anime({
|
||||||
@@ -142,6 +151,7 @@ $border-color = rgba(27, 31, 35, 0.15)
|
|||||||
> a
|
> a
|
||||||
display block
|
display block
|
||||||
padding 8px 16px
|
padding 8px 16px
|
||||||
|
width 100%
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
color $theme-color-foreground
|
color $theme-color-foreground
|
||||||
|
@@ -69,7 +69,7 @@ export default Vue.extend({
|
|||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
@import '~const.styl'
|
@import '~const.styl'
|
||||||
|
|
||||||
.mk-poll-editor
|
root(isDark)
|
||||||
padding 8px
|
padding 8px
|
||||||
|
|
||||||
> .caution
|
> .caution
|
||||||
@@ -102,6 +102,8 @@ export default Vue.extend({
|
|||||||
padding 6px 8px
|
padding 6px 8px
|
||||||
width 300px
|
width 300px
|
||||||
font-size 14px
|
font-size 14px
|
||||||
|
color isDark ? #fff : #000
|
||||||
|
background isDark ? #191b22 : #fff
|
||||||
border solid 1px rgba($theme-color, 0.1)
|
border solid 1px rgba($theme-color, 0.1)
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
|
|
||||||
@@ -139,4 +141,10 @@ export default Vue.extend({
|
|||||||
&:active
|
&:active
|
||||||
color darken($theme-color, 30%)
|
color darken($theme-color, 30%)
|
||||||
|
|
||||||
|
.mk-poll-editor[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.mk-poll-editor:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -68,7 +68,7 @@ export default Vue.extend({
|
|||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
@import '~const.styl'
|
@import '~const.styl'
|
||||||
|
|
||||||
.mk-poll
|
root(isDark)
|
||||||
|
|
||||||
> ul
|
> ul
|
||||||
display block
|
display block
|
||||||
@@ -81,7 +81,8 @@ export default Vue.extend({
|
|||||||
margin 4px 0
|
margin 4px 0
|
||||||
padding 4px 8px
|
padding 4px 8px
|
||||||
width 100%
|
width 100%
|
||||||
border solid 1px #eee
|
color isDark ? #fff : #000
|
||||||
|
border solid 1px isDark ? #5e636f : #eee
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
overflow hidden
|
overflow hidden
|
||||||
cursor pointer
|
cursor pointer
|
||||||
@@ -108,6 +109,8 @@ export default Vue.extend({
|
|||||||
margin-left 4px
|
margin-left 4px
|
||||||
|
|
||||||
> p
|
> p
|
||||||
|
color isDark ? #a3aebf : #000
|
||||||
|
|
||||||
a
|
a
|
||||||
color inherit
|
color inherit
|
||||||
|
|
||||||
@@ -121,4 +124,10 @@ export default Vue.extend({
|
|||||||
&:active
|
&:active
|
||||||
background transparent
|
background transparent
|
||||||
|
|
||||||
|
.mk-poll[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.mk-poll:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -65,16 +65,16 @@ export default Vue.extend({
|
|||||||
iframe
|
iframe
|
||||||
width 100%
|
width 100%
|
||||||
|
|
||||||
.mk-url-preview
|
root(isDark)
|
||||||
display block
|
display block
|
||||||
font-size 16px
|
font-size 16px
|
||||||
border solid 1px #eee
|
border solid 1px isDark ? #58606b : #eee
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
overflow hidden
|
overflow hidden
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
text-decoration none
|
text-decoration none
|
||||||
border-color #ddd
|
border-color isDark ? #7d8590 : #ddd
|
||||||
|
|
||||||
> article > header > h1
|
> article > header > h1
|
||||||
text-decoration underline
|
text-decoration underline
|
||||||
@@ -99,11 +99,11 @@ iframe
|
|||||||
> h1
|
> h1
|
||||||
margin 0
|
margin 0
|
||||||
font-size 1em
|
font-size 1em
|
||||||
color #555
|
color isDark ? #d6dae0 : #555
|
||||||
|
|
||||||
> p
|
> p
|
||||||
margin 0
|
margin 0
|
||||||
color #777
|
color isDark ? #a4aab3 : #777
|
||||||
font-size 0.8em
|
font-size 0.8em
|
||||||
|
|
||||||
> footer
|
> footer
|
||||||
@@ -120,7 +120,7 @@ iframe
|
|||||||
> p
|
> p
|
||||||
display inline-block
|
display inline-block
|
||||||
margin 0
|
margin 0
|
||||||
color #666
|
color isDark ? #b0b4bf : #666
|
||||||
font-size 0.8em
|
font-size 0.8em
|
||||||
line-height 16px
|
line-height 16px
|
||||||
vertical-align top
|
vertical-align top
|
||||||
@@ -139,4 +139,10 @@ iframe
|
|||||||
> article
|
> article
|
||||||
padding 8px
|
padding 8px
|
||||||
|
|
||||||
|
.mk-url-preview[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.mk-url-preview:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -25,6 +25,7 @@ import updateBanner from './api/update-banner';
|
|||||||
|
|
||||||
import MkIndex from './views/pages/index.vue';
|
import MkIndex from './views/pages/index.vue';
|
||||||
import MkUser from './views/pages/user/user.vue';
|
import MkUser from './views/pages/user/user.vue';
|
||||||
|
import MkFavorites from './views/pages/favorites.vue';
|
||||||
import MkSelectDrive from './views/pages/selectdrive.vue';
|
import MkSelectDrive from './views/pages/selectdrive.vue';
|
||||||
import MkDrive from './views/pages/drive.vue';
|
import MkDrive from './views/pages/drive.vue';
|
||||||
import MkHomeCustomize from './views/pages/home-customize.vue';
|
import MkHomeCustomize from './views/pages/home-customize.vue';
|
||||||
@@ -50,6 +51,7 @@ init(async (launch) => {
|
|||||||
routes: [
|
routes: [
|
||||||
{ path: '/', name: 'index', component: MkIndex },
|
{ path: '/', name: 'index', component: MkIndex },
|
||||||
{ path: '/i/customize-home', component: MkHomeCustomize },
|
{ path: '/i/customize-home', component: MkHomeCustomize },
|
||||||
|
{ path: '/i/favorites', component: MkFavorites },
|
||||||
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
||||||
{ path: '/i/drive', component: MkDrive },
|
{ path: '/i/drive', component: MkDrive },
|
||||||
{ path: '/i/drive/folder/:folder', component: MkDrive },
|
{ path: '/i/drive/folder/:folder', component: MkDrive },
|
||||||
|
@@ -47,6 +47,23 @@ html
|
|||||||
&[data-darkmode]
|
&[data-darkmode]
|
||||||
background #191B22
|
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
|
body
|
||||||
display flex
|
display flex
|
||||||
flex-direction column
|
flex-direction column
|
||||||
|
@@ -219,7 +219,7 @@ export default Vue.extend({
|
|||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
@import '~const.styl'
|
@import '~const.styl'
|
||||||
|
|
||||||
.mk-home
|
root(isDark)
|
||||||
display block
|
display block
|
||||||
|
|
||||||
&[data-customize]
|
&[data-customize]
|
||||||
@@ -249,7 +249,8 @@ export default Vue.extend({
|
|||||||
left 0
|
left 0
|
||||||
width 100%
|
width 100%
|
||||||
height 48px
|
height 48px
|
||||||
background #f7f7f7
|
color isDark ? #fff : #000
|
||||||
|
background isDark ? #313543 : #f7f7f7
|
||||||
box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
|
box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
|
||||||
|
|
||||||
> a
|
> a
|
||||||
@@ -289,7 +290,7 @@ export default Vue.extend({
|
|||||||
line-height 48px
|
line-height 48px
|
||||||
|
|
||||||
&.trash
|
&.trash
|
||||||
border-left solid 1px #ddd
|
border-left solid 1px isDark ? #1c2023 : #ddd
|
||||||
|
|
||||||
> div
|
> div
|
||||||
width 100%
|
width 100%
|
||||||
@@ -329,7 +330,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
.mk-post-form
|
.mk-post-form
|
||||||
margin-bottom 16px
|
margin-bottom 16px
|
||||||
border solid 1px #e5e5e5
|
border solid 1px rgba(#000, 0.075)
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
|
|
||||||
> *:not(.main)
|
> *:not(.main)
|
||||||
@@ -357,4 +358,10 @@ export default Vue.extend({
|
|||||||
max-width 700px
|
max-width 700px
|
||||||
margin 0 auto
|
margin 0 auto
|
||||||
|
|
||||||
|
.mk-home[data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
.mk-home:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -4,15 +4,13 @@
|
|||||||
<x-sub :note="p.reply"/>
|
<x-sub :note="p.reply"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="renote" v-if="isRenote">
|
<div class="renote" v-if="isRenote">
|
||||||
<p>
|
<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
|
||||||
<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"/>
|
||||||
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
|
</router-link>
|
||||||
</router-link>
|
%fa:retweet%
|
||||||
%fa:retweet%
|
<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
|
||||||
<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>
|
||||||
<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>
|
||||||
<span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span>
|
|
||||||
</p>
|
|
||||||
<mk-time :time="note.createdAt"/>
|
<mk-time :time="note.createdAt"/>
|
||||||
</div>
|
</div>
|
||||||
<article>
|
<article>
|
||||||
@@ -324,36 +322,44 @@ root(isDark)
|
|||||||
border-radius 4px
|
border-radius 4px
|
||||||
|
|
||||||
> .renote
|
> .renote
|
||||||
|
display flex
|
||||||
|
align-items baseline
|
||||||
|
padding 16px 32px
|
||||||
|
line-height 28px
|
||||||
color #9dbb00
|
color #9dbb00
|
||||||
background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
|
background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
|
||||||
|
|
||||||
> p
|
.avatar-anchor
|
||||||
margin 0
|
display inline-block
|
||||||
padding 16px 32px
|
|
||||||
line-height 28px
|
|
||||||
|
|
||||||
.avatar-anchor
|
.avatar
|
||||||
display inline-block
|
vertical-align bottom
|
||||||
|
width 28px
|
||||||
|
height 28px
|
||||||
|
margin 0 8px 0 0
|
||||||
|
border-radius 6px
|
||||||
|
|
||||||
.avatar
|
[data-fa]
|
||||||
vertical-align bottom
|
margin-right 4px
|
||||||
width 28px
|
|
||||||
height 28px
|
|
||||||
margin 0 8px 0 0
|
|
||||||
border-radius 6px
|
|
||||||
|
|
||||||
[data-fa]
|
> span
|
||||||
margin-right 4px
|
flex-shrink 0
|
||||||
|
|
||||||
.name
|
&:last-of-type
|
||||||
font-weight bold
|
margin-right 8px
|
||||||
|
|
||||||
|
.name
|
||||||
|
overflow hidden
|
||||||
|
flex-shrink 1
|
||||||
|
text-overflow ellipsis
|
||||||
|
white-space nowrap
|
||||||
|
font-weight bold
|
||||||
|
|
||||||
> .mk-time
|
> .mk-time
|
||||||
position absolute
|
display block
|
||||||
top 16px
|
margin-left auto
|
||||||
right 32px
|
flex-shrink 0
|
||||||
font-size 0.9em
|
font-size 0.9em
|
||||||
line-height 28px
|
|
||||||
|
|
||||||
& + article
|
& + article
|
||||||
padding-top 8px
|
padding-top 8px
|
||||||
@@ -421,12 +427,14 @@ root(isDark)
|
|||||||
margin 0 .5em 0 0
|
margin 0 .5em 0 0
|
||||||
padding 1px 6px
|
padding 1px 6px
|
||||||
font-size 12px
|
font-size 12px
|
||||||
color #aaa
|
color isDark ? #758188 :#aaa
|
||||||
border solid 1px #ddd
|
border solid 1px isDark ? #57616f : #ddd
|
||||||
border-radius 3px
|
border-radius 3px
|
||||||
|
|
||||||
> .username
|
> .username
|
||||||
margin 0 .5em 0 0
|
margin 0 .5em 0 0
|
||||||
|
overflow hidden
|
||||||
|
text-overflow ellipsis
|
||||||
color isDark ? #606984 : #ccc
|
color isDark ? #606984 : #ccc
|
||||||
|
|
||||||
> .info
|
> .info
|
||||||
@@ -539,7 +547,7 @@ root(isDark)
|
|||||||
|
|
||||||
> .mk-note-preview
|
> .mk-note-preview
|
||||||
padding 16px
|
padding 16px
|
||||||
border dashed 1px #c0dac6
|
border dashed 1px isDark ? #4e945e : #c0dac6
|
||||||
border-radius 8px
|
border-radius 8px
|
||||||
|
|
||||||
> footer
|
> footer
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-notes">
|
<div class="mk-notes">
|
||||||
<template v-for="(note, i) in _notes">
|
<transition-group name="mk-notes" class="transition">
|
||||||
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
|
<template v-for="(note, i) in _notes">
|
||||||
<p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
|
||||||
<span>%fa:angle-up%{{ note._datetext }}</span>
|
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
||||||
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
|
<span>%fa:angle-up%{{ note._datetext }}</span>
|
||||||
</p>
|
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
|
||||||
</template>
|
</p>
|
||||||
|
</template>
|
||||||
|
</transition-group>
|
||||||
<footer>
|
<footer>
|
||||||
<slot name="footer"></slot>
|
<slot name="footer"></slot>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -51,21 +53,30 @@ export default Vue.extend({
|
|||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
root(isDark)
|
root(isDark)
|
||||||
> .date
|
.transition
|
||||||
display block
|
.mk-notes-enter
|
||||||
margin 0
|
.mk-notes-leave-to
|
||||||
line-height 32px
|
opacity 0
|
||||||
font-size 14px
|
transform translateY(-30px)
|
||||||
text-align center
|
|
||||||
color isDark ? #666b79 : #aaa
|
|
||||||
background isDark ? #242731 : #fdfdfd
|
|
||||||
border-bottom solid 1px isDark ? #1c2023 : #eaeaea
|
|
||||||
|
|
||||||
span
|
> *
|
||||||
margin 0 16px
|
transition transform .3s ease, opacity .3s ease
|
||||||
|
|
||||||
[data-fa]
|
> .date
|
||||||
margin-right 8px
|
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
|
> footer
|
||||||
> *
|
> *
|
||||||
|
@@ -302,7 +302,7 @@ root(isDark)
|
|||||||
min-width 100%
|
min-width 100%
|
||||||
min-height calc(16px + 12px + 12px)
|
min-height calc(16px + 12px + 12px)
|
||||||
font-size 16px
|
font-size 16px
|
||||||
color #333
|
color isDark ? #fff : #333
|
||||||
background isDark ? #191d23 : #fff
|
background isDark ? #191d23 : #fff
|
||||||
outline none
|
outline none
|
||||||
border solid 1px rgba($theme-color, 0.1)
|
border solid 1px rgba($theme-color, 0.1)
|
||||||
@@ -392,7 +392,7 @@ root(isDark)
|
|||||||
cursor pointer
|
cursor pointer
|
||||||
|
|
||||||
> .mk-poll-editor
|
> .mk-poll-editor
|
||||||
background lighten($theme-color, 98%)
|
background isDark ? #181b23 : lighten($theme-color, 98%)
|
||||||
border solid 1px rgba($theme-color, 0.1)
|
border solid 1px rgba($theme-color, 0.1)
|
||||||
border-top none
|
border-top none
|
||||||
border-radius 0 0 4px 4px
|
border-radius 0 0 4px 4px
|
||||||
|
@@ -45,6 +45,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<mk-switch v-model="os.i.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
|
<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.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="マップの自動展開">
|
<mk-switch v-model="os.i.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
|
||||||
<span>位置情報が添付された投稿のマップを自動的に展開します。</span>
|
<span>位置情報が添付された投稿のマップを自動的に展開します。</span>
|
||||||
</mk-switch>
|
</mk-switch>
|
||||||
@@ -319,6 +321,18 @@ export default Vue.extend({
|
|||||||
value: v
|
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) {
|
onChangeShowMaps(v) {
|
||||||
(this as any).api('i/update_client_setting', {
|
(this as any).api('i/update_client_setting', {
|
||||||
name: 'showMaps',
|
name: 'showMaps',
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-home-timeline">
|
<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"/>
|
<mk-friends-maker v-if="src == 'home' && alone"/>
|
||||||
<div class="fetching" v-if="fetching">
|
<div class="fetching" v-if="fetching">
|
||||||
<mk-ellipsis-icon/>
|
<mk-ellipsis-icon/>
|
||||||
@@ -20,6 +21,9 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { url } from '../../../config';
|
import { url } from '../../../config';
|
||||||
|
|
||||||
|
const fetchLimit = 10;
|
||||||
|
const displayLimit = 30;
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: {
|
props: {
|
||||||
src: {
|
src: {
|
||||||
@@ -34,6 +38,7 @@ export default Vue.extend({
|
|||||||
moreFetching: false,
|
moreFetching: false,
|
||||||
existMore: false,
|
existMore: false,
|
||||||
notes: [],
|
notes: [],
|
||||||
|
queue: [],
|
||||||
connection: null,
|
connection: null,
|
||||||
connectionId: null,
|
connectionId: null,
|
||||||
date: null
|
date: null
|
||||||
@@ -59,6 +64,10 @@ export default Vue.extend({
|
|||||||
: this.src == 'local'
|
: this.src == 'local'
|
||||||
? 'notes/local-timeline'
|
? 'notes/local-timeline'
|
||||||
: 'notes/global-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);
|
this.connection.on('unfollow', this.onChangeFollowing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', this.onKeydown);
|
||||||
|
window.addEventListener('scroll', this.onScroll);
|
||||||
|
|
||||||
this.fetch();
|
this.fetch();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -82,17 +94,27 @@ export default Vue.extend({
|
|||||||
this.connection.off('unfollow', this.onChangeFollowing);
|
this.connection.off('unfollow', this.onChangeFollowing);
|
||||||
}
|
}
|
||||||
this.stream.dispose(this.connectionId);
|
this.stream.dispose(this.connectionId);
|
||||||
|
|
||||||
|
document.removeEventListener('keydown', this.onKeydown);
|
||||||
|
window.removeEventListener('scroll', this.onScroll);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
isScrollTop() {
|
||||||
|
return window.scrollY <= 8;
|
||||||
|
},
|
||||||
|
|
||||||
fetch(cb?) {
|
fetch(cb?) {
|
||||||
|
this.queue = [];
|
||||||
this.fetching = true;
|
this.fetching = true;
|
||||||
|
|
||||||
(this as any).api(this.endpoint, {
|
(this as any).api(this.endpoint, {
|
||||||
limit: 11,
|
limit: fetchLimit + 1,
|
||||||
untilDate: this.date ? this.date.getTime() : undefined
|
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 => {
|
}).then(notes => {
|
||||||
if (notes.length == 11) {
|
if (notes.length == fetchLimit + 1) {
|
||||||
notes.pop();
|
notes.pop();
|
||||||
this.existMore = true;
|
this.existMore = true;
|
||||||
}
|
}
|
||||||
@@ -104,13 +126,17 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
more() {
|
more() {
|
||||||
if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return;
|
if (!this.canFetchMore) return;
|
||||||
|
|
||||||
this.moreFetching = true;
|
this.moreFetching = true;
|
||||||
|
|
||||||
(this as any).api(this.endpoint, {
|
(this as any).api(this.endpoint, {
|
||||||
limit: 11,
|
limit: fetchLimit + 1,
|
||||||
untilId: this.notes[this.notes.length - 1].id
|
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 => {
|
}).then(notes => {
|
||||||
if (notes.length == 11) {
|
if (notes.length == fetchLimit + 1) {
|
||||||
notes.pop();
|
notes.pop();
|
||||||
} else {
|
} else {
|
||||||
this.existMore = false;
|
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`);
|
const sound = new Audio(`${url}/assets/post.mp3`);
|
||||||
sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
|
sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
|
||||||
sound.play();
|
sound.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepent a note
|
||||||
this.notes.unshift(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() {
|
onChangeFollowing() {
|
||||||
@@ -145,13 +204,41 @@ export default Vue.extend({
|
|||||||
warp(date) {
|
warp(date) {
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.fetch();
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
.mk-home-timeline
|
.mk-home-timeline
|
||||||
|
> .newer-indicator
|
||||||
|
position -webkit-sticky
|
||||||
|
position sticky
|
||||||
|
z-index 100
|
||||||
|
height 3px
|
||||||
|
background $theme-color
|
||||||
|
|
||||||
> .mk-friends-maker
|
> .mk-friends-maker
|
||||||
border-bottom solid 1px #eee
|
border-bottom solid 1px #eee
|
||||||
|
|
||||||
|
@@ -27,35 +27,12 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
document.addEventListener('keydown', this.onKeydown);
|
|
||||||
window.addEventListener('scroll', this.onScroll);
|
|
||||||
|
|
||||||
(this.$refs.tl as any).$once('loaded', () => {
|
(this.$refs.tl as any).$once('loaded', () => {
|
||||||
this.$emit('loaded');
|
this.$emit('loaded');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
|
||||||
document.removeEventListener('keydown', this.onKeydown);
|
|
||||||
window.removeEventListener('scroll', this.onScroll);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
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) {
|
warp(date) {
|
||||||
(this.$refs.tl as any).warp(date);
|
(this.$refs.tl as any).warp(date);
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,9 @@
|
|||||||
<li @click="drive">
|
<li @click="drive">
|
||||||
<p>%fa:cloud%<span>%i18n:@drive%</span>%fa:angle-right%</p>
|
<p>%fa:cloud%<span>%i18n:@drive%</span>%fa:angle-right%</p>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<router-link to="/i/favorites">%fa:star%<span>%i18n:@favorites%</span>%fa:angle-right%</router-link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@@ -24,7 +27,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li @click="signout">
|
<li @click="signout">
|
||||||
<p>%fa:power-off%<span>%i18n:@signout%</span></p>
|
<p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -209,7 +212,7 @@ root(isDark)
|
|||||||
pointer-events none
|
pointer-events none
|
||||||
|
|
||||||
> span:first-child
|
> span:first-child
|
||||||
padding-left 16px
|
padding-left 22px
|
||||||
|
|
||||||
> [data-fa]:first-child
|
> [data-fa]:first-child
|
||||||
margin-right 6px
|
margin-right 6px
|
||||||
@@ -233,6 +236,16 @@ root(isDark)
|
|||||||
&:active
|
&:active
|
||||||
background darken($theme-color, 10%)
|
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-enter-active,
|
||||||
.zoom-in-top-leave-active {
|
.zoom-in-top-leave-active {
|
||||||
transform-origin: center -16px;
|
transform-origin: center -16px;
|
||||||
|
@@ -43,6 +43,8 @@ export default Vue.extend({
|
|||||||
XClock,
|
XClock,
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$store.commit('setUiHeaderHeight', 48);
|
||||||
|
|
||||||
if ((this as any).os.isSignedIn) {
|
if ((this as any).os.isSignedIn) {
|
||||||
const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000
|
const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000
|
||||||
const isHisasiburi = ago >= 3600;
|
const isHisasiburi = ago >= 3600;
|
||||||
|
@@ -141,7 +141,7 @@ root(isDark)
|
|||||||
> .description
|
> .description
|
||||||
padding 0 16px
|
padding 0 16px
|
||||||
font-size 0.7em
|
font-size 0.7em
|
||||||
color #555
|
color isDark ? #9ea4ad : #555
|
||||||
|
|
||||||
> .status
|
> .status
|
||||||
padding 8px 16px
|
padding 8px 16px
|
||||||
|
73
src/client/app/desktop/views/pages/favorites.vue
Normal file
73
src/client/app/desktop/views/pages/favorites.vue
Normal 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>
|
@@ -2,6 +2,7 @@
|
|||||||
<div class="mkw-messaging">
|
<div class="mkw-messaging">
|
||||||
<mk-widget-container :show-header="props.design == 0">
|
<mk-widget-container :show-header="props.design == 0">
|
||||||
<template slot="header">%fa:comments%%i18n:@title%</template>
|
<template slot="header">%fa:comments%%i18n:@title%</template>
|
||||||
|
<button slot="func" @click="add">%fa:plus%</button>
|
||||||
|
|
||||||
<mk-messaging ref="index" compact @navigate="navigate"/>
|
<mk-messaging ref="index" compact @navigate="navigate"/>
|
||||||
</mk-widget-container>
|
</mk-widget-container>
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import define from '../../../common/define-widget';
|
import define from '../../../common/define-widget';
|
||||||
import MkMessagingRoomWindow from '../components/messaging-room-window.vue';
|
import MkMessagingRoomWindow from '../components/messaging-room-window.vue';
|
||||||
|
import MkMessagingWindow from '../components/messaging-window.vue';
|
||||||
|
|
||||||
export default define({
|
export default define({
|
||||||
name: 'messaging',
|
name: 'messaging',
|
||||||
@@ -24,6 +26,9 @@ export default define({
|
|||||||
user: user
|
user: user
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
add() {
|
||||||
|
(this as any).os.new(MkMessagingWindow);
|
||||||
|
},
|
||||||
func() {
|
func() {
|
||||||
if (this.props.design == 1) {
|
if (this.props.design == 1) {
|
||||||
this.props.design = 0;
|
this.props.design = 0;
|
||||||
@@ -37,7 +42,7 @@ export default define({
|
|||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.mkw-messaging
|
.mkw-messaging
|
||||||
> .mk-messaging
|
.mk-messaging
|
||||||
max-height 250px
|
max-height 250px
|
||||||
overflow auto
|
overflow auto
|
||||||
|
|
||||||
|
@@ -67,7 +67,7 @@ root(isDark)
|
|||||||
> .poll
|
> .poll
|
||||||
padding 16px
|
padding 16px
|
||||||
font-size 12px
|
font-size 12px
|
||||||
color #555
|
color isDark ? #9ea4ad : #555
|
||||||
|
|
||||||
> p
|
> p
|
||||||
margin 0 0 8px 0
|
margin 0 0 8px 0
|
||||||
|
@@ -91,7 +91,7 @@ root(isDark)
|
|||||||
|
|
||||||
> .banner
|
> .banner
|
||||||
height 100px
|
height 100px
|
||||||
background-color #f5f5f5
|
background-color isDark ? #303e4a : #f5f5f5
|
||||||
background-size cover
|
background-size cover
|
||||||
background-position center
|
background-position center
|
||||||
cursor pointer
|
cursor pointer
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import Vuex from 'vuex';
|
||||||
import VueRouter from 'vue-router';
|
import VueRouter from 'vue-router';
|
||||||
import VModal from 'vue-js-modal';
|
import VModal from 'vue-js-modal';
|
||||||
import * as TreeView from 'vue-json-tree-view';
|
import * as TreeView from 'vue-json-tree-view';
|
||||||
@@ -23,6 +24,7 @@ switch (lang) {
|
|||||||
default: elementLocale = ElementLocaleEn; break;
|
default: elementLocale = ElementLocaleEn; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vue.use(Vuex);
|
||||||
Vue.use(VueRouter);
|
Vue.use(VueRouter);
|
||||||
Vue.use(VModal);
|
Vue.use(VModal);
|
||||||
Vue.use(TreeView);
|
Vue.use(TreeView);
|
||||||
@@ -39,6 +41,17 @@ require('./common/views/widgets');
|
|||||||
// Register global filters
|
// Register global filters
|
||||||
require('./common/views/filters');
|
require('./common/views/filters');
|
||||||
|
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
state: {
|
||||||
|
uiHeaderHeight: 0
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setUiHeaderHeight(state, height) {
|
||||||
|
state.uiHeaderHeight = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Vue.mixin({
|
Vue.mixin({
|
||||||
destroyed(this: any) {
|
destroyed(this: any) {
|
||||||
if (this.$el.parentNode) {
|
if (this.$el.parentNode) {
|
||||||
@@ -145,6 +158,7 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
|
|||||||
});
|
});
|
||||||
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
|
store,
|
||||||
router,
|
router,
|
||||||
created() {
|
created() {
|
||||||
this.$watch('os.i', i => {
|
this.$watch('os.i', i => {
|
||||||
|
@@ -4,15 +4,13 @@
|
|||||||
<x-sub :note="p.reply"/>
|
<x-sub :note="p.reply"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="renote" v-if="isRenote">
|
<div class="renote" v-if="isRenote">
|
||||||
<p>
|
<router-link class="avatar-anchor" :to="note.user | userPage">
|
||||||
<router-link class="avatar-anchor" :to="note.user | userPage">
|
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
|
||||||
<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
|
</router-link>
|
||||||
</router-link>
|
%fa:retweet%
|
||||||
%fa:retweet%
|
<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
|
||||||
<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
|
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
|
||||||
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
|
<span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span>
|
||||||
<span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span>
|
|
||||||
</p>
|
|
||||||
<mk-time :time="note.createdAt"/>
|
<mk-time :time="note.createdAt"/>
|
||||||
</div>
|
</div>
|
||||||
<article>
|
<article>
|
||||||
@@ -251,42 +249,47 @@ export default Vue.extend({
|
|||||||
font-size 16px
|
font-size 16px
|
||||||
|
|
||||||
> .renote
|
> .renote
|
||||||
|
display flex
|
||||||
|
align-items baseline
|
||||||
|
padding 8px 16px
|
||||||
|
line-height 28px
|
||||||
color #9dbb00
|
color #9dbb00
|
||||||
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
|
background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
|
||||||
|
|
||||||
> p
|
@media (min-width 500px)
|
||||||
margin 0
|
padding 16px
|
||||||
padding 8px 16px
|
|
||||||
line-height 28px
|
|
||||||
|
|
||||||
@media (min-width 500px)
|
.avatar-anchor
|
||||||
padding 16px
|
display inline-block
|
||||||
|
|
||||||
.avatar-anchor
|
.avatar
|
||||||
display inline-block
|
vertical-align bottom
|
||||||
|
width 28px
|
||||||
|
height 28px
|
||||||
|
margin 0 8px 0 0
|
||||||
|
border-radius 6px
|
||||||
|
|
||||||
.avatar
|
[data-fa]
|
||||||
vertical-align bottom
|
margin-right 4px
|
||||||
width 28px
|
|
||||||
height 28px
|
|
||||||
margin 0 8px 0 0
|
|
||||||
border-radius 6px
|
|
||||||
|
|
||||||
[data-fa]
|
> span
|
||||||
margin-right 4px
|
flex-shrink 0
|
||||||
|
|
||||||
.name
|
&:last-of-type
|
||||||
font-weight bold
|
margin-right 8px
|
||||||
|
|
||||||
|
.name
|
||||||
|
overflow hidden
|
||||||
|
flex-shrink 1
|
||||||
|
text-overflow ellipsis
|
||||||
|
white-space nowrap
|
||||||
|
font-weight bold
|
||||||
|
|
||||||
> .mk-time
|
> .mk-time
|
||||||
position absolute
|
display block
|
||||||
top 8px
|
margin-left auto
|
||||||
right 16px
|
flex-shrink 0
|
||||||
font-size 0.9em
|
font-size 0.9em
|
||||||
line-height 28px
|
|
||||||
|
|
||||||
@media (min-width 500px)
|
|
||||||
top 16px
|
|
||||||
|
|
||||||
& + article
|
& + article
|
||||||
padding-top 8px
|
padding-top 8px
|
||||||
@@ -368,6 +371,8 @@ export default Vue.extend({
|
|||||||
|
|
||||||
> .username
|
> .username
|
||||||
margin 0 0.5em 0 0
|
margin 0 0.5em 0 0
|
||||||
|
overflow hidden
|
||||||
|
text-overflow ellipsis
|
||||||
color #ccc
|
color #ccc
|
||||||
|
|
||||||
> .info
|
> .info
|
||||||
|
@@ -2,13 +2,15 @@
|
|||||||
<div class="mk-notes">
|
<div class="mk-notes">
|
||||||
<slot name="head"></slot>
|
<slot name="head"></slot>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<template v-for="(note, i) in _notes">
|
<transition-group name="mk-notes" class="transition">
|
||||||
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
|
<template v-for="(note, i) in _notes">
|
||||||
<p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
|
||||||
<span>%fa:angle-up%{{ note._datetext }}</span>
|
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
|
||||||
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
|
<span>%fa:angle-up%{{ note._datetext }}</span>
|
||||||
</p>
|
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
|
||||||
</template>
|
</p>
|
||||||
|
</template>
|
||||||
|
</transition-group>
|
||||||
<footer>
|
<footer>
|
||||||
<slot name="tail"></slot>
|
<slot name="tail"></slot>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -52,6 +54,31 @@ export default Vue.extend({
|
|||||||
border-radius 8px
|
border-radius 8px
|
||||||
box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
|
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
|
> .init
|
||||||
padding 64px 0
|
padding 64px 0
|
||||||
text-align center
|
text-align center
|
||||||
@@ -73,22 +100,6 @@ export default Vue.extend({
|
|||||||
font-size 3em
|
font-size 3em
|
||||||
color #ccc
|
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
|
> footer
|
||||||
text-align center
|
text-align center
|
||||||
border-top solid 1px #eaeaea
|
border-top solid 1px #eaeaea
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-timeline">
|
<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-friends-maker v-if="alone"/>
|
||||||
<mk-notes :notes="notes">
|
<mk-notes :notes="notes">
|
||||||
<div class="init" v-if="fetching">
|
<div class="init" v-if="fetching">
|
||||||
@@ -9,7 +10,7 @@
|
|||||||
%fa:R comments%
|
%fa:R comments%
|
||||||
%i18n:@empty%
|
%i18n:@empty%
|
||||||
</div>
|
</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:@load-more%</span>
|
||||||
<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
|
<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -20,7 +21,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
const limit = 10;
|
const fetchLimit = 10;
|
||||||
|
const displayLimit = 30;
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: {
|
props: {
|
||||||
@@ -30,21 +32,29 @@ export default Vue.extend({
|
|||||||
default: null
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
fetching: true,
|
fetching: true,
|
||||||
moreFetching: false,
|
moreFetching: false,
|
||||||
notes: [],
|
notes: [],
|
||||||
|
queue: [],
|
||||||
existMore: false,
|
existMore: false,
|
||||||
connection: null,
|
connection: null,
|
||||||
connectionId: null
|
connectionId: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
alone(): boolean {
|
alone(): boolean {
|
||||||
return (this as any).os.i.followingCount == 0;
|
return (this as any).os.i.followingCount == 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
canFetchMore(): boolean {
|
||||||
|
return !this.moreFetching && !this.fetching && this.notes.length > 0 && this.existMore;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.connection = (this as any).os.stream.getConnection();
|
this.connection = (this as any).os.stream.getConnection();
|
||||||
this.connectionId = (this as any).os.stream.use();
|
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('follow', this.onChangeFollowing);
|
||||||
this.connection.on('unfollow', this.onChangeFollowing);
|
this.connection.on('unfollow', this.onChangeFollowing);
|
||||||
|
|
||||||
this.fetch();
|
window.addEventListener('scroll', this.onScroll);
|
||||||
|
|
||||||
|
this.fetch();
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.connection.off('note', this.onNote);
|
this.connection.off('note', this.onNote);
|
||||||
this.connection.off('follow', this.onChangeFollowing);
|
this.connection.off('follow', this.onChangeFollowing);
|
||||||
this.connection.off('unfollow', this.onChangeFollowing);
|
this.connection.off('unfollow', this.onChangeFollowing);
|
||||||
(this as any).os.stream.dispose(this.connectionId);
|
(this as any).os.stream.dispose(this.connectionId);
|
||||||
|
|
||||||
|
window.removeEventListener('scroll', this.onScroll);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
isScrollTop() {
|
||||||
|
return window.scrollY <= 8;
|
||||||
|
},
|
||||||
|
|
||||||
fetch(cb?) {
|
fetch(cb?) {
|
||||||
|
this.queue = [];
|
||||||
this.fetching = true;
|
this.fetching = true;
|
||||||
(this as any).api('notes/timeline', {
|
(this as any).api('notes/timeline', {
|
||||||
limit: limit + 1,
|
limit: fetchLimit + 1,
|
||||||
untilDate: this.date ? (this.date as any).getTime() : undefined
|
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 => {
|
}).then(notes => {
|
||||||
if (notes.length == limit + 1) {
|
if (notes.length == fetchLimit + 1) {
|
||||||
notes.pop();
|
notes.pop();
|
||||||
this.existMore = true;
|
this.existMore = true;
|
||||||
}
|
}
|
||||||
@@ -78,13 +101,16 @@ this.fetch();
|
|||||||
if (cb) cb();
|
if (cb) cb();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
more() {
|
more() {
|
||||||
this.moreFetching = true;
|
this.moreFetching = true;
|
||||||
(this as any).api('notes/timeline', {
|
(this as any).api('notes/timeline', {
|
||||||
limit: limit + 1,
|
limit: fetchLimit + 1,
|
||||||
untilId: this.notes[this.notes.length - 1].id
|
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 => {
|
}).then(notes => {
|
||||||
if (notes.length == limit + 1) {
|
if (notes.length == fetchLimit + 1) {
|
||||||
notes.pop();
|
notes.pop();
|
||||||
this.existMore = true;
|
this.existMore = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -94,20 +120,71 @@ this.fetch();
|
|||||||
this.moreFetching = false;
|
this.moreFetching = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onNote(note) {
|
|
||||||
|
prependNote(note) {
|
||||||
|
// Prepent a note
|
||||||
this.notes.unshift(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() {
|
onChangeFollowing() {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
|
},
|
||||||
|
|
||||||
|
onScroll() {
|
||||||
|
if (this.isScrollTop()) {
|
||||||
|
this.releaseQueue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.mk-friends-maker
|
@import '~const.styl'
|
||||||
margin-bottom 8px
|
|
||||||
|
.mk-timeline
|
||||||
|
> .newer-indicator
|
||||||
|
position -webkit-sticky
|
||||||
|
position sticky
|
||||||
|
z-index 100
|
||||||
|
height 3px
|
||||||
|
background $theme-color
|
||||||
|
|
||||||
|
> .mk-friends-maker
|
||||||
|
margin-bottom 8px
|
||||||
</style>
|
</style>
|
||||||
|
@@ -32,6 +32,8 @@ export default Vue.extend({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$store.commit('setUiHeaderHeight', 48);
|
||||||
|
|
||||||
if ((this as any).os.isSignedIn) {
|
if ((this as any).os.isSignedIn) {
|
||||||
this.connection = (this as any).os.stream.getConnection();
|
this.connection = (this as any).os.stream.getConnection();
|
||||||
this.connectionId = (this as any).os.stream.use();
|
this.connectionId = (this as any).os.stream.use();
|
||||||
@@ -150,6 +152,9 @@ export default Vue.extend({
|
|||||||
width 100%
|
width 100%
|
||||||
box-shadow 0 1px 0 rgba(#000, 0.075)
|
box-shadow 0 1px 0 rgba(#000, 0.075)
|
||||||
|
|
||||||
|
&, *
|
||||||
|
user-select none
|
||||||
|
|
||||||
> .main
|
> .main
|
||||||
color rgba(#fff, 0.9)
|
color rgba(#fff, 0.9)
|
||||||
|
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
import * as mongo from 'mongodb';
|
import * as mongo from 'mongodb';
|
||||||
|
import deepcopy = require('deepcopy');
|
||||||
import db from '../db/mongodb';
|
import db from '../db/mongodb';
|
||||||
|
import { pack as packNote } from './note';
|
||||||
|
|
||||||
const Favorite = db.get<IFavorite>('favorites');
|
const Favorite = db.get<IFavorite>('favorites');
|
||||||
|
Favorite.createIndex(['userId', 'noteId'], { unique: true });
|
||||||
export default Favorite;
|
export default Favorite;
|
||||||
|
|
||||||
export type IFavorite = {
|
export type IFavorite = {
|
||||||
@@ -37,3 +40,35 @@ export async function deleteFavorite(favorite: string | mongo.ObjectID | IFavori
|
|||||||
_id: f._id
|
_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);
|
||||||
|
});
|
||||||
|
@@ -233,6 +233,12 @@ const endpoints: Endpoint[] = [
|
|||||||
kind: 'notification-read'
|
kind: 'notification-read'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'i/favorites',
|
||||||
|
withCredential: true,
|
||||||
|
kind: 'favorites-read'
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'othello/match',
|
name: 'othello/match',
|
||||||
withCredential: true
|
withCredential: true
|
||||||
|
@@ -2,43 +2,52 @@
|
|||||||
* Module dependencies
|
* Module dependencies
|
||||||
*/
|
*/
|
||||||
import $ from 'cafy';
|
import $ from 'cafy';
|
||||||
import Favorite from '../../../../models/favorite';
|
import Favorite, { pack } from '../../../../models/favorite';
|
||||||
import { pack } from '../../../../models/note';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get followers of a user
|
* Get favorited notes
|
||||||
*
|
|
||||||
* @param {any} params
|
|
||||||
* @param {any} user
|
|
||||||
* @return {Promise<any>}
|
|
||||||
*/
|
*/
|
||||||
module.exports = (params, user) => new Promise(async (res, rej) => {
|
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
// Get 'limit' parameter
|
// Get 'limit' parameter
|
||||||
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
|
||||||
if (limitErr) return rej('invalid limit param');
|
if (limitErr) return rej('invalid limit param');
|
||||||
|
|
||||||
// Get 'offset' parameter
|
// Get 'sinceId' parameter
|
||||||
const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
|
const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
|
||||||
if (offsetErr) return rej('invalid offset param');
|
if (sinceIdErr) return rej('invalid sinceId param');
|
||||||
|
|
||||||
// Get 'sort' parameter
|
// Get 'untilId' parameter
|
||||||
const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$;
|
const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
|
||||||
if (sortError) return rej('invalid sort param');
|
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
|
// Get favorites
|
||||||
const favorites = await Favorite
|
const favorites = await Favorite
|
||||||
.find({
|
.find(query, { limit, sort });
|
||||||
userId: user._id
|
|
||||||
}, {
|
|
||||||
limit: limit,
|
|
||||||
skip: offset,
|
|
||||||
sort: {
|
|
||||||
_id: sort == 'asc' ? 1 : -1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Serialize
|
// Serialize
|
||||||
res(await Promise.all(favorites.map(async favorite =>
|
res(await Promise.all(favorites.map(favorite => pack(favorite, user))));
|
||||||
await pack(favorite.noteId)
|
|
||||||
)));
|
|
||||||
});
|
});
|
||||||
|
@@ -37,6 +37,14 @@ module.exports = async (params, user, app) => {
|
|||||||
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
|
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([
|
const [followings, watchingChannelIds, mutedUserIds] = await Promise.all([
|
||||||
// フォローを取得
|
// フォローを取得
|
||||||
// Fetch following
|
// Fetch following
|
||||||
@@ -84,38 +92,76 @@ module.exports = async (params, user, app) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
$or: [{
|
$and: [{
|
||||||
$and: [{
|
$or: [{
|
||||||
// フォローしている人のタイムラインへの投稿
|
$and: [{
|
||||||
$or: followQuery
|
// フォローしている人のタイムラインへの投稿
|
||||||
}, {
|
$or: followQuery
|
||||||
// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
|
|
||||||
$or: [{
|
|
||||||
channelId: {
|
|
||||||
$exists: false
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
channelId: null
|
// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
|
||||||
|
$or: [{
|
||||||
|
channelId: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
channelId: null
|
||||||
|
}]
|
||||||
}]
|
}]
|
||||||
}]
|
}, {
|
||||||
}, {
|
// Watchしているチャンネルへの投稿
|
||||||
// Watchしているチャンネルへの投稿
|
channelId: {
|
||||||
channelId: {
|
$in: watchingChannelIds
|
||||||
$in: watchingChannelIds
|
}
|
||||||
}
|
}],
|
||||||
}],
|
// mute
|
||||||
// mute
|
userId: {
|
||||||
userId: {
|
$nin: mutedUserIds
|
||||||
$nin: mutedUserIds
|
},
|
||||||
},
|
'_reply.userId': {
|
||||||
'_reply.userId': {
|
$nin: mutedUserIds
|
||||||
$nin: mutedUserIds
|
},
|
||||||
},
|
'_renote.userId': {
|
||||||
'_renote.userId': {
|
$nin: mutedUserIds
|
||||||
$nin: mutedUserIds
|
},
|
||||||
},
|
}]
|
||||||
} as any;
|
} 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) {
|
if (sinceId) {
|
||||||
sort._id = 1;
|
sort._id = 1;
|
||||||
query._id = {
|
query._id = {
|
||||||
|
@@ -61,7 +61,7 @@ router.get('/manifest.json', async ctx => {
|
|||||||
router.use('/docs', docs.routes());
|
router.use('/docs', docs.routes());
|
||||||
|
|
||||||
// URL preview endpoint
|
// URL preview endpoint
|
||||||
router.get('url', require('./url-preview'));
|
router.get('/url', require('./url-preview'));
|
||||||
|
|
||||||
// Render base html for all requests
|
// Render base html for all requests
|
||||||
router.get('*', async ctx => {
|
router.get('*', async ctx => {
|
||||||
|
@@ -54,9 +54,9 @@ const handlers = {
|
|||||||
document.body.appendChild(blockquote);
|
document.body.appendChild(blockquote);
|
||||||
},
|
},
|
||||||
|
|
||||||
title({ document }, { title }) {
|
title({ document }, { content }) {
|
||||||
const h1 = document.createElement('h1');
|
const h1 = document.createElement('h1');
|
||||||
h1.textContent = title;
|
h1.textContent = content;
|
||||||
document.body.appendChild(h1);
|
document.body.appendChild(h1);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -75,6 +75,13 @@ const handlers = {
|
|||||||
a.href = url;
|
a.href = url;
|
||||||
a.textContent = url;
|
a.textContent = url;
|
||||||
document.body.appendChild(a);
|
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
13
src/text/parse/elements/search.ts
Normal file
13
src/text/parse/elements/search.ts
Normal 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]
|
||||||
|
};
|
||||||
|
};
|
Reference in New Issue
Block a user