This commit is contained in:
syuilo 2018-05-19 07:33:34 +09:00
parent ddad9da1c1
commit e6eb1b2ae1
36 changed files with 5 additions and 1626 deletions

View file

@ -324,8 +324,6 @@ desktop/views/components/home.vue:
polls: "投票" polls: "投票"
post-form: "投稿フォーム" post-form: "投稿フォーム"
messaging: "メッセージ" messaging: "メッセージ"
channel: "チャンネル"
access-log: "アクセスログ"
server: "サーバー情報" server: "サーバー情報"
donation: "寄付のお願い" donation: "寄付のお願い"
nav: "ナビゲーション" nav: "ナビゲーション"
@ -336,12 +334,6 @@ desktop/views/input-dialog.vue:
cancel: "キャンセル" cancel: "キャンセル"
ok: "決定" ok: "決定"
desktop/views/components/mentions.vue:
all: "すべて"
followed: "フォロー中"
empty: "あなた宛ての投稿はありません。"
empty-followed: "あなたがフォローしているユーザーからの言及はありません。"
desktop/views/components/messaging-room-window.vue: desktop/views/components/messaging-room-window.vue:
title: "メッセージ:" title: "メッセージ:"
@ -660,11 +652,6 @@ desktop/views/widgets/users.vue:
refresh: "他を見る" refresh: "他を見る"
no-one: "いません!" no-one: "いません!"
desktop/views/widgets/channel.vue:
title: "チャンネル"
settings: "ウィジェットの設定"
get-started: "右上の歯車をクリックして受信するチャンネルを指定してください"
mobile/views/components/drive.vue: mobile/views/components/drive.vue:
drive: "ドライブ" drive: "ドライブ"
used: "使用中" used: "使用中"

View file

@ -1,15 +0,0 @@
/**
* Channels
*/
// Style
import './style.styl';
require('./tags');
import init from '../init';
/**
* init
*/
init(() => {
});

View file

@ -1,10 +0,0 @@
@import "../app"
html
padding 8px
background #efefef
#wait
top auto
bottom 15px
left 15px

View file

