Compare commits

...

25 Commits

Author SHA1 Message Date
syuilo
fd01b4d204 v5089 2018-04-22 17:34:56 +09:00
syuilo
3a767f29be Fix #1532 2018-04-22 17:34:25 +09:00
syuilo
ed0885ce5c #1533 2018-04-22 17:32:56 +09:00
syuilo
08b8d829f9 ダークモード情報をアカウントではなくブラウザに保存するように 2018-04-22 17:28:21 +09:00
syuilo
d2d3a7d52b CW 2018-04-22 17:04:52 +09:00
syuilo
4e032a9188 [wip] #1455 2018-04-22 10:53:27 +09:00
syuilo
20e77196f2 AP: 投票をレンダリング 2018-04-22 10:44:17 +09:00
syuilo
45dcfc8a00 Fix #1531 2018-04-22 10:14:25 +09:00
syuilo
7ee0cad010 Fix bug 2018-04-22 07:50:09 +09:00
syuilo
40849a5aa8 ✌️ 2018-04-22 07:34:33 +09:00
syuilo
132c30e557 🎨 2018-04-22 07:23:56 +09:00
syuilo
ee13d2382b Merge branch 'master' of https://github.com/syuilo/misskey 2018-04-22 07:21:54 +09:00
syuilo
3e1f7861a1 oops 2018-04-22 07:21:51 +09:00
syuilo
1aeeb1f073 Merge pull request #1530 from mei23/mei-ap3
Fix can't communicate with other Misskey
2018-04-22 06:22:42 +09:00
mei23
0bb59bd73b Fix Misskey同士でフォローできない 2018-04-22 00:41:07 +09:00
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
34 changed files with 771 additions and 480 deletions

View File

@@ -140,8 +140,8 @@ inquirer.prompt(form).then(as => {
pass: as['es_pass'] || null
},
recaptcha: {
siteKey: as['recaptcha_site'],
secretKey: as['recaptcha_secret']
site_key: as['recaptcha_site'],
secret_key: as['recaptcha_secret']
}
};

13
docs/manage.ja.md Normal file
View File

@@ -0,0 +1,13 @@
# 運営ガイド
## ジョブキューの状態を調べる
Misskeyのディレクトリで:
``` shell
node_modules/kue/bin/kue-dashboard -p 3050
```
ポート3050にアクセスするとUIが表示されます
## ユーザーを凍結する
``` shell
node cli/suspend (ユーザーID)
```

View File

@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "0.0.5064",
"version": "0.0.5089",
"codename": "nighthike",
"license": "MIT",
"description": "A miniblog-based SNS",

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);
@@ -63,11 +61,8 @@
}
// Dark/Light
const me = JSON.parse(localStorage.getItem('me') || null);
if (me && me.clientSettings) {
if ((app == 'desktop' && me.clientSettings.dark) || (app == 'mobile' && me.clientSettings.darkMobile)) {
document.documentElement.setAttribute('data-darkmode', 'true');
}
if (localStorage.getItem('darkmode') == 'true') {
document.documentElement.setAttribute('data-darkmode', 'true');
}
// Script version
@@ -80,11 +75,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 +120,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 +137,4 @@
// Force reload
location.reload(true);
}
}
})();

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

@@ -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 ? #191b1f : #eee
border-radius 4px
overflow hidden
&:hover
text-decoration none
border-color #ddd
border-color isDark ? #4f5561 : #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

