diff options
Diffstat (limited to 'src/server/web/app/ch')
| -rw-r--r-- | src/server/web/app/ch/script.ts | 15 | ||||
| -rw-r--r-- | src/server/web/app/ch/style.styl | 10 | ||||
| -rw-r--r-- | src/server/web/app/ch/tags/channel.tag | 403 | ||||
| -rw-r--r-- | src/server/web/app/ch/tags/header.tag | 20 | ||||
| -rw-r--r-- | src/server/web/app/ch/tags/index.tag | 37 | ||||
| -rw-r--r-- | src/server/web/app/ch/tags/index.ts | 3 |
6 files changed, 488 insertions, 0 deletions
diff --git a/src/server/web/app/ch/script.ts b/src/server/web/app/ch/script.ts new file mode 100644 index 0000000000..4c6b6dfd1b --- /dev/null +++ b/src/server/web/app/ch/script.ts @@ -0,0 +1,15 @@ +/** + * Channels + */ + +// Style +import './style.styl'; + +require('./tags'); +import init from '../init'; + +/** + * init + */ +init(() => { +}); diff --git a/src/server/web/app/ch/style.styl b/src/server/web/app/ch/style.styl new file mode 100644 index 0000000000..21ca648cbe --- /dev/null +++ b/src/server/web/app/ch/style.styl @@ -0,0 +1,10 @@ +@import "../app" + +html + padding 8px + background #efefef + +#wait + top auto + bottom 15px + left 15px diff --git a/src/server/web/app/ch/tags/channel.tag b/src/server/web/app/ch/tags/channel.tag new file mode 100644 index 0000000000..dc4b8e1426 --- /dev/null +++ b/src/server/web/app/ch/tags/channel.tag @@ -0,0 +1,403 @@ +<mk-channel> + <mk-header/> + <hr> + <main v-if="!fetching"> + <h1>{ channel.title }</h1> + + <div v-if="$root.$data.os.isSignedIn"> + <p v-if="channel.is_watching">このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p> + <p v-if="!channel.is_watching"><a @click="watch">このチャンネルをウォッチする</a></p> + </div> + + <div class="share"> + <mk-twitter-button/> + <mk-line-button/> + </div> + + <div class="body"> + <p v-if="postsFetching">読み込み中<mk-ellipsis/></p> + <div v-if="!postsFetching"> + <p v-if="posts == null || posts.length == 0">まだ投稿がありません</p> + <template v-if="posts != null"> + <mk-channel-post each={ post in posts.slice().reverse() } post={ post } 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.postsFetching = true; + this.channel = null; + this.posts = 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', { + channel_id: 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/posts', { + channel_id: this.id + }).then(posts => { + if (fetched) { + Progress.done(); + } else { + Progress.set(0.5); + fetched = true; + } + + this.update({ + postsFetching: false, + posts: posts + }); + }); + + this.connection.on('post', this.onPost); + document.addEventListener('visibilitychange', this.onVisibilitychange, false); + }); + + this.on('unmount', () => { + this.connection.off('post', this.onPost); + this.connection.close(); + document.removeEventListener('visibilitychange', this.onVisibilitychange); + }); + + this.onPost = post => { + this.posts.unshift(post); + this.update(); + + if (document.hidden && this.$root.$data.os.isSignedIn && post.user_id !== 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', { + channel_id: this.id + }).then(() => { + this.channel.is_watching = true; + this.update(); + }, e => { + alert('error'); + }); + }; + + this.unwatch = () => { + this.$root.$data.os.api('channels/unwatch', { + channel_id: this.id + }).then(() => { + this.channel.is_watching = false; + this.update(); + }, e => { + alert('error'); + }); + }; + </script> +</mk-channel> + +<mk-channel-post> + <header> + <a class="index" @click="reply">{ post.index }:</a> + <a class="name" href={ _URL_ + '/@' + acct }><b>{ post.user.name }</b></a> + <mk-time time={ post.created_at }/> + <mk-time time={ post.created_at } mode="detail"/> + <span>ID:<i>{ acct }</i></span> + </header> + <div> + <a v-if="post.reply">>>{ post.reply.index }</a> + { post.text } + <div class="media" v-if="post.media"> + <template each={ file in post.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 '../../../../common/user/get-acct'; + + this.post = this.opts.post; + this.form = this.opts.form; + this.acct = getAcct(this.post.user); + + this.reply = () => { + this.form.update({ + reply: this.post + }); + }; + </script> +</mk-channel-post> + +<mk-channel-form> + <p v-if="reply"><b>>>{ reply.index }</b> ({ reply.user.name }): <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="post"> + <template v-if="!wait">%fa:paper-plane%</template>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<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"> + 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.post = () => { + this.update({ + wait: true + }); + + const files = this.files && this.files.length > 0 + ? this.files.map(f => f.id) + : undefined; + + this.$root.$data.os.api('posts/create', { + text: this.$refs.text.value == '' ? undefined : this.$refs.text.value, + media_ids: files, + reply_id: this.reply ? this.reply.id : undefined, + channel_id: 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()); + } + }); + }; + </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> diff --git a/src/server/web/app/ch/tags/header.tag b/src/server/web/app/ch/tags/header.tag new file mode 100644 index 0000000000..901123d63b --- /dev/null +++ b/src/server/web/app/ch/tags/header.tag @@ -0,0 +1,20 @@ +<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> diff --git a/src/server/web/app/ch/tags/index.tag b/src/server/web/app/ch/tags/index.tag new file mode 100644 index 0000000000..88df2ec45d --- /dev/null +++ b/src/server/web/app/ch/tags/index.tag @@ -0,0 +1,37 @@ +<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> diff --git a/src/server/web/app/ch/tags/index.ts b/src/server/web/app/ch/tags/index.ts new file mode 100644 index 0000000000..12ffdaeb84 --- /dev/null +++ b/src/server/web/app/ch/tags/index.ts @@ -0,0 +1,3 @@ +require('./index.tag'); +require('./channel.tag'); +require('./header.tag'); |