@ -1,409 +0,0 @@
<mk-channel>
<mk-header/>
<hr>
<main v-if="!fetching">
<h1>{ channel.title }</h1>
<div v-if="$root.$data.os.isSignedIn">
<p v-if="channel.isWatching">このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p>
<p v-if="!channel.isWatching"><a @click="watch">このチャンネルをウォッチする</a></p>
</div>
<div class="share">
<mk-twitter-button/>
<mk-line-button/>
</div>
<div class="body">
<p v-if="notesFetching">読み込み中<mk-ellipsis/></p>
<div v-if="!notesFetching">
<p v-if="notes == null || notes.length == 0">まだ投稿がありません</p>
<template v-if="notes != null">
<mk-channel-note each={ note in notes.slice().reverse() } note={ note } form={ parent.refs.form }/>
</template>
</div>
</div>
<hr>
<mk-channel-form v-if="$root.$data.os.isSignedIn" channel={ channel } ref="form"/>
<div v-if="!$root.$data.os.isSignedIn">
<p>参加するには<a href={ _URL_ }>ログインまたは新規登録</a>してください</p>
</div>
<hr>
<footer>
<small><a href={ _URL_ }>Misskey</a> ver { _VERSION_ } (葵 aoi)</small>
</footer>
</main>
<style lang="stylus" scoped>
:scope
display block
> main
> h1
font-size 1.5em
color #f00
> .share
> *
margin-right 4px
> .body
margin 8px 0 0 0
> mk-channel-form
max-width 500px
</style>
<script lang="typescript">
import Progress from '../../common/scripts/loading';
import ChannelStream from '../../common/scripts/streaming/channel-stream';
this.mixin('i');
this.mixin('api');
this.id = this.opts.id;
this.fetching = true;
this.notesFetching = true;
this.channel = null;
this.notes = null;
this.connection = new ChannelStream(this.id);
this.unreadCount = 0;
this.on('mount', () => {
document.documentElement.style.background = '#efefef';
Progress.start();
let fetched = false;
// チャンネル概要読み込み
this.$root.$data.os.api('channels/show', {
channelId: this.id
}).then(channel => {
if (fetched) {
Progress.done();
} else {
Progress.set(0.5);
fetched = true;
}
this.update({
fetching: false,
channel: channel
});
document.title = channel.title + ' | Misskey'
});
// 投稿読み込み
this.$root.$data.os.api('channels/notes', {
channelId: this.id
}).then(notes => {
if (fetched) {
Progress.done();
} else {
Progress.set(0.5);
fetched = true;
}
this.update({
notesFetching: false,
notes: notes
});
});
this.connection.on('note', this.onNote);
document.addEventListener('visibilitychange', this.onVisibilitychange, false);
});
this.on('unmount', () => {
this.connection.off('note', this.onNote);
this.connection.close();
document.removeEventListener('visibilitychange', this.onVisibilitychange);
});
this.onNote = note => {
this.notes.unshift(note);
this.update();
if (document.hidden && this.$root.$data.os.isSignedIn && note.userId !== this.$root.$data.os.i.id) {
this.unreadCount++;
document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`;
}
};
this.onVisibilitychange = () => {
if (!document.hidden) {
this.unreadCount = 0;
document.title = this.channel.title + ' | Misskey'
}
};
this.watch = () => {
this.$root.$data.os.api('channels/watch', {
channelId: this.id
}).then(() => {
this.channel.isWatching = true;
this.update();
}, e => {
alert('error');
});
};
this.unwatch = () => {
this.$root.$data.os.api('channels/unwatch', {
channelId: this.id
}).then(() => {
this.channel.isWatching = false;
this.update();
}, e => {
alert('error');
});
};
</script>
</mk-channel>
<mk-channel-note>
<header>
<a class="index" @click="reply">{ note.index }:</a>
<a class="name" href={ _URL_ + '/@' + acct }><b>{ getUserName(note.user) }</b></a>
<mk-time time={ note.createdAt }/>
<mk-time time={ note.createdAt } mode="detail"/>
<span>ID:<i>{ acct }</i></span>
</header>
<div>
<a v-if="note.reply">&gt;&gt;{ note.reply.index }</a>
{ note.text }
<div class="media" v-if="note.media">
<template each={ file in note.media }>
<a href={ file.url } target="_blank">
<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
</a>
</template>
</div>
</div>
<style lang="stylus" scoped>
:scope
display block
margin 0
padding 0
> header
position -webkit-sticky
position sticky
z-index 1
top 0
background rgba(239, 239, 239, 0.9)
> .index
margin-right 0.25em
color #000
> .name
margin-right 0.5em
color #008000
> mk-time
margin-right 0.5em
&:first-of-type
display none
@media (max-width 600px)
> mk-time
&:first-of-type
display initial
&:last-of-type
display none
> div
padding 0 0 1em 2em
> .media
> a
display inline-block
> img
max-width 100%
vertical-align bottom
</style>
<script lang="typescript">
import getAcct from '../../../../acct/render';
import getUserName from '../../../../renderers/get-user-name';
this.note = this.opts.note;
this.form = this.opts.form;
this.acct = getAcct(this.note.user);
this.name = getUserName(this.note.user);
this.reply = () => {
this.form.update({
reply: this.note
});
};
</script>
</mk-channel-note>
<mk-channel-form>
<p v-if="reply"><b>&gt;&gt;{ reply.index }</b> ({ getUserName(reply.user) }): <a @click="clearReply">[x]</a></p>
<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder="%i18n:ch.tags.mk-channel-form.textarea%"></textarea>
<div class="actions">
<button @click="selectFile">%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button>
<button @click="drive">%fa:cloud%%i18n:ch.tags.mk-channel-form.drive%</button>
<button :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0) } @click="note">
<template v-if="!wait">%fa:paper-plane%</template>{ wait ? '%i18n:!ch.tags.mk-channel-form.posting%' : '%i18n:!ch.tags.mk-channel-form.note%' }<mk-ellipsis v-if="wait"/>
</button>
</div>
<mk-uploader ref="uploader"/>
<ol v-if="files">
<li each={ files }>{ name }</li>
</ol>
<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
<style lang="stylus" scoped>
:scope
display block
> textarea
width 100%
max-width 100%
min-width 100%
min-height 5em
> .actions
display flex
> button
> [data-fa]
margin-right 0.25em
&:last-child
margin-left auto
&.wait
cursor wait
> input[type='file']
display none
</style>
<script lang="typescript">
import getUserName from '../../../../renderers/get-user-name';
this.mixin('api');
this.channel = this.opts.channel;
this.files = null;
this.on('mount', () => {
this.$refs.uploader.on('uploaded', file => {
this.update({
files: [file]
});
});
});
this.upload = file => {
this.$refs.uploader.upload(file);
};
this.clearReply = () => {
this.update({
reply: null
});
};
this.clear = () => {
this.clearReply();
this.update({
files: null
});
this.$refs.text.value = '';
};
this.note = () => {
this.update({
wait: true
});
const files = this.files && this.files.length > 0
? this.files.map(f => f.id)
: undefined;
this.$root.$data.os.api('notes/create', {
text: this.$refs.text.value == '' ? undefined : this.$refs.text.value,
mediaIds: files,
replyId: this.reply ? this.reply.id : undefined,
channelId: this.channel.id
}).then(data => {
this.clear();
}).catch(err => {
alert('失敗した');
}).then(() => {
this.update({
wait: false
});
});
};
this.changeFile = () => {
Array.from(this.$refs.file.files).forEach(this.upload);
};
this.selectFile = () => {
this.$refs.file.click();
};
this.drive = () => {
window['cb'] = files => {
this.update({
files: files
});
};
window.open(_URL_ + '/selectdrive?multiple=true',
'drive_window',
'height=500,width=800');
};
this.onkeydown = e => {
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
};
this.onpaste = e => {
Array.from(e.clipboardData.items).forEach(item => {
if (item.kind == 'file') {
this.upload(item.getAsFile());
}
});
};
this.getUserName = getUserName;
</script>
</mk-channel-form>
<mk-twitter-button>
<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a>
<script lang="typescript">
this.on('mount', () => {
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.setAttribute('src', 'https://platform.twitter.com/widgets.js');
script.setAttribute('async', 'async');
head.appendChild(script);
});
</script>
</mk-twitter-button>
<mk-line-button>
<div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ _CH_URL_ } style="display: none;"></div>
<script lang="typescript">
this.on('mount', () => {
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.setAttribute('src', 'https://d.line-scdn.net/r/web/social-plugin/js/thirdparty/loader.min.js');
script.setAttribute('async', 'async');
head.appendChild(script);
});
</script>
</mk-line-button>

View file

@ -1,20 +0,0 @@
<mk-header>
<div>
<a href={ _CH_URL_ }>Index</a> | <a href={ _URL_ }>Misskey</a>
</div>
<div>
<a v-if="!$root.$data.os.isSignedIn" href={ _URL_ }>ログイン(新規登録)</a>
<a v-if="$root.$data.os.isSignedIn" href={ _URL_ + '/@' + I.username }>{ I.username }</a>
</div>
<style lang="stylus" scoped>
:scope
display flex
> div:last-child
margin-left auto
</style>
<script lang="typescript">
this.mixin('i');
</script>
</mk-header>

View file

@ -1,37 +0,0 @@
<mk-index>
<mk-header/>
<hr>
<button @click="n">%i18n:ch.tags.mk-index.new%</button>
<hr>
<ul v-if="channels">
<li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li>
</ul>
<style lang="stylus" scoped>
:scope
display block
</style>
<script lang="typescript">
this.mixin('api');
this.on('mount', () => {
this.$root.$data.os.api('channels', {
limit: 100
}).then(channels => {
this.update({
channels: channels
});
});
});
this.n = () => {
const title = window.prompt('%i18n:!ch.tags.mk-index.channel-title%');
this.$root.$data.os.api('channels/create', {
title: title
}).then(channel => {
location.href = '/' + channel.id;
});
};
</script>
</mk-index>

View file

@ -1,3 +0,0 @@
require('./index.tag');
require('./channel.tag');
require('./header.tag');

View file

@ -1,13 +0,0 @@
import Stream from './stream';
import MiOS from '../../../mios';
/**
* Channel stream connection
*/
export default class Connection extends Stream {
constructor(os: MiOS, channelId) {
super(os, 'channel', {
channel: channelId
});
}
}

View file

@ -1,91 +0,0 @@
<template>
<div class="mkw-access-log">
<mk-widget-container :show-header="props.design == 0">
<template slot="header">%fa:server%%i18n:@title%</template>
<div :class="$style.logs" ref="log">
<p v-for="req in requests">
<span :class="$style.ip" :style="`color:${ req.fg }; background:${ req.bg }`">{{ req.ip }}</span>
<b>{{ req.method }}</b>
<span>{{ req.path }}</span>
</p>
</div>
</mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
import * as seedrandom from 'seedrandom';
export default define({
name: 'broadcast',
props: () => ({
design: 0
})
}).extend({
data() {
return {
requests: [],
connection: null,
connectionId: null
};
},
mounted() {
this.connection = (this as any).os.streams.requestsStream.getConnection();
this.connectionId = (this as any).os.streams.requestsStream.use();
this.connection.on('request', this.onRequest);
},
beforeDestroy() {
this.connection.off('request', this.onRequest);
(this as any).os.streams.requestsStream.dispose(this.connectionId);
},
methods: {
onRequest(request) {
const random = seedrandom(request.ip);
const r = Math.floor(random() * 255);
const g = Math.floor(random() * 255);
const b = Math.floor(random() * 255);
const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b); // SMPTE C, Rec. 709 weightings
request.bg = `rgb(${r}, ${g}, ${b})`;
request.fg = luma >= 165 ? '#000' : '#fff';
this.requests.push(request);
if (this.requests.length > 30) this.requests.shift();
(this.$refs.log as any).scrollTop = (this.$refs.log as any).scrollHeight;
},
func() {
if (this.props.design == 1) {
this.props.design = 0;
} else {
this.props.design++;
}
this.save();
}
}
});
</script>
<style lang="stylus" module>
.logs
max-height 250px
overflow auto
> p
margin 0
padding 8px
font-size 0.8em
color #555
&:nth-child(odd)
background rgba(#000, 0.025)
> b
margin-right 4px
.ip
margin-right 4px
padding 0 4px
</style>

View file

@ -1,6 +1,5 @@
import Vue from 'vue'; import Vue from 'vue';
import wAccessLog from './access-log.vue';
import wVersion from './version.vue'; import wVersion from './version.vue';
import wRss from './rss.vue'; import wRss from './rss.vue';
import wServer from './server.vue'; import wServer from './server.vue';
@ -22,4 +21,3 @@ Vue.component('mkw-broadcast', wBroadcast);
Vue.component('mkw-server', wServer); Vue.component('mkw-server', wServer);
Vue.component('mkw-rss', wRss); Vue.component('mkw-rss', wRss);
Vue.component('mkw-version', wVersion); Vue.component('mkw-version', wVersion);
Vue.component('mkw-access-log', wAccessLog);

View file

@ -21,8 +21,6 @@
<option value="polls">%i18n:@polls%</option> <option value="polls">%i18n:@polls%</option>
<option value="post-form">%i18n:@post-form%</option> <option value="post-form">%i18n:@post-form%</option>
<option value="messaging">%i18n:@messaging%</option> <option value="messaging">%i18n:@messaging%</option>
<option value="channel">%i18n:@channel%</option>
<option value="access-log">%i18n:@access-log%</option>
<option value="server">%i18n:@server%</option> <option value="server">%i18n:@server%</option>
<option value="donation">%i18n:@donation%</option> <option value="donation">%i18n:@donation%</option>
<option value="nav">%i18n:@nav%</option> <option value="nav">%i18n:@nav%</option>

View file

@ -1,125 +0,0 @@
<template>
<div class="mk-mentions">
<header>
<span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%</span>
<span :data-active="mode == 'following'" @click="mode = 'following'">%i18n:@followed%</span>
</header>
<div class="fetching" v-if="fetching">
<mk-ellipsis-icon/>
</div>
<p class="empty" v-if="notes.length == 0 && !fetching">
%fa:R comments%
<span v-if="mode == 'all'">%i18n:@empty%</span>
<span v-if="mode == 'following'">%i18n:@empty-followed%</span>
</p>
<mk-notes :notes="notes" ref="timeline"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
fetching: true,
moreFetching: false,
mode: 'all',
notes: []
};
},
watch: {
mode() {
this.fetch();
}
},
mounted() {
document.addEventListener('keydown', this.onDocumentKeydown);
window.addEventListener('scroll', this.onScroll);
this.fetch(() => this.$emit('loaded'));
},
beforeDestroy() {
document.removeEventListener('keydown', this.onDocumentKeydown);
window.removeEventListener('scroll', this.onScroll);
},
methods: {
onDocumentKeydown(e) {
if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
if (e.which == 84) { // t
(this.$refs.timeline as any).focus();
}
}
},
onScroll() {
const current = window.scrollY + window.innerHeight;
if (current > document.body.offsetHeight - 8) this.more();
},
fetch(cb?) {
this.fetching = true;
this.notes = [];
(this as any).api('notes/mentions', {
following: this.mode == 'following'
}).then(notes => {
this.notes = notes;
this.fetching = false;
if (cb) cb();
});
},
more() {
if (this.moreFetching || this.fetching || this.notes.length == 0) return;
this.moreFetching = true;
(this as any).api('notes/mentions', {
following: this.mode == 'following',
untilId: this.notes[this.notes.length - 1].id
}).then(notes => {
this.notes = this.notes.concat(notes);
this.moreFetching = false;
});
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.mk-mentions
background #fff
border solid 1px rgba(#000, 0.075)
border-radius 6px
> header
padding 8px 16px
border-bottom solid 1px #eee
> span
margin-right 16px
line-height 27px
font-size 18px
color #555
&:not([data-active])
color $theme-color
cursor pointer
&:hover
text-decoration underline
> .fetching
padding 64px 0
> .empty
display block
margin 0 auto
padding 32px
max-width 400px
text-align center
color #999
> [data-fa]
display block
margin-bottom 16px
font-size 3em
color #ccc
</style>

View file

@ -33,9 +33,6 @@
</div> </div>
</header> </header>
<div class="body"> <div class="body">
<p class="channel" v-if="p.channel">
<a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
</p>
<p v-if="p.cw != null" class="cw"> <p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
<span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span> <span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span>
@ -574,9 +571,6 @@ root(isDark)
.mk-url-preview .mk-url-preview
margin-top 8px margin-top 8px
> .channel
margin 0
> .mk-poll > .mk-poll
font-size 80% font-size 80%

View file

@ -1,67 +0,0 @@
<template>
<div class="form">
<input v-model="text" :disabled="wait" @keydown="onKeydown" placeholder="書いて">
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
text: '',
wait: false
};
},
methods: {
onKeydown(e) {
if (e.which == 10 || e.which == 13) this.post();
},
post() {
this.wait = true;
let reply = null;
if (/^>>([0-9]+) /.test(this.text)) {
const index = this.text.match(/^>>([0-9]+) /)[1];
reply = (this.$parent as any).notes.find(p => p.index.toString() == index);
this.text = this.text.replace(/^>>([0-9]+) /, '');
}
(this as any).api('notes/create', {
text: this.text,
replyId: reply ? reply.id : undefined,
channelId: (this.$parent as any).channel.id
}).then(data => {
this.text = '';
}).catch(err => {
alert('失敗した');
}).then(() => {
this.wait = false;
});
}
}
});
</script>
<style lang="stylus" scoped>
.form
width 100%
height 38px
padding 4px
border-top solid 1px #ddd
> input
padding 0 8px
width 100%
height 100%
font-size 14px
color #55595c
border solid 1px #dadada
border-radius 4px
&:hover
&:focus
border-color #aeaeae
</style>

View file

@ -1,65 +0,0 @@
<template>
<div class="note">
<header>
<a class="index" @click="reply">{{ note.index }}:</a>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"><b>{{ note.user | userName }}</b></router-link>
<span>ID:<i>{{ note.user | acct }}</i></span>
</header>
<div>
<a v-if="note.reply">&gt;&gt;{{ note.reply.index }}</a>
{{ note.text }}
<div class="media" v-if="note.media">
<a v-for="file in note.media" :href="file.url" target="_blank">
<img :src="`${file.url}?thumbnail&size=512`" :alt="file.name" :title="file.name"/>
</a>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: ['note'],
methods: {
reply() {
this.$emit('reply', this.note);
}
}
});
</script>
<style lang="stylus" scoped>
.note
margin 0
padding 0
color #444
> header
position -webkit-sticky
position sticky
z-index 1
top 0
padding 8px 4px 4px 16px
background rgba(255, 255, 255, 0.9)
> .index
margin-right 0.25em
> .name
margin-right 0.5em
color #008000
> div
padding 0 16px 16px 16px
> .media
> a
display inline-block
> img
max-width 100%
vertical-align bottom
</style>

View file

@ -1,106 +0,0 @@
<template>
<div class="channel">
<p v-if="fetching">読み込み中<mk-ellipsis/></p>
<div v-if="!fetching" ref="notes" class="notes">
<p v-if="notes.length == 0">まだ投稿がありません</p>
<x-note class="note" v-for="note in notes.slice().reverse()" :note="note" :key="note.id" @reply="reply"/>
</div>
<x-form class="form" ref="form"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import ChannelStream from '../../../common/scripts/streaming/channel';
import XForm from './channel.channel.form.vue';
import XNote from './channel.channel.note.vue';
export default Vue.extend({
components: {
XForm,
XNote
},
props: ['channel'],
data() {
return {
fetching: true,
notes: [],
connection: null
};
},
watch: {
channel() {
this.zap();
}
},
mounted() {
this.zap();
},
beforeDestroy() {
this.disconnect();
},
methods: {
zap() {
this.fetching = true;
(this as any).api('channels/notes', {
channelId: this.channel.id
}).then(notes => {
this.notes = notes;
this.fetching = false;
this.$nextTick(() => {
this.scrollToBottom();
});
this.disconnect();
this.connection = new ChannelStream((this as any).os, this.channel.id);
this.connection.on('note', this.onNote);
});
},
disconnect() {
if (this.connection) {
this.connection.off('note', this.onNote);
this.connection.close();
}
},
onNote(note) {
this.notes.unshift(note);
this.scrollToBottom();
},
scrollToBottom() {
(this.$refs.notes as any).scrollTop = (this.$refs.notes as any).scrollHeight;
},
reply(note) {
(this.$refs.form as any).text = `>>${ note.index } `;
}
}
});
</script>
<style lang="stylus" scoped>
.channel
> p
margin 0
padding 16px
text-align center
color #aaa
> .notes
height calc(100% - 38px)
overflow auto
font-size 0.9em
> .note
border-bottom solid 1px #eee
&:last-child
border-bottom none
> .form
position absolute
left 0
bottom 0
</style>

View file

@ -1,108 +0,0 @@
<template>
<div class="mkw-channel">
<template v-if="!props.compact">
<p class="title">%fa:tv%{{ channel ? channel.title : '%i18n:!@title%' }}</p>
<button @click="settings" title="%i18n:@settings%">%fa:cog%</button>
</template>
<p class="get-started" v-if="props.channel == null">%i18n:@get-started%</p>
<x-channel class="channel" :channel="channel" v-if="channel != null"/>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
import XChannel from './channel.channel.vue';
export default define({
name: 'server',
props: () => ({
channel: null,
compact: false
})
}).extend({
components: {
XChannel
},
data() {
return {
fetching: true,
channel: null
};
},
mounted() {
if (this.props.channel) {
this.zap();
}
},
methods: {
func() {
this.props.compact = !this.props.compact;
this.save();
},
settings() {
const id = window.prompt('チャンネルID');
if (!id) return;
this.props.channel = id;
this.zap();
},
zap() {
this.fetching = true;
(this as any).api('channels/show', {
channelId: this.props.channel
}).then(channel => {
this.channel = channel;
this.fetching = false;
});
}
}
});
</script>
<style lang="stylus" scoped>
.mkw-channel
background #fff
border solid 1px rgba(#000, 0.075)
border-radius 6px
overflow hidden
> .title
z-index 2
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color #888
box-shadow 0 1px rgba(#000, 0.07)
> [data-fa]
margin-right 4px
> button
position absolute
z-index 2
top 0
right 0
padding 0
width 42px
font-size 0.9em
line-height 42px
color #ccc
&:hover
color #aaa
&:active
color #999
> .get-started
margin 0
padding 16px
text-align center
color #aaa
> .channel
height 200px
</style>

View file

@ -8,7 +8,6 @@ import wUsers from './users.vue';
import wPolls from './polls.vue'; import wPolls from './polls.vue';
import wPostForm from './post-form.vue'; import wPostForm from './post-form.vue';
import wMessaging from './messaging.vue'; import wMessaging from './messaging.vue';
import wChannel from './channel.vue';
import wProfile from './profile.vue'; import wProfile from './profile.vue';
Vue.component('mkw-notifications', wNotifications); Vue.component('mkw-notifications', wNotifications);
@ -19,5 +18,4 @@ Vue.component('mkw-users', wUsers);
Vue.component('mkw-polls', wPolls); Vue.component('mkw-polls', wPolls);
Vue.component('mkw-post-form', wPostForm); Vue.component('mkw-post-form', wPostForm);
Vue.component('mkw-messaging', wMessaging); Vue.component('mkw-messaging', wMessaging);
Vue.component('mkw-channel', wChannel);
Vue.component('mkw-profile', wProfile); Vue.component('mkw-profile', wProfile);

View file

@ -32,7 +32,6 @@
</div> </div>
</header> </header>
<div class="body"> <div class="body">
<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
<p v-if="p.cw != null" class="cw"> <p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
<span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span> <span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span>
@ -470,9 +469,6 @@ root(isDark)
.mk-url-preview .mk-url-preview
margin-top 8px margin-top 8px
> .channel
margin 0
> .tags > .tags
margin 4px 0 0 0 margin 4px 0 0 0

View file

@ -15,7 +15,6 @@
<option value="photo-stream">フォトストリーム</option> <option value="photo-stream">フォトストリーム</option>
<option value="slideshow">スライドショー</option> <option value="slideshow">スライドショー</option>
<option value="version">バージョン</option> <option value="version">バージョン</option>
<option value="access-log">アクセスログ</option>
<option value="server">サーバー情報</option> <option value="server">サーバー情報</option>
<option value="donation">寄付のお願い</option> <option value="donation">寄付のお願い</option>
<option value="nav">ナビゲーション</option> <option value="nav">ナビゲーション</option>

View file

@ -1,13 +0,0 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const ChannelWatching = db.get<IChannelWatching>('channelWatching');
export default ChannelWatching;
export interface IChannelWatching {
_id: mongo.ObjectID;
createdAt: Date;
deletedAt: Date;
channelId: mongo.ObjectID;
userId: mongo.ObjectID;
}

View file

@ -1,75 +0,0 @@
import * as mongo from 'mongodb';
import deepcopy = require('deepcopy');
import { IUser } from './user';
import Watching from './channel-watching';
import db from '../db/mongodb';
const Channel = db.get<IChannel>('channels');
export default Channel;
export type IChannel = {
_id: mongo.ObjectID;
createdAt: Date;
title: string;
userId: mongo.ObjectID;
index: number;
watchingCount: number;
};
/**
* Pack a channel for API response
*
* @param channel target
* @param me? serializee
* @return response
*/
export const pack = (
channel: string | mongo.ObjectID | IChannel,
me?: string | mongo.ObjectID | IUser
) => new Promise<any>(async (resolve, reject) => {
let _channel: any;
// Populate the channel if 'channel' is ID
if (mongo.ObjectID.prototype.isPrototypeOf(channel)) {
_channel = await Channel.findOne({
_id: channel
});
} else if (typeof channel === 'string') {
_channel = await Channel.findOne({
_id: new mongo.ObjectID(channel)
});
} else {
_channel = deepcopy(channel);
}
// Rename _id to id
_channel.id = _channel._id;
delete _channel._id;
// Remove needless properties
delete _channel.userId;
// Me
const meId: mongo.ObjectID = me
? mongo.ObjectID.prototype.isPrototypeOf(me)
? me as mongo.ObjectID
: typeof me === 'string'
? new mongo.ObjectID(me)
: (me as IUser)._id
: null;
if (me) {
//#region Watchしているかどうか
const watch = await Watching.findOne({
userId: meId,
channelId: _channel.id,
deletedAt: { $exists: false }
});
_channel.isWatching = watch !== null;
//#endregion
}
resolve(_channel);
});

View file

@ -4,7 +4,6 @@ import rap from '@prezzemolo/rap';
import db from '../db/mongodb'; import db from '../db/mongodb';
import { IUser, pack as packUser } from './user'; import { IUser, pack as packUser } from './user';
import { pack as packApp } from './app'; import { pack as packApp } from './app';
import { pack as packChannel } from './channel';
import PollVote, { deletePollVote } from './poll-vote'; import PollVote, { deletePollVote } from './poll-vote';
import Reaction, { deleteNoteReaction } from './note-reaction'; import Reaction, { deleteNoteReaction } from './note-reaction';
import { pack as packFile } from './drive-file'; import { pack as packFile } from './drive-file';
@ -29,7 +28,6 @@ export function isValidCw(text: string): boolean {
export type INote = { export type INote = {
_id: mongo.ObjectID; _id: mongo.ObjectID;
channelId: mongo.ObjectID;
createdAt: Date; createdAt: Date;
deletedAt: Date; deletedAt: Date;
mediaIds: mongo.ObjectID[]; mediaIds: mongo.ObjectID[];
@ -258,11 +256,6 @@ export const pack = async (
_note.app = packApp(_note.appId); _note.app = packApp(_note.appId);
} }
// Populate channel
if (_note.channelId) {
_note.channel = packChannel(_note.channelId);
}
// Populate media // Populate media
_note.media = hide ? [] : Promise.all(_note.mediaIds.map(fileId => _note.media = hide ? [] : Promise.all(_note.mediaIds.map(fileId =>
packFile(fileId) packFile(fileId)

View file

@ -45,10 +45,6 @@ class MisskeyEvent {
this.publish(`othello-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value); this.publish(`othello-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value);
} }
public publishChannelStream(channelId: ID, type: string, value?: any): void {
this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value);
}
public publishLocalTimelineStream(note: any): void { public publishLocalTimelineStream(note: any): void {
this.redisClient.publish('misskey:local-timeline', JSON.stringify(note)); this.redisClient.publish('misskey:local-timeline', JSON.stringify(note));
} }
@ -79,4 +75,3 @@ export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev); export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev);
export const publishOthelloStream = ev.publishOthelloStream.bind(ev); export const publishOthelloStream = ev.publishOthelloStream.bind(ev);
export const publishOthelloGameStream = ev.publishOthelloGameStream.bind(ev); export const publishOthelloGameStream = ev.publishOthelloGameStream.bind(ev);
export const publishChannelStream = ev.publishChannelStream.bind(ev);

View file

@ -9,9 +9,6 @@ const summarize = (note: any): string => {
let summary = ''; let summary = '';
// チャンネル
summary += note.channel ? `${note.channel.title}:` : '';
// 本文 // 本文
summary += note.text ? note.text : ''; summary += note.text ? note.text : '';

View file

@ -621,33 +621,7 @@ const endpoints: Endpoint[] = [
name: 'messaging/messages/create', name: 'messaging/messages/create',
withCredential: true, withCredential: true,
kind: 'messaging-write' kind: 'messaging-write'
},
{
name: 'channels/create',
withCredential: true,
limit: {
duration: ms('1hour'),
max: 3,
minInterval: ms('10seconds')
} }
},
{
name: 'channels/show'
},
{
name: 'channels/notes'
},
{
name: 'channels/watch',
withCredential: true
},
{
name: 'channels/unwatch',
withCredential: true
},
{
name: 'channels'
},
]; ];
export default endpoints; export default endpoints;

View file

@ -1,58 +0,0 @@
/**
* Module dependencies
*/
import $ from 'cafy'; import ID from '../../../cafy-id';
import Channel, { pack } from '../../../models/channel';
/**
* Get all channels
*
* @param {any} params
* @param {any} me
* @return {Promise<any>}
*/
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'limit' parameter
const [limit = 10, limitErr] = $.num.optional().range(1, 100).get(params.limit);
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
const [sinceId, sinceIdErr] = $.type(ID).optional().get(params.sinceId);
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
const [untilId, untilIdErr] = $.type(ID).optional().get(params.untilId);
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');
}
// Construct query
const sort = {
_id: -1
};
const query = {} as any;
if (sinceId) {
sort._id = 1;
query._id = {
$gt: sinceId
};
} else if (untilId) {
query._id = {
$lt: untilId
};
}
// Issue query
const channels = await Channel
.find(query, {
limit: limit,
sort: sort
});
// Serialize
res(await Promise.all(channels.map(async channel =>
await pack(channel, me))));
});

View file

@ -1,35 +0,0 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Channel from '../../../../models/channel';
import Watching from '../../../../models/channel-watching';
import { pack } from '../../../../models/channel';
/**
* Create a channel
*/
module.exports = async (params, user) => new Promise(async (res, rej) => {
// Get 'title' parameter
const [title, titleErr] = $.str.range(1, 100).get(params.title);
if (titleErr) return rej('invalid title param');
// Create a channel
const channel = await Channel.insert({
createdAt: new Date(),
userId: user._id,
title: title,
index: 0,
watchingCount: 1
});
// Response
res(await pack(channel));
// Create Watching
await Watching.insert({
createdAt: new Date(),
userId: user._id,
channelId: channel._id
});
});

View file

@ -1,74 +0,0 @@
/**
* Module dependencies
*/
import $ from 'cafy'; import ID from '../../../../cafy-id';
import { default as Channel, IChannel } from '../../../../models/channel';
import Note, { pack } from '../../../../models/note';
/**
* Show a notes of a channel
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'limit' parameter
const [limit = 1000, limitErr] = $.num.optional().range(1, 1000).get(params.limit);
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
const [sinceId, sinceIdErr] = $.type(ID).optional().get(params.sinceId);
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
const [untilId, untilIdErr] = $.type(ID).optional().get(params.untilId);
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');
}
// Get 'channelId' parameter
const [channelId, channelIdErr] = $.type(ID).get(params.channelId);
if (channelIdErr) return rej('invalid channelId param');
// Fetch channel
const channel: IChannel = await Channel.findOne({
_id: channelId
});
if (channel === null) {
return rej('channel not found');
}
//#region Construct query
const sort = {
_id: -1
};
const query = {
channelId: channel._id
} as any;
if (sinceId) {
sort._id = 1;
query._id = {
$gt: sinceId
};
} else if (untilId) {
query._id = {
$lt: untilId
};
}
//#endregion Construct query
// Issue query
const notes = await Note
.find(query, {
limit: limit,
sort: sort
});
// Serialize
res(await Promise.all(notes.map(async (note) =>
await pack(note, user)
)));
});

View file

@ -1,26 +0,0 @@
/**
* Module dependencies
*/
import $ from 'cafy'; import ID from '../../../../cafy-id';
import Channel, { IChannel, pack } from '../../../../models/channel';
/**
* Show a channel
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'channelId' parameter
const [channelId, channelIdErr] = $.type(ID).get(params.channelId);
if (channelIdErr) return rej('invalid channelId param');
// Fetch channel
const channel: IChannel = await Channel.findOne({
_id: channelId
});
if (channel === null) {
return rej('channel not found');
}
// Serialize
res(await pack(channel, user));
});

View file

@ -1,56 +0,0 @@
/**
* Module dependencies
*/
import $ from 'cafy'; import ID from '../../../../cafy-id';
import Channel from '../../../../models/channel';
import Watching from '../../../../models/channel-watching';
/**
* Unwatch a channel
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'channelId' parameter
const [channelId, channelIdErr] = $.type(ID).get(params.channelId);
if (channelIdErr) return rej('invalid channelId param');
//#region Fetch channel
const channel = await Channel.findOne({
_id: channelId
});
if (channel === null) {
return rej('channel not found');
}
//#endregion
//#region Check whether not watching
const exist = await Watching.findOne({
userId: user._id,
channelId: channel._id,
deletedAt: { $exists: false }
});
if (exist === null) {
return rej('already not watching');
}
//#endregion
// Delete watching
await Watching.update({
_id: exist._id
}, {
$set: {
deletedAt: new Date()
}
});
// Send response
res();
// Decrement watching count
Channel.update(channel._id, {
$inc: {
watchingCount: -1
}
});
});

View file

@ -1,54 +0,0 @@
/**
* Module dependencies
*/
import $ from 'cafy'; import ID from '../../../../cafy-id';
import Channel from '../../../../models/channel';
import Watching from '../../../../models/channel-watching';
/**
* Watch a channel
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'channelId' parameter
const [channelId, channelIdErr] = $.type(ID).get(params.channelId);
if (channelIdErr) return rej('invalid channelId param');
//#region Fetch channel
const channel = await Channel.findOne({
_id: channelId
});
if (channel === null) {
return rej('channel not found');
}
//#endregion
//#region Check whether already watching
const exist = await Watching.findOne({
userId: user._id,
channelId: channel._id,
deletedAt: { $exists: false }
});
if (exist !== null) {
return rej('already watching');
}
//#endregion
// Create Watching
await Watching.insert({
createdAt: new Date(),
userId: user._id,
channelId: channel._id
});
// Send response
res();
// Increment watching count
Channel.update(channel._id, {
$inc: {
watchingCount: 1
}
});
});

View file

@ -4,7 +4,6 @@
import $ from 'cafy'; import ID from '../../../../cafy-id'; import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note'; import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note';
import User, { ILocalUser } from '../../../../models/user'; import User, { ILocalUser } from '../../../../models/user';
import Channel, { IChannel } from '../../../../models/channel';
import DriveFile from '../../../../models/drive-file'; import DriveFile from '../../../../models/drive-file';
import create from '../../../../services/note/create'; import create from '../../../../services/note/create';
import { IApp } from '../../../../models/app'; import { IApp } from '../../../../models/app';
@ -89,7 +88,6 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
if (renoteIdErr) return rej('invalid renoteId'); if (renoteIdErr) return rej('invalid renoteId');
let renote: INote = null; let renote: INote = null;
let isQuote = false;
if (renoteId !== undefined) { if (renoteId !== undefined) {
// Fetch renote to note // Fetch renote to note
renote = await Note.findOne({ renote = await Note.findOne({
@ -101,8 +99,6 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
} else if (renote.renoteId && !renote.text && !renote.mediaIds) { } else if (renote.renoteId && !renote.text && !renote.mediaIds) {
return rej('cannot renote to renote'); return rej('cannot renote to renote');
} }
isQuote = text != null || files != null;
} }
// Get 'replyId' parameter // Get 'replyId' parameter
@ -126,47 +122,6 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
} }
} }
// Get 'channelId' parameter
const [channelId, channelIdErr] = $.type(ID).optional().get(params.channelId);
if (channelIdErr) return rej('invalid channelId');
let channel: IChannel = null;
if (channelId !== undefined) {
// Fetch channel
channel = await Channel.findOne({
_id: channelId
});
if (channel === null) {
return rej('channel not found');
}
// 返信対象の投稿がこのチャンネルじゃなかったらダメ
if (reply && !channelId.equals(reply.channelId)) {
return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません');
}
// Renote対象の投稿がこのチャンネルじゃなかったらダメ
if (renote && !channelId.equals(renote.channelId)) {
return rej('チャンネル内部からチャンネル外部の投稿をRenoteすることはできません');
}
// 引用ではないRenoteはダメ
if (renote && !isQuote) {
return rej('チャンネル内部では引用ではないRenoteをすることはできません');
}
} else {
// 返信対象の投稿がチャンネルへの投稿だったらダメ
if (reply && reply.channelId != null) {
return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません');
}
// Renote対象の投稿がチャンネルへの投稿だったらダメ
if (renote && renote.channelId != null) {
return rej('チャンネル外部からチャンネル内部の投稿をRenoteすることはできません');
}
}
// Get 'poll' parameter // Get 'poll' parameter
const [poll, pollErr] = $.obj.optional().strict() const [poll, pollErr] = $.obj.optional().strict()
.have('choices', $.arr($.str) .have('choices', $.arr($.str)

View file

@ -4,7 +4,6 @@
import $ from 'cafy'; import ID from '../../../../cafy-id'; import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note from '../../../../models/note'; import Note from '../../../../models/note';
import Mute from '../../../../models/mute'; import Mute from '../../../../models/mute';
import ChannelWatching from '../../../../models/channel-watching';
import { getFriends } from '../../common/get-friends'; import { getFriends } from '../../common/get-friends';
import { pack } from '../../../../models/note'; import { pack } from '../../../../models/note';
@ -45,18 +44,11 @@ module.exports = async (params, user, app) => {
const [includeRenotedMyNotes = true, includeRenotedMyNotesErr] = $.bool.optional().get(params.includeRenotedMyNotes); const [includeRenotedMyNotes = true, includeRenotedMyNotesErr] = $.bool.optional().get(params.includeRenotedMyNotes);
if (includeRenotedMyNotesErr) throw 'invalid includeRenotedMyNotes param'; if (includeRenotedMyNotesErr) throw 'invalid includeRenotedMyNotes param';
const [followings, watchingChannelIds, mutedUserIds] = await Promise.all([ const [followings, mutedUserIds] = await Promise.all([
// フォローを取得 // フォローを取得
// Fetch following // Fetch following
getFriends(user._id), getFriends(user._id),
// Watchしているチャンネルを取得
ChannelWatching.find({
userId: user._id,
// 削除されたドキュメントは除く
deletedAt: { $exists: false }
}).then(watches => watches.map(w => w.channelId)),
// ミュートしているユーザーを取得 // ミュートしているユーザーを取得
Mute.find({ Mute.find({
muterId: user._id muterId: user._id
@ -93,26 +85,9 @@ module.exports = async (params, user, app) => {
const query = { const query = {
$and: [{ $and: [{
$or: [{ // フォローしている人の投稿
$and: [{ $or: followQuery,
// フォローしている人のタイムラインへの投稿
$or: followQuery
}, {
// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
$or: [{
channelId: {
$exists: false
}
}, {
channelId: null
}]
}]
}, {
// Watchしているチャンネルへの投稿
channelId: {
$in: watchingChannelIds
}
}],
// mute // mute
userId: { userId: {
$nin: mutedUserIds $nin: mutedUserIds

View file

@ -1,14 +0,0 @@
import * as websocket from 'websocket';
import * as redis from 'redis';
import { ParsedUrlQuery } from 'querystring';
export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient): void {
const q = request.resourceURL.query as ParsedUrlQuery;
const channel = q.channel;
// Subscribe channel stream
subscriber.subscribe(`misskey:channel-stream:${channel}`);
subscriber.on('message', (_, data) => {
connection.send(data);
});
}

View file

@ -14,7 +14,6 @@ import othelloGameStream from './stream/othello-game';
import othelloStream from './stream/othello'; import othelloStream from './stream/othello';
import serverStream from './stream/server'; import serverStream from './stream/server';
import requestsStream from './stream/requests'; import requestsStream from './stream/requests';
import channelStream from './stream/channel';
import { ParsedUrlQuery } from 'querystring'; import { ParsedUrlQuery } from 'querystring';
import authenticate from './authenticate'; import authenticate from './authenticate';
@ -48,11 +47,6 @@ module.exports = (server: http.Server) => {
subscriber.quit(); subscriber.quit();
}); });
if (request.resourceURL.pathname === '/channel') {
channelStream(request, connection, subscriber);
return;
}
const q = request.resourceURL.query as ParsedUrlQuery; const q = request.resourceURL.query as ParsedUrlQuery;
const [user, app] = await authenticate(q.i as string); const [user, app] = await authenticate(q.i as string);