Compare commits

..

17 Commits

Author SHA1 Message Date
c3c885de47 8.35.0 2018-09-13 02:52:29 +09:00
8a8c079b2f Improve following/followers view
Closes #2235
2018-09-13 02:08:17 +09:00
b4a30e2a25 Refactor: remove needless await 2018-09-13 02:07:03 +09:00
f19f92c538 Clean up: Remove unused import 2018-09-13 01:50:36 +09:00
e1a946ab45 Fix bug 2018-09-13 01:50:21 +09:00
aa1817737e Fix glitch 2018-09-13 01:06:18 +09:00
25ec5a24ab Add toLowerCase 2018-09-12 06:33:02 +09:00
2118fadc48 Add toUpperCase function (#2697) 2018-09-12 03:51:33 +09:00
ca2230f690 Refactor 2018-09-12 03:32:47 +09:00
0ed197d4d9 Use unique (#2695) 2018-09-12 03:02:14 +09:00
046976dffc Resolve #2691 2018-09-12 02:48:19 +09:00
bb8139196e Fix wrong indentation 2018-09-12 01:48:30 +09:00
1fea2cdcbe Fix bug 2018-09-11 20:57:25 +09:00
fe3dd25bc3 8.34.4 2018-09-10 21:40:14 +09:00
5b09209ef9 Fix bug 2018-09-10 21:36:43 +09:00
62db650e3c Revert "Fix bug"
This reverts commit f3ab8199a5.
2018-09-10 21:17:49 +09:00
b847886254 fix docs (#2678) 2018-09-10 18:32:51 +09:00
26 changed files with 306 additions and 220 deletions

View File

@ -10,7 +10,7 @@ Misskeyサーバーの構築にご関心をお寄せいただきありがとう
*1.* Misskeyユーザーの作成 *1.* Misskeyユーザーの作成
---------------------------------------------------------------- ----------------------------------------------------------------
Misskeyrootで実行しない方がよいため、代わりにユーザーを作成します。 Misskeyrootユーザーで実行しない方がよいため、代わりにユーザーを作成します。
Debianの例: Debianの例:
``` ```

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "8.34.3", "version": "8.35.0",
"clientVersion": "1.0.9572", "clientVersion": "1.0.9589",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,

View File

@ -1,15 +1,15 @@
<template> <template>
<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick"> <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
<span class="inner" :style="style"></span> <span class="inner" :style="icon"></span>
</span> </span>
<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick"> <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
<span class="inner" :style="style"></span> <span class="inner" :style="icon"></span>
</span> </span>
<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id"> <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
<span class="inner" :style="style"></span> <span class="inner" :style="icon"></span>
</router-link> </router-link>
<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview"> <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
<span class="inner" :style="style"></span> <span class="inner" :style="icon"></span>
</router-link> </router-link>
</template> </template>
@ -42,6 +42,11 @@ export default Vue.extend({
return this.user.isCat && this.$store.state.settings.circleIcons; return this.user.isCat && this.$store.state.settings.circleIcons;
}, },
style(): any { style(): any {
return {
borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
};
},
icon(): any {
return { return {
backgroundColor: this.lightmode backgroundColor: this.lightmode
? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})` ? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`

View File

@ -1,4 +1,4 @@
import Vue from 'vue'; import Vue, { VNode } from 'vue';
import * as emojilib from 'emojilib'; import * as emojilib from 'emojilib';
import { length } from 'stringz'; import { length } from 'stringz';
import parse from '../../../../../mfm/parse'; import parse from '../../../../../mfm/parse';
@ -6,10 +6,7 @@ import getAcct from '../../../../../misc/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'; import MkGoogle from './google.vue';
import { concat } from '../../../../../prelude/array';
const flatten = list => list.reduce(
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
);
export default Vue.component('misskey-flavored-markdown', { export default Vue.component('misskey-flavored-markdown', {
props: { props: {
@ -32,20 +29,20 @@ export default Vue.component('misskey-flavored-markdown', {
}, },
render(createElement) { render(createElement) {
let ast; let ast: any[];
if (this.ast == null) { if (this.ast == null) {
// Parse text to ast // Parse text to ast
ast = parse(this.text); ast = parse(this.text);
} else { } else {
ast = this.ast; ast = this.ast as any[];
} }
let bigCount = 0; let bigCount = 0;
let motionCount = 0; let motionCount = 0;
// Parse ast to DOM // Parse ast to DOM
const els = flatten(ast.map(token => { const els = concat(ast.map((token): VNode[] => {
switch (token.type) { switch (token.type) {
case 'text': { case 'text': {
const text = token.content.replace(/(\r\n|\n|\r)/g, '\n'); const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
@ -56,12 +53,12 @@ export default Vue.component('misskey-flavored-markdown', {
x[x.length - 1].pop(); x[x.length - 1].pop();
return x; return x;
} else { } else {
return createElement('span', text.replace(/\n/g, ' ')); return [createElement('span', text.replace(/\n/g, ' '))];
} }
} }
case 'bold': { case 'bold': {
return createElement('b', token.bold); return [createElement('b', token.bold)];
} }
case 'big': { case 'big': {
@ -95,23 +92,23 @@ export default Vue.component('misskey-flavored-markdown', {
} }
case 'url': { case 'url': {
return createElement(MkUrl, { return [createElement(MkUrl, {
props: { props: {
url: token.content, url: token.content,
target: '_blank' target: '_blank'
} }
}); })];
} }
case 'link': { case 'link': {
return createElement('a', { return [createElement('a', {
attrs: { attrs: {
class: 'link', class: 'link',
href: token.url, href: token.url,
target: '_blank', target: '_blank',
title: token.url title: token.url
} }
}, token.title); }, token.title)];
} }
case 'mention': { case 'mention': {
@ -129,16 +126,16 @@ export default Vue.component('misskey-flavored-markdown', {
} }
case 'hashtag': { case 'hashtag': {
return createElement('a', { return [createElement('a', {
attrs: { attrs: {
href: `${url}/tags/${encodeURIComponent(token.hashtag)}`, href: `${url}/tags/${encodeURIComponent(token.hashtag)}`,
target: '_blank' target: '_blank'
} }
}, token.content); }, token.content)];
} }
case 'code': { case 'code': {
return createElement('pre', { return [createElement('pre', {
class: 'code' class: 'code'
}, [ }, [
createElement('code', { createElement('code', {
@ -146,15 +143,15 @@ export default Vue.component('misskey-flavored-markdown', {
innerHTML: token.html innerHTML: token.html
} }
}) })
]); ])];
} }
case 'inline-code': { case 'inline-code': {
return createElement('code', { return [createElement('code', {
domProps: { domProps: {
innerHTML: token.html innerHTML: token.html
} }
}); })];
} }
case 'quote': { case 'quote': {
@ -164,43 +161,45 @@ export default Vue.component('misskey-flavored-markdown', {
const x = text2.split('\n') const x = text2.split('\n')
.map(t => [createElement('span', t), createElement('br')]); .map(t => [createElement('span', t), createElement('br')]);
x[x.length - 1].pop(); x[x.length - 1].pop();
return createElement('div', { return [createElement('div', {
attrs: { attrs: {
class: 'quote' class: 'quote'
} }
}, x); }, x)];
} else { } else {
return createElement('span', { return [createElement('span', {
attrs: { attrs: {
class: 'quote' class: 'quote'
} }
}, text2.replace(/\n/g, ' ')); }, text2.replace(/\n/g, ' '))];
} }
} }
case 'title': { case 'title': {
return createElement('div', { return [createElement('div', {
attrs: { attrs: {
class: 'title' class: 'title'
} }
}, token.title); }, token.title)];
} }
case 'emoji': { case 'emoji': {
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': { case 'search': {
return createElement(MkGoogle, { return [createElement(MkGoogle, {
props: { props: {
q: token.query q: token.query
} }
}); })];
} }
default: { default: {
console.log('unknown ast type:', token.type); console.log('unknown ast type:', token.type);
return [];
} }
} }
})); }));

View File

@ -32,7 +32,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import parseAcct from '../../../../../misc/acct/parse'; import parseAcct from '../../../../../misc/acct/parse';
import getUserName from '../../../../../misc/get-user-name';
import Progress from '../../../common/scripts/loading'; import Progress from '../../../common/scripts/loading';
export default Vue.extend({ export default Vue.extend({

View File

@ -62,7 +62,7 @@ import getFace from '../../../common/scripts/get-face';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import parse from '../../../../../mfm/parse'; import parse from '../../../../../mfm/parse';
import { host } from '../../../config'; import { host } from '../../../config';
import { erase } from '../../../../../prelude/array'; import { erase, unique } from '../../../../../prelude/array';
import { length } from 'stringz'; import { length } from 'stringz';
import parseAcct from '../../../../../misc/acct/parse'; import parseAcct from '../../../../../misc/acct/parse';
@ -397,7 +397,7 @@ export default Vue.extend({
if (this.text && this.text != '') { if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag); const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], []))); localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
} }
}, },

View File

@ -2,8 +2,8 @@
<div class="mk-timeline"> <div class="mk-timeline">
<header> <header>
<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
<span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span> <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span> <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> <span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
<button @click="chooseList" title="%i18n:@list%">%fa:list%</button> <button @click="chooseList" title="%i18n:@list%">%fa:list%</button>
@ -29,7 +29,8 @@ export default Vue.extend({
data() { data() {
return { return {
src: 'home', src: 'home',
list: null list: null,
enableLocalTimeline: false
}; };
}, },
@ -44,6 +45,10 @@ export default Vue.extend({
}, },
created() { created() {
(this as any).os.getMeta().then(meta => {
this.enableLocalTimeline = !meta.disableLocalTimeline;
});
if (this.$store.state.device.tl) { if (this.$store.state.device.tl) {
this.src = this.$store.state.device.tl.src; this.src = this.$store.state.device.tl.src;
if (this.src == 'list') { if (this.src == 'list') {

View File

@ -1,17 +1,16 @@
<template> <template>
<div class="root item"> <div class="zvdbznxvfixtmujpsigoccczftvpiwqh">
<mk-avatar class="avatar" :user="user"/> <div class="banner" :style="bannerStyle"></div>
<div class="main"> <mk-avatar class="avatar" :user="user" :disable-preview="true"/>
<header> <div class="body">
<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link> <router-link :to="user | userPage" class="name">{{ user | userName }}</router-link>
<span class="username">@{{ user | acct }}</span> <span class="username">@{{ user | acct }}</span>
</header> <div class="description">
<div class="body"> <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
<p class="followed" v-if="user.isFollowed">%i18n:@followed%</p>
<div class="description">{{ user.description }}</div>
</div> </div>
<p class="followed" v-if="user.isFollowed">%i18n:@followed%</p>
<mk-follow-button :user="user" :size="'big'"/>
</div> </div>
<mk-follow-button :user="user"/>
</div> </div>
</template> </template>
@ -19,76 +18,69 @@
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
props: ['user'] props: ['user'],
computed: {
bannerStyle(): any {
if (this.user.bannerUrl == null) return {};
return {
backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundImage: `url(${ this.user.bannerUrl })`
};
}
},
}); });
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.root.item .zvdbznxvfixtmujpsigoccczftvpiwqh
padding 16px $bg = #fff
font-size 16px
&:after margin 16px auto
content "" max-width calc(100% - 32px)
display block font-size 16px
clear both text-align center
background $bg
box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
> .banner
height 100px
background-color #f9f4f4
background-position center
background-size cover
> .avatar > .avatar
display block display block
float left margin -40px auto 0 auto
margin 0 16px 0 0 width 80px
width 58px height 80px
height 58px border-radius 100%
border-radius 8px border solid 4px $bg
> .main > .body
float left padding 4px 32px 32px 32px
width calc(100% - 74px)
> header @media (max-width 400px)
margin-bottom 2px padding 4px 16px 16px 16px
> .name > .name
display inline font-size 20px
margin 0 font-weight bold
padding 0
color #777
font-size 1em
font-weight 700
text-align left
text-decoration none
&:hover > .username
text-decoration underline display block
opacity 0.7
> .username > .description
text-align left margin 16px 0
margin 0 0 0 8px
color #ccc
> .body > .followed
> .followed margin 0 0 16px 0
display inline-block padding 0
margin 0 0 4px 0 line-height 24px
padding 2px 8px font-size 0.8em
vertical-align top color #71afc7
font-size 10px background #eefaff
color #71afc7 border-radius 4px
background #eefaff
border-radius 4px
> .description
cursor default
display block
margin 0
padding 0
overflow-wrap break-word
font-size 1.1em
color #717171
> .mk-follow-button
position absolute
top 16px
right 16px
</style> </style>

View File

@ -33,7 +33,7 @@ export default Vue.extend({
props: ['fetch', 'count', 'youKnowCount'], props: ['fetch', 'count', 'youKnowCount'],
data() { data() {
return { return {
limit: 30, limit: 20,
mode: 'all', mode: 'all',
fetching: true, fetching: true,
moreFetching: false, moreFetching: false,
@ -73,10 +73,14 @@ export default Vue.extend({
.mk-users-list .mk-users-list
height 100% height 100%
background #fff overflow auto
background #eee
> nav > nav
z-index 1 z-index 10
position sticky
top 0
background #fff
box-shadow 0 1px 0 rgba(#000, 0.1) box-shadow 0 1px 0 rgba(#000, 0.1)
> div > div
@ -114,16 +118,14 @@ export default Vue.extend({
background #eee background #eee
border-radius 20px border-radius 20px
> .users > button
height calc(100% - 54px) display block
overflow auto width calc(100% - 32px)
margin 16px
padding 16px
> * &:hover
border-bottom solid 1px rgba(#000, 0.05) background rgba(#000, 0.1)
> *
max-width 600px
margin 0 auto
> .no > .no
margin 0 margin 0

View File

@ -1,22 +1,34 @@
<template> <template>
<div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card"> <div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card">
<header>%i18n:@dashboard%</header> <header>%i18n:@dashboard%</header>
<div v-if="stats" class="stats"> <div v-if="stats" class="stats">
<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> <div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> <div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> <div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> <div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
</div> </div>
<div class="cpu-memory"> <div class="cpu-memory">
<x-cpu-memory :connection="connection"/> <x-cpu-memory :connection="connection"/>
</div> </div>
<div>
<label> <div class="form">
<input type="checkbox" v-model="disableRegistration" @change="updateMeta"> <div>
<span>disableRegistration</span> <label>
</label> <input type="checkbox" v-model="disableRegistration" @change="updateMeta">
<button class="ui" @click="invite">%i18n:@invite%</button> <span>%i18n:@disableRegistration%</span>
<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> </label>
<button class="ui" @click="invite">%i18n:@invite%</button>
<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
</div>
<div>
<label>
<input type="checkbox" v-model="disableLocalTimeline" @change="updateMeta">
<span>%i18n:@disableLocalTimeline%</span>
</label>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -33,6 +45,7 @@ export default Vue.extend({
return { return {
stats: null, stats: null,
disableRegistration: false, disableRegistration: false,
disableLocalTimeline: false,
inviteCode: null, inviteCode: null,
connection: null, connection: null,
connectionId: null connectionId: null
@ -44,6 +57,7 @@ export default Vue.extend({
(this as any).os.getMeta().then(meta => { (this as any).os.getMeta().then(meta => {
this.disableRegistration = meta.disableRegistration; this.disableRegistration = meta.disableRegistration;
this.disableLocalTimeline = meta.disableLocalTimeline;
}); });
(this as any).api('stats').then(stats => { (this as any).api('stats').then(stats => {
@ -61,7 +75,8 @@ export default Vue.extend({
}, },
updateMeta() { updateMeta() {
(this as any).api('admin/update-meta', { (this as any).api('admin/update-meta', {
disableRegistration: this.disableRegistration disableRegistration: this.disableRegistration,
disableLocalTimeline: this.disableLocalTimeline
}); });
} }
} }
@ -97,4 +112,8 @@ export default Vue.extend({
border solid 1px #eee border solid 1px #eee
border-radius: 8px border-radius: 8px
> .form
> div
border-bottom solid 1px #eee
</style> </style>

View File

@ -308,7 +308,7 @@ export default Vue.extend({
if (this.text && this.text != '') { if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag); const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], []))); localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
} }
}, },

View File

@ -24,8 +24,8 @@
<div class="body"> <div class="body">
<div> <div>
<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
<span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span> <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span> <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
<template v-if="lists"> <template v-if="lists">
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span> <span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
@ -60,7 +60,8 @@ export default Vue.extend({
src: 'home', src: 'home',
list: null, list: null,
lists: null, lists: null,
showNav: false showNav: false,
enableLocalTimeline: false
}; };
}, },
@ -85,6 +86,10 @@ export default Vue.extend({
}, },
created() { created() {
(this as any).os.getMeta().then(meta => {
this.enableLocalTimeline = !meta.disableLocalTimeline;
});
if (this.$store.state.device.tl) { if (this.$store.state.device.tl) {
this.src = this.$store.state.device.tl.src; this.src = this.$store.state.device.tl.src;
if (this.src == 'list') { if (this.src == 'list') {

View File

@ -1,4 +1,4 @@
import { capitalize } from "../../../prelude/string"; import { capitalize, toUpperCase } from "../../../prelude/string";
function escape(text: string) { function escape(text: string) {
return text return text
@ -92,7 +92,7 @@ const _keywords = [
const keywords = _keywords const keywords = _keywords
.concat(_keywords.map(capitalize)) .concat(_keywords.map(capitalize))
.concat(_keywords.map(k => k.toUpperCase())) .concat(_keywords.map(toUpperCase))
.sort((a, b) => b.length - a.length); .sort((a, b) => b.length - a.length);
const symbols = [ const symbols = [

View File

@ -12,5 +12,6 @@ export type IMeta = {
originalUsersCount: number; originalUsersCount: number;
}; };
disableRegistration?: boolean; disableRegistration?: boolean;
disableLocalTimeline?: boolean;
hidedTags?: string[]; hidedTags?: string[];
}; };

View File

@ -1,3 +1,11 @@
export function capitalize(s: string): string { export function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
}
export function toUpperCase(s: string): string {
return s.toUpperCase();
}
export function toLowerCase(s: string): string {
return s.toLowerCase();
} }

View File

@ -23,6 +23,12 @@ export const meta = {
} }
}), }),
disableLocalTimeline: $.bool.optional.nullable.note({
desc: {
'ja-JP': 'ローカルタイムライン(とソーシャルタイムライン)を無効にするか否か'
}
}),
hidedTags: $.arr($.str).optional.nullable.note({ hidedTags: $.arr($.str).optional.nullable.note({
desc: { desc: {
'ja-JP': '統計などで無視するハッシュタグ' 'ja-JP': '統計などで無視するハッシュタグ'
@ -45,6 +51,10 @@ export default (params: any) => new Promise(async (res, rej) => {
set.disableRegistration = ps.disableRegistration; set.disableRegistration = ps.disableRegistration;
} }
if (typeof ps.disableLocalTimeline === 'boolean') {
set.disableLocalTimeline = ps.disableLocalTimeline;
}
if (Array.isArray(ps.hidedTags)) { if (Array.isArray(ps.hidedTags)) {
set.hidedTags = ps.hidedTags; set.hidedTags = ps.hidedTags;
} }

View File

@ -57,7 +57,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
} }
// Create following // Create following
create(follower, followee); await create(follower, followee);
// Send response // Send response
res(await pack(followee._id, user)); res(await pack(followee._id, user));

View File

@ -57,7 +57,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
} }
// Delete following // Delete following
deleteFollowing(follower, followee); await deleteFollowing(follower, followee);
// Send response // Send response
res(await pack(followee._id, user)); res(await pack(followee._id, user));

View File

@ -34,6 +34,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}, },
broadcasts: meta.broadcasts, broadcasts: meta.broadcasts,
disableRegistration: meta.disableRegistration, disableRegistration: meta.disableRegistration,
disableLocalTimeline: meta.disableLocalTimeline,
driveCapacityPerLocalUserMb: config.localDriveCapacityMb, driveCapacityPerLocalUserMb: config.localDriveCapacityMb,
recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null, recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null,
swPublickey: config.sw ? config.sw.public_key : null, swPublickey: config.sw ? config.sw.public_key : null,

View File

@ -73,8 +73,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
} }
// Serialize // Serialize
const users = await Promise.all(following.map(async f => const users = await Promise.all(following.map(f => pack(f.followerId, me, { detail: true })));
await pack(f.followerId, me, { detail: true })));
// Response // Response
res({ res({

View File

@ -73,8 +73,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
} }
// Serialize // Serialize
const users = await Promise.all(following.map(async f => const users = await Promise.all(following.map(f => pack(f.followeeId, me, { detail: true })));
await pack(f.followeeId, me, { detail: true })));
// Response // Response
res({ res({

View File

@ -36,6 +36,13 @@ export default async function(
// Subscribe Home stream channel // Subscribe Home stream channel
subscriber.on(`user-stream:${user._id}`, async x => { subscriber.on(`user-stream:${user._id}`, async x => {
// Renoteなら再pack
if (x.type == 'note' && x.body.renoteId != null) {
x.body.renote = await pack(x.body.renoteId, user, {
detail: true
});
}
//#region 流れてきたメッセージがミュートしているユーザーが関わるものだったら無視する //#region 流れてきたメッセージがミュートしているユーザーが関わるものだったら無視する
if (x.type == 'note') { if (x.type == 'note') {
if (mutedUserIds.includes(x.body.userId)) { if (mutedUserIds.includes(x.body.userId)) {
@ -54,13 +61,6 @@ export default async function(
} }
//#endregion //#endregion
// Renoteなら再pack
if (x.type == 'note' && x.body.renoteId != null) {
x.body.renote = await pack(x.body.renoteId, user, {
detail: true
});
}
connection.send(JSON.stringify(x)); connection.send(JSON.stringify(x));
}); });

View File

@ -19,6 +19,13 @@ export default async function(
subscriber.on(`hybrid-timeline:${user._id}`, onEvent); subscriber.on(`hybrid-timeline:${user._id}`, onEvent);
async function onEvent(note: any) { async function onEvent(note: any) {
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, user, {
detail: true
});
}
//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する //#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (mutedUserIds.indexOf(note.userId) != -1) { if (mutedUserIds.indexOf(note.userId) != -1) {
return; return;
@ -31,13 +38,6 @@ export default async function(
} }
//#endregion //#endregion
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, user, {
detail: true
});
}
connection.send(JSON.stringify({ connection.send(JSON.stringify({
type: 'note', type: 'note',
body: note body: note

View File

@ -16,6 +16,13 @@ export default async function(
// Subscribe stream // Subscribe stream
subscriber.on('local-timeline', async note => { subscriber.on('local-timeline', async note => {
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, user, {
detail: true
});
}
//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する //#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (mutedUserIds.indexOf(note.userId) != -1) { if (mutedUserIds.indexOf(note.userId) != -1) {
return; return;
@ -28,13 +35,6 @@ export default async function(
} }
//#endregion //#endregion
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, user, {
detail: true
});
}
connection.send(JSON.stringify({ connection.send(JSON.stringify({
type: 'note', type: 'note',
body: note body: note

View File

@ -36,8 +36,11 @@ async function save(path: string, name: string, type: string, hash: string, size
if (config.drive && config.drive.storage == 'minio') { if (config.drive && config.drive.storage == 'minio') {
const minio = new Minio.Client(config.drive.config); const minio = new Minio.Client(config.drive.config);
const key = `${config.drive.prefix}/${uuid.v4()}/${encodeURIComponent(name)}`;
const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}/${encodeURIComponent(name)}.thumbnail.jpg`; const keyDir = `${config.drive.prefix}/${uuid.v4()}`;
const key = `${keyDir}/${name}`;
const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`;
const thumbnailKey = `${thumbnailKeyDir}/${name}.thumbnail.jpg`;
const baseUrl = config.drive.baseUrl const baseUrl = config.drive.baseUrl
|| `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`;
@ -61,8 +64,8 @@ async function save(path: string, name: string, type: string, hash: string, size
key: key, key: key,
thumbnailKey: thumbnailKey thumbnailKey: thumbnailKey
}, },
url: `${ baseUrl }/${ key }`, url: `${ baseUrl }/${ keyDir }/${ encodeURIComponent(name) }`,
thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailKey }` : null thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailKeyDir }/${ encodeURIComponent(name) }.thumbnail.jpg` : null
}); });
const file = await DriveFile.insert({ const file = await DriveFile.insert({

View File

@ -1,58 +1,97 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import Xev from 'xev'; import Xev from 'xev';
import Meta, { IMeta } from './models/meta';
const ev = new Xev();
type ID = string | mongo.ObjectID; type ID = string | mongo.ObjectID;
function publish(channel: string, type: string, value?: any): void { class Publisher {
const message = type == null ? value : value == null ? private ev: Xev;
{ type: type } : private meta: IMeta;
{ type: type, body: value };
ev.emit(channel, message); constructor() {
this.ev = new Xev();
setInterval(async () => {
this.meta = await Meta.findOne({});
}, 5000);
}
public getMeta = async () => {
if (this.meta != null) return this.meta;
this.meta = await Meta.findOne({});
return this.meta;
}
private publish = (channel: string, type: string, value?: any): void => {
const message = type == null ? value : value == null ?
{ type: type } :
{ type: type, body: value };
this.ev.emit(channel, message);
}
public publishUserStream = (userId: ID, type: string, value?: any): void => {
this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
public publishDriveStream = (userId: ID, type: string, value?: any): void => {
this.publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
public publishNoteStream = (noteId: ID, type: string): void => {
this.publish(`note-stream:${noteId}`, null, noteId);
}
public publishUserListStream = (listId: ID, type: string, value?: any): void => {
this.publish(`user-list-stream:${listId}`, type, typeof value === 'undefined' ? null : value);
}
public publishMessagingStream = (userId: ID, otherpartyId: ID, type: string, value?: any): void => {
this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
}
public publishMessagingIndexStream = (userId: ID, type: string, value?: any): void => {
this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
public publishReversiStream = (userId: ID, type: string, value?: any): void => {
this.publish(`reversi-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
public publishReversiGameStream = (gameId: ID, type: string, value?: any): void => {
this.publish(`reversi-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value);
}
public publishLocalTimelineStream = async (note: any): Promise<void> => {
const meta = await this.getMeta();
if (meta.disableLocalTimeline) return;
this.publish('local-timeline', null, note);
}
public publishHybridTimelineStream = async (userId: ID, note: any): Promise<void> => {
const meta = await this.getMeta();
if (meta.disableLocalTimeline) return;
this.publish(userId ? `hybrid-timeline:${userId}` : 'hybrid-timeline', null, note);
}
public publishGlobalTimelineStream = (note: any): void => {
this.publish('global-timeline', null, note);
}
} }
export function publishUserStream(userId: ID, type: string, value?: any): void { const publisher = new Publisher();
publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
export function publishDriveStream(userId: ID, type: string, value?: any): void { export default publisher;
publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
export function publishNoteStream(noteId: ID, type: string): void { export const publishUserStream = publisher.publishUserStream;
publish(`note-stream:${noteId}`, null, noteId); export const publishDriveStream = publisher.publishDriveStream;
} export const publishNoteStream = publisher.publishNoteStream;
export const publishUserListStream = publisher.publishUserListStream;
export function publishUserListStream(listId: ID, type: string, value?: any): void { export const publishMessagingStream = publisher.publishMessagingStream;
publish(`user-list-stream:${listId}`, type, typeof value === 'undefined' ? null : value); export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
} export const publishReversiStream = publisher.publishReversiStream;
export const publishReversiGameStream = publisher.publishReversiGameStream;
export function publishMessagingStream(userId: ID, otherpartyId: ID, type: string, value?: any): void { export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
} export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
export function publishMessagingIndexStream(userId: ID, type: string, value?: any): void {
publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
export function publishReversiStream(userId: ID, type: string, value?: any): void {
publish(`reversi-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
export function publishReversiGameStream(gameId: ID, type: string, value?: any): void {
publish(`reversi-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value);
}
export function publishLocalTimelineStream(note: any): void {
publish('local-timeline', null, note);
}
export function publishHybridTimelineStream(userId: ID, note: any): void {
publish(userId ? `hybrid-timeline:${userId}` : 'hybrid-timeline', null, note);
}
export function publishGlobalTimelineStream(note: any): void {
publish('global-timeline', null, note);
}