@@ -31,7 +31,7 @@ export default Vue.extend({
const xp = mouseX / this.$el.offsetWidth * 100;
const yp = mouseY / this.$el.offsetHeight * 100;
this.$el.style.backgroundPosition = xp + '% ' + yp + '%';
this.$el.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")';
this.$el.style.backgroundImage = `url("${this.image.url}")`;
},
onMouseleave() {

View File

@@ -34,24 +34,30 @@
<p class="channel" v-if="p.channel">
<a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
</p>
<div class="text">
<a class="reply" v-if="p.reply">%fa:reply%</a>
<mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
<a class="rp" v-if="p.renote">RP:</a>
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
<span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
<a class="reply" v-if="p.reply">%fa:reply%</a>
<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
<a class="rp" v-if="p.renote">RP:</a>
</div>
<div class="media" v-if="p.media.length > 0">
<mk-media-list :media-list="p.media"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
</div>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
<div class="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/>
</div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
</div>
<div class="media" v-if="p.media.length > 0">
<mk-media-list :media-list="p.media"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
</div>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
<div class="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/>
</div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
</div>
<footer>
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
@@ -113,6 +119,7 @@ export default Vue.extend({
data() {
return {
showContent: false,
isDetailOpened: false,
connection: null,
connectionId: null
@@ -456,7 +463,7 @@ root(isDark)
> .body
> .text
> .cw
cursor default
display block
margin 0
@@ -465,90 +472,117 @@ root(isDark)
font-size 1.1em
color isDark ? #fff : #717171
>>> .title
display block
margin-bottom 4px
padding 4px
font-size 90%
text-align center
background isDark ? #2f3944 : #eef1f3
border-radius 4px
>>> .code
margin 8px 0
>>> .quote
margin 8px
padding 6px 12px
color isDark ? #6f808e : #aaa
border-left solid 3px isDark ? #637182 : #eee
> .reply
> .text
margin-right 8px
color isDark ? #99abbf : #717171
> .rp
margin-left 4px
font-style oblique
color #a0bf46
> .location
margin 4px 0
font-size 12px
color #ccc
> .map
width 100%
height 300px
&:empty
display none
> .tags
margin 4px 0 0 0
> *
> .toggle
display inline-block
margin 0 8px 0 0
padding 2px 8px 2px 16px
font-size 90%
color #8d969e
background #edf0f3
border-radius 4px
&:before
content ""
display block
position absolute
top 0
bottom 0
left 4px
width 8px
height 8px
margin auto 0
background #fff
border-radius 100%
padding 4px 8px
font-size 0.7em
color isDark ? #393f4f : #fff
background isDark ? #687390 : #b1b9c1
border-radius 2px
cursor pointer
user-select none
&:hover
text-decoration none
background #e2e7ec
background isDark ? #707b97 : #bbc4ce
.mk-url-preview
margin-top 8px
> .content
> .channel
margin 0
> .text
cursor default
display block
margin 0
padding 0
overflow-wrap break-word
font-size 1.1em
color isDark ? #fff : #717171
> .mk-poll
font-size 80%
>>> .title
display block
margin-bottom 4px
padding 4px
font-size 90%
text-align center
background isDark ? #2f3944 : #eef1f3
border-radius 4px
> .renote
margin 8px 0
>>> .code
margin 8px 0
> .mk-note-preview
padding 16px
border dashed 1px isDark ? #4e945e : #c0dac6
border-radius 8px
>>> .quote
margin 8px
padding 6px 12px
color isDark ? #6f808e : #aaa
border-left solid 3px isDark ? #637182 : #eee
> .reply
margin-right 8px
color isDark ? #99abbf : #717171
> .rp
margin-left 4px
font-style oblique
color #a0bf46
> .location
margin 4px 0
font-size 12px
color #ccc
> .map
width 100%
height 300px
&:empty
display none
> .tags
margin 4px 0 0 0
> *
display inline-block
margin 0 8px 0 0
padding 2px 8px 2px 16px
font-size 90%
color #8d969e
background #edf0f3
border-radius 4px
&:before
content ""
display block
position absolute
top 0
bottom 0
left 4px
width 8px
height 8px
margin auto 0
background #fff
border-radius 100%
&:hover
text-decoration none
background #e2e7ec
.mk-url-preview
margin-top 8px
> .channel
margin 0
> .mk-poll
font-size 80%
> .renote
margin 8px 0
> .mk-note-preview
padding 16px
border dashed 1px isDark ? #4e945e : #c0dac6
border-radius 8px
> footer
> button

View File

@@ -1,96 +1,98 @@
<template>
<div class="mk-notifications">
<div class="notifications" v-if="notifications.length != 0">
<template v-for="(notification, i) in _notifications">
<div class="notification" :class="notification.type" :key="notification.id">
<mk-time :time="notification.createdAt"/>
<template v-if="notification.type == 'reaction'">
<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
<div class="text">
<p>
<mk-reaction-icon :reaction="notification.reaction"/>
<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
</p>
<router-link class="note-ref" :to="notification.note | notePage">
%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
<transition-group name="mk-notifications" class="transition">
<template v-for="(notification, i) in _notifications">
<div class="notification" :class="notification.type" :key="notification.id">
<mk-time :time="notification.createdAt"/>
<template v-if="notification.type == 'reaction'">
<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
</div>
</template>
<template v-if="notification.type == 'renote'">
<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
<div class="text">
<p>%fa:retweet%
<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
</p>
<router-link class="note-ref" :to="notification.note | notePage">
%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
<div class="text">
<p>
<mk-reaction-icon :reaction="notification.reaction"/>
<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
</p>
<router-link class="note-ref" :to="notification.note | notePage">
%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
</router-link>
</div>
</template>
<template v-if="notification.type == 'renote'">
<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
</div>
</template>
<template v-if="notification.type == 'quote'">
<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
<div class="text">
<p>%fa:quote-left%
<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
</p>
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
</div>
</template>
<template v-if="notification.type == 'follow'">
<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
<div class="text">
<p>%fa:user-plus%
<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
</p>
</div>
</template>
<template v-if="notification.type == 'reply'">
<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
<div class="text">
<p>%fa:reply%
<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
</p>
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
</div>
</template>
<template v-if="notification.type == 'mention'">
<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
<div class="text">
<p>%fa:at%
<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
</p>
<a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a>
</div>
</template>
<template v-if="notification.type == 'poll_vote'">
<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
<div class="text">
<p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p>
<router-link class="note-ref" :to="notification.note | notePage">
%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
<div class="text">
<p>%fa:retweet%
<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
</p>
<router-link class="note-ref" :to="notification.note | notePage">
%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
</router-link>
</div>
</template>
<template v-if="notification.type == 'quote'">
<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
</div>
</template>
</div>
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
<span>%fa:angle-up%{{ notification._datetext }}</span>
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
<div class="text">
<p>%fa:quote-left%
<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
</p>
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
</div>
</template>
<template v-if="notification.type == 'follow'">
<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
<div class="text">
<p>%fa:user-plus%
<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
</p>
</div>
</template>
<template v-if="notification.type == 'reply'">
<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
<div class="text">
<p>%fa:reply%
<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
</p>
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
</div>
</template>
<template v-if="notification.type == 'mention'">
<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
<div class="text">
<p>%fa:at%
<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
</p>
<a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a>
</div>
</template>
<template v-if="notification.type == 'poll_vote'">
<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
</router-link>
<div class="text">
<p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p>
<router-link class="note-ref" :to="notification.note | notePage">
%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
</router-link>
</div>
</template>
</div>
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
<span>%fa:angle-up%{{ notification._datetext }}</span>
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
</transition-group>
</div>
<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:!common.loading%' : '%i18n:!@more%' }}
@@ -186,97 +188,107 @@ export default Vue.extend({
<style lang="stylus" scoped>
root(isDark)
.transition
.mk-notifications-enter
.mk-notifications-leave-to
opacity 0
transform translateY(-30px)
> *
transition transform .3s ease, opacity .3s ease
> .notifications
> .notification
margin 0
padding 16px
overflow-wrap break-word
font-size 0.9em
border-bottom solid 1px isDark ? #1c2023 : rgba(0, 0, 0, 0.05)
> *
> .notification
margin 0
padding 16px
overflow-wrap break-word
font-size 0.9em
border-bottom solid 1px isDark ? #1c2023 : rgba(0, 0, 0, 0.05)
&:last-child
border-bottom none
&:last-child
border-bottom none
> .mk-time
display inline
position absolute
top 16px
right 12px
vertical-align top
color isDark ? #606984 : rgba(0, 0, 0, 0.6)
font-size small
> .mk-time
display inline
position absolute
top 16px
right 12px
vertical-align top
color isDark ? #606984 : rgba(0, 0, 0, 0.6)
font-size small
&:after
content ""
display block
clear both
> .avatar-anchor
display block
float left
position -webkit-sticky
position sticky
top 16px
> img
&:after
content ""
display block
min-width 36px
min-height 36px
max-width 36px
max-height 36px
border-radius 6px
clear both
> .text
float right
width calc(100% - 36px)
padding-left 8px
> .avatar-anchor
display block
float left
position -webkit-sticky
position sticky
top 16px
p
margin 0
> img
display block
min-width 36px
min-height 36px
max-width 36px
max-height 36px
border-radius 6px
i, .mk-reaction-icon
margin-right 4px
> .text
float right
width calc(100% - 36px)
padding-left 8px
.note-preview
color isDark ? #c2cad4 : rgba(0, 0, 0, 0.7)
p
margin 0
.note-ref
color isDark ? #c2cad4 : rgba(0, 0, 0, 0.7)
i, .mk-reaction-icon
margin-right 4px
.note-preview
color isDark ? #c2cad4 : rgba(0, 0, 0, 0.7)
.note-ref
color isDark ? #c2cad4 : rgba(0, 0, 0, 0.7)
[data-fa]
font-size 1em
font-weight normal
font-style normal
display inline-block
margin-right 3px
&.renote, &.quote
.text p i
color #77B255
&.follow
.text p i
color #53c7ce
&.reply, &.mention
.text p i
color #555
> .date
display block
margin 0
line-height 32px
text-align center
font-size 0.8em
color isDark ? #666b79 : #aaa
background isDark ? #242731 : #fdfdfd
border-bottom solid 1px isDark ? #1c2023 : rgba(0, 0, 0, 0.05)
span
margin 0 16px
[data-fa]
font-size 1em
font-weight normal
font-style normal
display inline-block
margin-right 3px
&.renote, &.quote
.text p i
color #77B255
&.follow
.text p i
color #53c7ce
&.reply, &.mention
.text p i
color #555
> .date
display block
margin 0
line-height 32px
text-align center
font-size 0.8em
color isDark ? #666b79 : #aaa
background isDark ? #242731 : #fdfdfd
border-bottom solid 1px isDark ? #1c2023 : rgba(0, 0, 0, 0.05)
span
margin 0 16px
[data-fa]
margin-right 8px
margin-right 8px
> .more
display block

View File

@@ -6,6 +6,7 @@
@drop.stop="onDrop"
>
<div class="content">
<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
<textarea :class="{ with: (files.length != 0 || poll) }"
ref="text" v-model="text" :disabled="posting"
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
@@ -27,6 +28,7 @@
<button class="drive" title="%i18n:@attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button>
<button class="kao" title="%i18n:@insert-a-kao%" @click="kao">%fa:R smile%</button>
<button class="poll" title="%i18n:@create-poll%" @click="poll = true">%fa:chart-pie%</button>
<button class="poll" title="内容を隠す" @click="useCw = !useCw">%fa:eye-slash%</button>
<button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
<p class="text-count" :class="{ over: text.length > 1000 }">{{ '%i18n:!@text-remain%'.replace('{}', 1000 - text.length) }}</p>
<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
@@ -46,7 +48,9 @@ export default Vue.extend({
components: {
XDraggable
},
props: ['reply', 'renote'],
data() {
return {
posting: false,
@@ -54,11 +58,14 @@ export default Vue.extend({
files: [],
uploadings: [],
poll: false,
useCw: false,
cw: null,
geo: null,
autocomplete: null,
draghover: false
};
},
computed: {
draftId(): string {
return this.renote
@@ -67,6 +74,7 @@ export default Vue.extend({
? 'reply:' + this.reply.id
: 'note';
},
placeholder(): string {
return this.renote
? '%i18n:!@quote-placeholder%'
@@ -74,6 +82,7 @@ export default Vue.extend({
? '%i18n:!@reply-placeholder%'
: '%i18n:!@note-placeholder%';
},
submitText(): string {
return this.renote
? '%i18n:!@renote%'
@@ -81,21 +90,26 @@ export default Vue.extend({
? '%i18n:!@reply%'
: '%i18n:!@note%';
},
canPost(): boolean {
return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote);
}
},
watch: {
text() {
this.saveDraft();
},
poll() {
this.saveDraft();
},
files() {
this.saveDraft();
}
},
mounted() {
this.$nextTick(() => {
// 書きかけの投稿を復元
@@ -113,13 +127,16 @@ export default Vue.extend({
}
});
},
methods: {
focus() {
(this.$refs.text as any).focus();
},
chooseFile() {
(this.$refs.file as any).click();
},
chooseFileFromDrive() {
(this as any).apis.chooseDriveFile({
multiple: true
@@ -127,32 +144,40 @@ export default Vue.extend({
files.forEach(this.attachMedia);
});
},
attachMedia(driveFile) {
this.files.push(driveFile);
this.$emit('change-attached-media', this.files);
},
detachMedia(id) {
this.files = this.files.filter(x => x.id != id);
this.$emit('change-attached-media', this.files);
},
onChangeFile() {
Array.from((this.$refs.file as any).files).forEach(this.upload);
},
upload(file) {
(this.$refs.uploader as any).upload(file);
},
onChangeUploadings(uploads) {
this.$emit('change-uploadings', uploads);
},
clear() {
this.text = '';
this.files = [];
this.poll = false;
this.$emit('change-attached-media', this.files);
},
onKeydown(e) {
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
},
onPaste(e) {
Array.from(e.clipboardData.items).forEach((item: any) => {
if (item.kind == 'file') {
@@ -160,6 +185,7 @@ export default Vue.extend({
}
});
},
onDragover(e) {
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
@@ -169,12 +195,15 @@ export default Vue.extend({
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
}
},
onDragenter(e) {
this.draghover = true;
},
onDragleave(e) {
this.draghover = false;
},
onDrop(e): void {
this.draghover = false;
@@ -195,6 +224,7 @@ export default Vue.extend({
}
//#endregion
},
setGeo() {
if (navigator.geolocation == null) {
alert('お使いの端末は位置情報に対応していません');
@@ -210,10 +240,12 @@ export default Vue.extend({
enableHighAccuracy: true
});
},
removeGeo() {
this.geo = null;
this.$emit('geo-dettached');
},
post() {
this.posting = true;
@@ -223,6 +255,7 @@ export default Vue.extend({
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
cw: this.useCw ? this.cw || '' : undefined,
geo: this.geo ? {
coordinates: [this.geo.longitude, this.geo.latitude],
altitude: this.geo.altitude,
@@ -250,6 +283,7 @@ export default Vue.extend({
this.posting = false;
});
},
saveDraft() {
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
@@ -264,6 +298,7 @@ export default Vue.extend({
localStorage.setItem('drafts', JSON.stringify(data));
},
deleteDraft() {
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
@@ -271,6 +306,7 @@ export default Vue.extend({
localStorage.setItem('drafts', JSON.stringify(data));
},
kao() {
this.text += getKao();
}
@@ -293,47 +329,54 @@ root(isDark)
> .content
textarea
> input
> textarea
display block
padding 12px
margin 0
width 100%
max-width 100%
min-width 100%
min-height calc(16px + 12px + 12px)
padding 12px
font-size 16px
color isDark ? #fff : #333
background isDark ? #191d23 : #fff
outline none
border solid 1px rgba($theme-color, 0.1)
border-radius 4px
transition border-color .3s ease
transition border-color .2s ease
&:hover
border-color rgba($theme-color, 0.2)
transition border-color .1s ease
& + *
& + * + *
border-color rgba($theme-color, 0.2)
transition border-color .1s ease
&:focus
color $theme-color
border-color rgba($theme-color, 0.5)
transition border-color 0s ease
& + *
& + * + *
border-color rgba($theme-color, 0.5)
transition border-color 0s ease
&:disabled
opacity 0.5
&::-webkit-input-placeholder
color rgba($theme-color, 0.3)
> input
margin-bottom 8px
> textarea
margin 0
max-width 100%
min-width 100%
min-height 64px
&:hover
& + *
& + * + *
border-color rgba($theme-color, 0.2)
transition border-color .1s ease
&:focus
& + *
& + * + *
border-color rgba($theme-color, 0.5)
transition border-color 0s ease
&.with
border-bottom solid 1px rgba($theme-color, 0.1) !important
border-radius 4px 4px 0 0

View File

@@ -40,7 +40,7 @@
<button class="ui button" @click="customizeHome" style="margin-bottom: 16px">ホームをカスタマイズ</button>
</div>
<div class="div">
<mk-switch v-model="os.i.clientSettings.dark" @change="onChangeDark" text="ダークモード"/>
<mk-switch v-model="darkmode" text="ダークモード"/>
<mk-switch v-model="os.i.clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
</div>
<mk-switch v-model="os.i.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
@@ -234,6 +234,7 @@ export default Vue.extend({
version,
latestVersion: undefined,
checkingForUpdate: false,
darkmode: localStorage.getItem('darkmode') == 'true',
enableSounds: localStorage.getItem('enableSounds') == 'true',
autoPopout: localStorage.getItem('autoPopout') == 'true',
apiViaStream: localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true,
@@ -257,6 +258,9 @@ export default Vue.extend({
apiViaStream() {
localStorage.setItem('apiViaStream', this.apiViaStream ? 'true' : 'false');
},
darkmode() {
(this as any)._updateDarkmode_(this.darkmode);
},
enableSounds() {
localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false');
},

View File

@@ -88,10 +88,7 @@ export default Vue.extend({
(this as any).os.signout();
},
dark() {
(this as any).api('i/update_client_setting', {
name: 'dark',
value: !(this as any)._darkmode_
});
(this as any)._updateDarkmode_(!(this as any)._darkmode_);
}
}
});

View File

@@ -61,39 +61,44 @@ Vue.mixin({
});
// Dark/Light
const bus = new Vue();
Vue.mixin({
data() {
return {
_darkmode_: false
_darkmode_: localStorage.getItem('darkmode') == 'true'
};
},
beforeCreate() {
// なぜか警告が出るため
this._darkmode_ = false;
// なぜか警告が出るので
this._darkmode_ = localStorage.getItem('darkmode') == 'true';
},
beforeDestroy() {
bus.$off('updated', this._onDarkmodeUpdated_);
},
mounted() {
const set = () => {
if (!this.$el || !this.$el.setAttribute || !this.os || !this.os.i) return;
if (this.os.i.clientSettings.dark) {
this._onDarkmodeUpdated_(this._darkmode_);
bus.$on('updated', this._onDarkmodeUpdated_);
},
methods: {
_updateDarkmode_(v) {
localStorage.setItem('darkmode', v.toString());
bus.$emit('updated', v);
if (v) {
document.documentElement.setAttribute('data-darkmode', 'true');
this.$el.setAttribute('data-darkmode', 'true');
this._darkmode_ = true;
this.$forceUpdate();
} else {
document.documentElement.removeAttribute('data-darkmode');
this.$el.removeAttribute('data-darkmode');
this._darkmode_ = false;
this.$forceUpdate();
}
};
set();
this.$watch('os.i.clientSettings', i => {
set();
}, {
deep: true
});
},
_onDarkmodeUpdated_(v) {
if (!this.$el || !this.$el.setAttribute) return;
if (v) {
this.$el.setAttribute('data-darkmode', 'true');
} else {
this.$el.removeAttribute('data-darkmode');
}
this._darkmode_ = v;
this.$forceUpdate();
}
}
});

View File

@@ -31,27 +31,33 @@
</header>
<div class="body">
<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
<div class="text">
<a class="reply" v-if="p.reply">
%fa:reply%
</a>
<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
<a class="rp" v-if="p.renote != null">RP:</a>
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
<span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
<a class="reply" v-if="p.reply">
%fa:reply%
</a>
<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
<a class="rp" v-if="p.renote != null">RP:</a>
</div>
<div class="media" v-if="p.media.length > 0">
<mk-media-list :media-list="p.media"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
</div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
<div class="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/>
</div>
</div>
<div class="media" v-if="p.media.length > 0">
<mk-media-list :media-list="p.media"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
</div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
<div class="map" v-if="p.geo" ref="map"></div>
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
<div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/>
</div>
</div>
<footer>
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
@@ -92,6 +98,7 @@ export default Vue.extend({
data() {
return {
showContent: false,
connection: null,
connectionId: null
};
@@ -229,7 +236,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
.note
root(isDark)
font-size 12px
border-bottom solid 1px #eaeaea
@@ -388,113 +395,140 @@ export default Vue.extend({
> .body
> .text
> .cw
cursor default
display block
margin 0
padding 0
overflow-wrap break-word
font-size 1.1em
color #717171
color isDark ? #fff : #717171
>>> .title
display block
margin-bottom 4px
padding 4px
font-size 90%
text-align center
background #eef1f3
border-radius 4px
>>> .code
margin 8px 0
>>> .quote
margin 8px
padding 6px 12px
color #aaa
border-left solid 3px #eee
> .reply
> .text
margin-right 8px
> .toggle
display inline-block
padding 4px 8px
font-size 0.7em
color isDark ? #393f4f : #fff
background isDark ? #687390 : #b1b9c1
border-radius 2px
cursor pointer
user-select none
&:hover
background isDark ? #707b97 : #bbc4ce
> .content
> .text
display block
margin 0
padding 0
overflow-wrap break-word
font-size 1.1em
color #717171
> .rp
margin-left 4px
font-style oblique
color #a0bf46
[data-is-me]:after
content "you"
padding 0 4px
margin-left 4px
font-size 80%
color $theme-color-foreground
background $theme-color
border-radius 4px
.mk-url-preview
margin-top 8px
> .channel
margin 0
> .tags
margin 4px 0 0 0
> *
display inline-block
margin 0 8px 0 0
padding 2px 8px 2px 16px
font-size 90%
color #8d969e
background #edf0f3
border-radius 4px
&:before
content ""
>>> .title
display block
position absolute
top 0
bottom 0
left 4px
width 8px
height 8px
margin auto 0
background #fff
border-radius 100%
margin-bottom 4px
padding 4px
font-size 90%
text-align center
background #eef1f3
border-radius 4px
> .media
> img
display block
max-width 100%
>>> .code
margin 8px 0
> .location
margin 4px 0
font-size 12px
color #ccc
>>> .quote
margin 8px
padding 6px 12px
color #aaa
border-left solid 3px #eee
> .map
width 100%
height 200px
> .reply
margin-right 8px
color #717171
&:empty
display none
> .rp
margin-left 4px
font-style oblique
color #a0bf46
[data-is-me]:after
content "you"
padding 0 4px
margin-left 4px
font-size 80%
color $theme-color-foreground
background $theme-color
border-radius 4px
.mk-url-preview
margin-top 8px
> .channel
margin 0
> .tags
margin 4px 0 0 0
> *
display inline-block
margin 0 8px 0 0
padding 2px 8px 2px 16px
font-size 90%
color #8d969e
background #edf0f3
border-radius 4px
&:before
content ""
display block
position absolute
top 0
bottom 0
left 4px
width 8px
height 8px
margin auto 0
background #fff
border-radius 100%
> .media
> img
display block
max-width 100%
> .location
margin 4px 0
font-size 12px
color #ccc
> .map
width 100%
height 200px
&:empty
display none
> .mk-poll
font-size 80%
> .renote
margin 8px 0
> .mk-note-preview
padding 16px
border dashed 1px #c0dac6
border-radius 8px
> .app
font-size 12px
color #ccc
> .mk-poll
font-size 80%
> .renote
margin 8px 0
> .mk-note-preview
padding 16px
border dashed 1px #c0dac6
border-radius 8px
> footer
> button
margin 0
@@ -524,6 +558,12 @@ export default Vue.extend({
@media (max-width 350px)
display none
.note[data-darkmode]
root(true)
.note:not([data-darkmode])
root(false)
</style>
<style lang="stylus" module>

View File

@@ -10,6 +10,7 @@
</header>
<div class="form">
<mk-note-preview v-if="reply" :note="reply"/>
<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
<textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : '%i18n:!@note-placeholder%'"></textarea>
<div class="attaches" v-show="files.length != 0">
<x-draggable class="files" :list="files" :options="{ animation: 150 }">
@@ -24,6 +25,7 @@
<button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>
<button class="kao" @click="kao">%fa:R smile%</button>
<button class="poll" @click="poll = true">%fa:chart-pie%</button>
<button class="poll" @click="useCw = !useCw">%fa:eye-slash%</button>
<button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
</div>
@@ -39,7 +41,9 @@ export default Vue.extend({
components: {
XDraggable
},
props: ['reply'],
data() {
return {
posting: false,
@@ -47,21 +51,27 @@ export default Vue.extend({
uploadings: [],
files: [],
poll: false,
geo: null
geo: null,
useCw: false,
cw: null
};
},
mounted() {
this.$nextTick(() => {
this.focus();
});
},
methods: {
focus() {
(this.$refs.text as any).focus();
},
chooseFile() {
(this.$refs.file as any).click();
},
chooseFileFromDrive() {
(this as any).apis.chooseDriveFile({
multiple: true
@@ -69,23 +79,29 @@ export default Vue.extend({
files.forEach(this.attachMedia);
});
},
attachMedia(driveFile) {
this.files.push(driveFile);
this.$emit('change-attached-media', this.files);
},
detachMedia(file) {
this.files = this.files.filter(x => x.id != file.id);
this.$emit('change-attached-media', this.files);
},
onChangeFile() {
Array.from((this.$refs.file as any).files).forEach(this.upload);
},
upload(file) {
(this.$refs.uploader as any).upload(file);
},
onChangeUploadings(uploads) {
this.$emit('change-uploadings', uploads);
},
setGeo() {
if (navigator.geolocation == null) {
alert('お使いの端末は位置情報に対応していません');
@@ -100,15 +116,18 @@ export default Vue.extend({
enableHighAccuracy: true
});
},
removeGeo() {
this.geo = null;
},
clear() {
this.text = '';
this.files = [];
this.poll = false;
this.$emit('change-attached-media');
},
post() {
this.posting = true;
const viaMobile = (this as any).os.i.clientSettings.disableViaMobile !== true;
@@ -117,6 +136,7 @@ export default Vue.extend({
mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
cw: this.useCw ? this.cw || '' : undefined,
geo: this.geo ? {
coordinates: [this.geo.longitude, this.geo.latitude],
altitude: this.geo.altitude,
@@ -133,10 +153,12 @@ export default Vue.extend({
this.posting = false;
});
},
cancel() {
this.$emit('cancel');
this.$destroy();
},
kao() {
this.text += getKao();
}
@@ -236,14 +258,12 @@ export default Vue.extend({
> .file
display none
> input
> textarea
display block
padding 12px
margin 0
width 100%
max-width 100%
min-width 100%
min-height 80px
font-size 16px
color #333
border none
@@ -253,6 +273,11 @@ export default Vue.extend({
&:disabled
opacity 0.5
> textarea
max-width 100%
min-width 100%
min-height 80px
> .upload
> .drive
> .kao

View File

@@ -29,12 +29,6 @@ props:
desc:
ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)"
en: "The text of this note (in Markdown like format if local)"
- name: "textHtml"
type: "string"
optional: true
desc:
ja: "投稿の本文 (HTML) (投稿時は無視)"
en: "The text of this note (in HTML. Ignored when posting.)"
- name: "mediaIds"
type: "id(DriveFile)[]"
optional: true

View File

@@ -29,12 +29,6 @@ props:
desc:
ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)"
en: "The text of this note (in Markdown like format if local)"
- name: "textHtml"
type: "string"
optional: true
desc:
ja: "投稿の本文 (HTML) (投稿時は無視)"
en: "The text of this note (in HTML. Ignored when posting.)"
- name: "mediaIds"
type: "id(DriveFile)[]"
optional: true

View File

@@ -12,7 +12,6 @@ export interface IMessagingMessage {
_id: mongo.ObjectID;
createdAt: Date;
text: string;
textHtml: string;
userId: mongo.ObjectID;
recipientId: mongo.ObjectID;
isRead: boolean;

View File

@@ -24,7 +24,7 @@ export function isValidText(text: string): boolean {
}
export function isValidCw(text: string): boolean {
return text.length <= 100 && text.trim() != '';
return text.length <= 100;
}
export type INote = {
@@ -38,7 +38,6 @@ export type INote = {
poll: any; // todo
text: string;
tags: string[];
textHtml: string;
cw: string;
userId: mongo.ObjectID;
appId: mongo.ObjectID;

View File

@@ -14,6 +14,7 @@ const queue = createQueue({
export function createHttp(data) {
return queue
.create('http', data)
.removeOnComplete(true)
.events(false)
.attempts(8)
.backoff({ delay: 16384, type: 'exponential' });
@@ -21,11 +22,12 @@ export function createHttp(data) {
export function deliver(user, content, to) {
createHttp({
title: 'deliver',
type: 'deliver',
user,
content,
to
}).removeOnComplete(true).save();
}).save();
}
export default function() {

View File

@@ -33,6 +33,11 @@ export default async (job: kue.Job, done): Promise<void> => {
}
user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser;
// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
if (user === null) {
user = await resolvePerson(activity.actor);
}
} else {
user = await User.findOne({
host: { $ne: null },

View File

@@ -22,7 +22,6 @@ export default async function(actor: IRemoteUser, uri: string): Promise<void> {
$set: {
deletedAt: new Date(),
text: null,
textHtml: null,
mediaIds: [],
poll: null
}

View File

@@ -0,0 +1,18 @@
import { INote } from "../../../models/note";
import toHtml from '../../../text/html';
import parse from '../../../text/parse';
import config from '../../../config';
export default function(note: INote) {
if (note.text == null) return null;
let html = toHtml(parse(note.text));
if (note.poll != null) {
const url = `${config.url}/notes/${note._id}`;
// TODO: i18n
html += `<p>【投票】<br />${url}</p>`;
}
return html;
}

View File

@@ -4,6 +4,7 @@ import config from '../../../config';
import DriveFile from '../../../models/drive-file';
import Note, { INote } from '../../../models/note';
import User from '../../../models/user';
import toHtml from '../misc/get-note-html';
export default async function renderNote(note: INote, dive = true) {
const promisedFiles = note.mediaIds
@@ -48,7 +49,7 @@ export default async function renderNote(note: INote, dive = true) {
id: `${config.url}/notes/${note._id}`,
type: 'Note',
attributedTo,
content: note.textHtml,
content: toHtml(note),
published: note.createdAt.toISOString(),
to: 'https://www.w3.org/ns/activitystreams#Public',
cc: `${attributedTo}/followers`,

View File

@@ -40,5 +40,10 @@ export default (user: ILocalUser, url: string, object) => new Promise((resolve,
keyId: `acct:${user.username}@${config.host}`
});
// Signature: Signature ... => Signature: ...
let sig = req.getHeader('Signature').toString();
sig = sig.replace(/^Signature /, '');
req.setHeader('Signature', sig);
req.end(JSON.stringify(object));
});

View File

@@ -12,8 +12,6 @@ import { pack } from '../../../../../models/messaging-message';
import publishUserStream from '../../../../../publishers/stream';
import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../publishers/stream';
import pushSw from '../../../../../publishers/push-sw';
import html from '../../../../../text/html';
import parse from '../../../../../text/parse';
import config from '../../../../../config';
/**
@@ -77,7 +75,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
fileId: file ? file._id : undefined,
recipientId: recipient._id,
text: text ? text : undefined,
textHtml: text ? html(parse(text)) : undefined,
userId: user._id,
isRead: false
});

View File

@@ -23,11 +23,11 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
if (visibilityErr) return rej('invalid visibility');
// Get 'text' parameter
const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
const [text = null, textErr] = $(params.text).optional.nullable.string().pipe(isValidText).$;
if (textErr) return rej('invalid text');
// Get 'cw' parameter
const [cw, cwErr] = $(params.cw).optional.string().pipe(isValidCw).$;
const [cw, cwErr] = $(params.cw).optional.nullable.string().pipe(isValidCw).$;
if (cwErr) return rej('invalid cw');
// Get 'viaMobile' parameter
@@ -187,14 +187,14 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
const note = await create(user, {
createdAt: new Date(),
media: files,
poll: poll,
text: text,
poll,
text,
reply,
renote,
cw: cw,
tags: tags,
app: app,
viaMobile: viaMobile,
cw,
tags,
app,
viaMobile,
visibility,
geo
});

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

@@ -15,7 +15,6 @@ import Mute from '../../models/mute';
import pushSw from '../../publishers/push-sw';
import event from '../../publishers/stream';
import parse from '../../text/parse';
import html from '../../text/html';
import { IApp } from '../../models/app';
export default async (user: IUser, data: {
@@ -63,9 +62,8 @@ export default async (user: IUser, data: {
replyId: data.reply ? data.reply._id : null,
renoteId: data.renote ? data.renote._id : null,
text: data.text,
textHtml: tokens === null ? null : html(tokens),
poll: data.poll,
cw: data.cw,
cw: data.cw == null ? null : data.cw,
tags,
userId: user._id,
viaMobile: data.viaMobile,

View File

@@ -46,11 +46,13 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise
publishNoteStream(note._id, 'reacted');
// Notify
notify(note.userId, user._id, 'reaction', {
noteId: note._id,
reaction: reaction
});
// リアクションされたユーザーがローカルユーザーなら通知を作成
if (isLocalUser(note._user)) {
notify(note.userId, user._id, 'reaction', {
noteId: note._id,
reaction: reaction
});
}
pushSw(note.userId, 'reaction', {
user: await packUser(user, note.userId),

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]
};
};

View File

@@ -12,7 +12,8 @@ const elements = [
require('./elements/code'),
require('./elements/inline-code'),
require('./elements/quote'),
require('./elements/emoji')
require('./elements/emoji'),
require('./elements/search')
];
export default (source: string): any[] => {