diff --git a/gulpfile.ts b/gulpfile.ts index a88c57e70d..85e5942401 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -17,7 +17,6 @@ import * as es from 'event-stream'; import stylus = require('gulp-stylus'); import cssnano = require('gulp-cssnano'); import * as uglify from 'gulp-uglify'; -import ls = require('browserify-livescript'); import riotify = require('riotify'); import transformify = require('syuilo-transformify'); import pug = require('gulp-pug'); @@ -169,7 +168,6 @@ gulp.task('build:client:scripts', () => new Promise(async (ok) => { browserify({ entries: [entry] }) - .transform(ls) .transform(transformify((source, file) => { return source .replace(/VERSION/g, `'${commit ? commit.hash : 'null'}'`) diff --git a/package.json b/package.json index 3c7cb95b66..de8aa794a0 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "bcryptjs": "2.4.3", "body-parser": "1.16.1", "browserify": "14.1.0", - "browserify-livescript": "0.2.3", "chai": "3.5.0", "chai-http": "3.0.0", "chalk": "1.1.3", diff --git a/src/web/app/boot.js b/src/web/app/boot.js index 32950071a2..b98602d402 100644 --- a/src/web/app/boot.js +++ b/src/web/app/boot.js @@ -5,10 +5,10 @@ const riot = require('riot'); require('velocity-animate'); const api = require('./common/scripts/api'); -const signout = require('./common/scripts/signout.ls'); -const generateDefaultUserdata = require('./common/scripts/generate-default-userdata.ls'); -const mixins = require('./common/mixins.ls'); -const checkForUpdate = require('./common/scripts/check-for-update.ls'); +const signout = require('./common/scripts/signout'); +const generateDefaultUserdata = require('./common/scripts/generate-default-userdata'); +const mixins = require('./common/mixins'); +const checkForUpdate = require('./common/scripts/check-for-update'); require('./common/tags'); /** diff --git a/src/web/app/common/mixins.js b/src/web/app/common/mixins.js new file mode 100644 index 0000000000..220e033846 --- /dev/null +++ b/src/web/app/common/mixins.js @@ -0,0 +1,48 @@ +const riot = require('riot'); + +module.exports = me => { + const i = me ? me.token : null; + + require('./scripts/i')(me); + + riot.mixin('api', { + api: require('./scripts/api').bind(null, i) + }); + + riot.mixin('cropper', { + Cropper: require('cropperjs') + }); + + riot.mixin('signout', { + signout: require('./scripts/signout') + }); + + riot.mixin('messaging-stream', { + MessagingStreamConnection: require('./scripts/messaging-stream') + }); + + riot.mixin('is-promise', { + isPromise: require('./scripts/is-promise') + }); + + riot.mixin('get-post-summary', { + getPostSummary: require('./scripts/get-post-summary') + }); + + riot.mixin('date-stringify', { + dateStringify: require('./scripts/date-stringify') + }); + + riot.mixin('text', { + analyze: require('../../../common/text/index'), + compile: require('./scripts/text-compiler') + }); + + riot.mixin('get-password-strength', { + getPasswordStrength: require('syuilo-password-strength') + }); + + riot.mixin('ui-progress', { + Progress: require('./scripts/loading') + }); +}; diff --git a/src/web/app/common/mixins.ls b/src/web/app/common/mixins.ls deleted file mode 100644 index 812bcae102..0000000000 --- a/src/web/app/common/mixins.ls +++ /dev/null @@ -1,37 +0,0 @@ -riot = require \riot - -module.exports = (me) ~> - i = if me? then me.token else null - - (require './scripts/i.ls') me - - riot.mixin \api do - api: (require './scripts/api').bind null i - - riot.mixin \cropper do - Cropper: require \cropperjs - - riot.mixin \signout do - signout: require './scripts/signout.ls' - - riot.mixin \messaging-stream do - MessagingStreamConnection: require './scripts/messaging-stream.ls' - - riot.mixin \is-promise do - is-promise: require './scripts/is-promise.ls' - - riot.mixin \get-post-summary do - get-post-summary: require './scripts/get-post-summary.ls' - - riot.mixin \date-stringify do - date-stringify: require './scripts/date-stringify.ls' - - riot.mixin \text do - analyze: require '../../../common/text/index.js' - compile: require './scripts/text-compiler.js' - - riot.mixin \get-password-strength do - get-password-strength: require 'syuilo-password-strength' - - riot.mixin \ui-progress do - Progress: require './scripts/loading.ls' diff --git a/src/web/app/common/scripts/check-for-update.js b/src/web/app/common/scripts/check-for-update.js new file mode 100644 index 0000000000..cd7279e3b8 --- /dev/null +++ b/src/web/app/common/scripts/check-for-update.js @@ -0,0 +1,11 @@ +module.exports = () => { + fetch('/api:meta').then(res => { + res.json().then(meta => { + if (meta.commit.hash !== VERSION) { + if (window.confirm('新しいMisskeyのバージョンがあります。更新しますか?\r\n(このメッセージが繰り返し表示される場合は、サーバーにデータがまだ届いていない可能性があるので、少し時間を置いてから再度お試しください)')) { + location.reload(true); + } + } + }); + }); +}; diff --git a/src/web/app/common/scripts/check-for-update.ls b/src/web/app/common/scripts/check-for-update.ls deleted file mode 100644 index 48e250a4c7..0000000000 --- a/src/web/app/common/scripts/check-for-update.ls +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = -> - fetch \/api:meta - .then (res) ~> - meta <~ res.json!.then - if meta.commit.hash != VERSION - if window.confirm '新しいMisskeyのバージョンがあります。更新しますか?\r\n(このメッセージが繰り返し表示される場合は、サーバーにデータがまだ届いていない可能性があるので、少し時間を置いてから再度お試しください)' - location.reload true - .catch ~> - # ignore diff --git a/src/web/app/common/scripts/date-stringify.js b/src/web/app/common/scripts/date-stringify.js new file mode 100644 index 0000000000..48e19704d5 --- /dev/null +++ b/src/web/app/common/scripts/date-stringify.js @@ -0,0 +1,13 @@ +module.exports = date => { + if (typeof date == 'string') date = new Date(date); + return ( + date.getFullYear() + '年' + + date.getMonth() + 1 + '月' + + date.getDate() + '日' + + ' ' + + date.getHours() + '時' + + date.getMinutes() + '分' + + ' ' + + `(${['日', '月', '火', '水', '木', '金', '土'][date.getDay()]})` + ); +}; diff --git a/src/web/app/common/scripts/date-stringify.ls b/src/web/app/common/scripts/date-stringify.ls deleted file mode 100644 index 7e85192ce7..0000000000 --- a/src/web/app/common/scripts/date-stringify.ls +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = (date) -> - if typeof date == \string then date = new Date date - - text = - date.get-full-year! + \年 + - date.get-month! + 1 + \月 + - date.get-date! + \日 + - ' ' + - date.get-hours! + \時 + - date.get-minutes! + \分 + - ' ' + - "(#{[\日 \月 \火 \水 \木 \金 \土][date.get-day!]})" - - return text diff --git a/src/web/app/common/scripts/generate-default-userdata.js b/src/web/app/common/scripts/generate-default-userdata.js new file mode 100644 index 0000000000..f6c8c2fe58 --- /dev/null +++ b/src/web/app/common/scripts/generate-default-userdata.js @@ -0,0 +1,47 @@ +const uuid = require('./uuid.js'); + +const home = { + left: [ + 'profile', + 'calendar', + 'rss-reader', + 'photo-stream' + ], + right: [ + 'broadcast', + 'notifications', + 'user-recommendation', + 'donation', + 'nav', + 'tips' + ] +}; + +module.exports = () => { + const homeData = []; + + home.left.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'left' + }); + }); + + home.right.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'right' + }); + }); + + const data = { + cache: true, + debug: false, + nya: true, + home: homeData + }; + + return data; +}; diff --git a/src/web/app/common/scripts/generate-default-userdata.ls b/src/web/app/common/scripts/generate-default-userdata.ls deleted file mode 100644 index c13d221bb9..0000000000 --- a/src/web/app/common/scripts/generate-default-userdata.ls +++ /dev/null @@ -1,28 +0,0 @@ -uuid = require './uuid.js' - -home = - left: [ \profile \calendar \rss-reader \photo-stream ] - right: [ \broadcast \notifications \user-recommendation \donation \nav \tips ] - -module.exports = ~> - home-data = [] - - home.left.for-each (widget) ~> - home-data.push do - name: widget - id: uuid! - place: \left - - home.right.for-each (widget) ~> - home-data.push do - name: widget - id: uuid! - place: \right - - data = - cache: true - debug: false - nya: true - home: home-data - - return data diff --git a/src/web/app/common/scripts/get-post-summary.js b/src/web/app/common/scripts/get-post-summary.js new file mode 100644 index 0000000000..8e17d54e81 --- /dev/null +++ b/src/web/app/common/scripts/get-post-summary.js @@ -0,0 +1,37 @@ +const getPostSummary = post => { + let = post.text ? post.text : ''; + + // メディアが添付されているとき + if (post.media) { + summary += ` (${post.media.length}つのメディア)`; + } + + // 投票が添付されているとき + if (post.poll) { + summary += ' (投票)'; + } + + // 返信のとき + if (post.reply_to_id) { + if (post.reply_to) { + replySummary = getPostSummary(post.reply_to); + summary += ` RE: ${replySummary}`; + } else { + summary += ' RE: ...'; + } + } + + // Repostのとき + if (post.repost_id) { + if (post.repost) { + repostSummary = getPostSummary(post.repost); + summary += ` RP: ${repostSummary}`; + } else { + summary += ' RP: ...'; + } + } + + return summary.trim(); +}; + +module.exports = getPostSummary; diff --git a/src/web/app/common/scripts/get-post-summary.ls b/src/web/app/common/scripts/get-post-summary.ls deleted file mode 100644 index 67178bc324..0000000000 --- a/src/web/app/common/scripts/get-post-summary.ls +++ /dev/null @@ -1,30 +0,0 @@ -get-post-summary = (post) ~> - summary = if post.text? then post.text else '' - - # メディアが添付されているとき - if post.media? - summary += " (#{post.media.length}つのメディア)" - - # 投票が添付されているとき - if post.poll? - summary += " (投票)" - - # 返信のとき - if post.reply_to_id? - if post.reply_to? - reply-summary = get-post-summary post.reply_to - summary += " RE: #{reply-summary}" - else - summary += " RE: ..." - - # Repostのとき - if post.repost_id? - if post.repost? - repost-summary = get-post-summary post.repost - summary += " RP: #{repost-summary}" - else - summary += " RP: ..." - - return summary.trim! - -module.exports = get-post-summary diff --git a/src/web/app/common/scripts/i.js b/src/web/app/common/scripts/i.js new file mode 100644 index 0000000000..66ce37d506 --- /dev/null +++ b/src/web/app/common/scripts/i.js @@ -0,0 +1,20 @@ +const riot = require('riot'); + +module.exports = me => { + riot.mixin('i', { + init: () => { + this.I = me; + this.SIGNIN = me != null; + + if (this.SIGNIN) { + this.on('mount', () => { + me.on('updated', this.update); + }); + this.on('unmount', () => { + me.off('updated', this.update); + }); + } + }, + me: me + }); +}; diff --git a/src/web/app/common/scripts/i.ls b/src/web/app/common/scripts/i.ls deleted file mode 100644 index 9b5fa87441..0000000000 --- a/src/web/app/common/scripts/i.ls +++ /dev/null @@ -1,13 +0,0 @@ -riot = require \riot - -module.exports = (me) -> - riot.mixin \i do - init: -> - @I = me - @SIGNIN = me? - - if @SIGNIN - @on \mount ~> me.on \updated @update - @on \unmount ~> me.off \updated @update - - me: me \ No newline at end of file diff --git a/src/web/app/common/scripts/is-promise.js b/src/web/app/common/scripts/is-promise.js new file mode 100644 index 0000000000..fd3dc42da3 --- /dev/null +++ b/src/web/app/common/scripts/is-promise.js @@ -0,0 +1 @@ +module.exports = x => typeof x.then == 'function'; diff --git a/src/web/app/common/scripts/is-promise.ls b/src/web/app/common/scripts/is-promise.ls deleted file mode 100644 index e3c7adff85..0000000000 --- a/src/web/app/common/scripts/is-promise.ls +++ /dev/null @@ -1 +0,0 @@ -module.exports = (x) -> typeof x.then == \function diff --git a/src/web/app/common/scripts/loading.js b/src/web/app/common/scripts/loading.js new file mode 100644 index 0000000000..fa7eafaf96 --- /dev/null +++ b/src/web/app/common/scripts/loading.js @@ -0,0 +1,21 @@ +const NProgress = require('nprogress'); +NProgress.configure({ + trickleSpeed: 500, + showSpinner: false +}); + +const root = document.getElementsByTagName('html')[0]; + +module.exports = { + start: () => { + root.classList.add('progress'); + NProgress.start(); + }, + done: () => { + root.classList.remove('progress'); + NProgress.done(); + }, + set: val => { + NProgress.set(val); + } +}; diff --git a/src/web/app/common/scripts/loading.ls b/src/web/app/common/scripts/loading.ls deleted file mode 100644 index 8ebede6db4..0000000000 --- a/src/web/app/common/scripts/loading.ls +++ /dev/null @@ -1,16 +0,0 @@ -NProgress = require \nprogress -NProgress.configure do - trickle-speed: 500ms - show-spinner: false - -root = document.get-elements-by-tag-name \html .0 - -module.exports = - start: ~> - root.class-list.add \progress - NProgress.start! - done: ~> - root.class-list.remove \progress - NProgress.done! - set: (val) ~> - NProgress.set val diff --git a/src/web/app/common/scripts/log.ls b/src/web/app/common/scripts/log.ls deleted file mode 100644 index 6e1e3735d8..0000000000 --- a/src/web/app/common/scripts/log.ls +++ /dev/null @@ -1,18 +0,0 @@ -riot = require \riot - -logs = [] - -ev = riot.observable! - -function log(msg) - logs.push do - date: new Date! - message: msg - ev.trigger \log - -riot.mixin \log do - logs: logs - log: log - log-event: ev - -module.exports = log diff --git a/src/web/app/common/scripts/messaging-stream.js b/src/web/app/common/scripts/messaging-stream.js new file mode 100644 index 0000000000..e6fc6f8bd0 --- /dev/null +++ b/src/web/app/common/scripts/messaging-stream.js @@ -0,0 +1,36 @@ +const ReconnectingWebSocket = require('reconnecting-websocket'); +const riot = require('riot'); + +class Connection { + constructor(me, otherparty) { + this.event = riot.observable(); + this.me = me; + + const host = CONFIG.api.url.replace('http', 'ws'); + this.socket = new ReconnectingWebSocket(`${host}/messaging?i=${me.token}&otherparty=${otherparty}`); + this.socket.addEventListener('open', this.onOpen); + this.socket.addEventListener('message', this.onMessage); + } + + onOpen() { + this.socket.send(JSON.stringify({ + i: this.me.token + })); + } + + onMessage(message) { + try { + const message = JSON.parse(message.data); + if (message.type) this.event.trigger(message.type, message.body); + } catch(e) { + // noop + } + } + + close() { + this.socket.removeEventListener('open', this.onOpen); + this.socket.removeEventListener('message', this.onMessage); + } +} + +module.exports = Connection; diff --git a/src/web/app/common/scripts/messaging-stream.ls b/src/web/app/common/scripts/messaging-stream.ls deleted file mode 100644 index ac3e74f1f5..0000000000 --- a/src/web/app/common/scripts/messaging-stream.ls +++ /dev/null @@ -1,34 +0,0 @@ -# Stream -#================================ - -ReconnectingWebSocket = require 'reconnecting-websocket' -riot = require 'riot' - -class Connection - (me, otherparty) ~> - @event = riot.observable! - @me = me - host = CONFIG.api.url.replace \http \ws - @socket = new ReconnectingWebSocket "#{host}/messaging?i=#{me.token}&otherparty=#{otherparty}" - - @socket.add-event-listener \open @on-open - @socket.add-event-listener \message @on-message - - on-open: ~> - @socket.send JSON.stringify do - i: @me.token - - on-message: (message) ~> - try - message = JSON.parse message.data - if message.type? - @event.trigger message.type, message.body - catch - # ignore - - close: ~> - @socket.remove-event-listener \open @on-open - @socket.remove-event-listener \message @on-message - @socket.close! - -module.exports = Connection diff --git a/src/web/app/common/scripts/signout.js b/src/web/app/common/scripts/signout.js new file mode 100644 index 0000000000..cd752423da --- /dev/null +++ b/src/web/app/common/scripts/signout.js @@ -0,0 +1,5 @@ +module.exports = () => { + localStorage.removeItem('me'); + document.cookie = `i=; domain=.${CONFIG.host}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`; + location.href = '/'; +}; diff --git a/src/web/app/common/scripts/signout.ls b/src/web/app/common/scripts/signout.ls deleted file mode 100644 index a647922678..0000000000 --- a/src/web/app/common/scripts/signout.ls +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = -> - local-storage.remove-item \me - document.cookie = "i=; domain=.#{CONFIG.host}; expires=Thu, 01 Jan 1970 00:00:01 GMT;" - location.href = \/ diff --git a/src/web/app/common/scripts/stream.js b/src/web/app/common/scripts/stream.js new file mode 100644 index 0000000000..b31e570444 --- /dev/null +++ b/src/web/app/common/scripts/stream.js @@ -0,0 +1,39 @@ +const ReconnectingWebSocket = require('reconnecting-websocket'); +const riot = require('riot'); + +module.exports = me => { + let state = 'initializing'; + const stateEv = riot.observable(); + const event = riot.observable(); + const host = CONFIG.api.url.replace('http', 'ws'); + const socket = new ReconnectingWebSocket(`${host}?i=${me.token}`); + + socket.onopen = () => { + state = 'connected'; + stateEv.trigger('connected'); + }; + + socket.onclose = () => { + state = 'reconnecting'; + stateEv.trigger('closed'); + }; + + socket.onmessage = message => { + try { + const message = JSON.parse(message.data); + if (message.type) { + event.trigger(message.type, message.body); + } + } catch (e) { + // noop + } + }; + + event.on('i_updated', me.update); + + return { + stateEv: stateEv, + getState: () => state, + event: event + }; +}; diff --git a/src/web/app/common/scripts/stream.ls b/src/web/app/common/scripts/stream.ls deleted file mode 100644 index c2c061603e..0000000000 --- a/src/web/app/common/scripts/stream.ls +++ /dev/null @@ -1,39 +0,0 @@ -# Stream -#================================ - -ReconnectingWebSocket = require \reconnecting-websocket -riot = require \riot - -module.exports = (me) ~> - state = \initializing - state-ev = riot.observable! - event = riot.observable! - - host = CONFIG.api.url.replace \http \ws - socket = new ReconnectingWebSocket "#{host}?i=#{me.token}" - - socket.onopen = ~> - state := \connected - state-ev.trigger \connected - - socket.onclose = ~> - state := \reconnecting - state-ev.trigger \closed - - socket.onmessage = (message) ~> - try - message = JSON.parse message.data - if message.type? - event.trigger message.type, message.body - catch - # ignore - - get-state = ~> state - - event.on \i_updated me.update - - { - state-ev - get-state - event - } diff --git a/src/web/app/desktop/mixins.js b/src/web/app/desktop/mixins.js new file mode 100644 index 0000000000..4a14b81763 --- /dev/null +++ b/src/web/app/desktop/mixins.js @@ -0,0 +1,42 @@ +const riot = require('riot'); + +module.exports = me => { + if (me) require('./scripts/stream')(me); + + require('./scripts/user-preview'); + require('./scripts/open-window'); + + riot.mixin('notify', { + notify: require('./scripts/notify') + }); + + const dialog = require('./scripts/dialog'); + + riot.mixin('dialog', { + dialog: dialog + }); + + riot.mixin('NotImplementedException', { + NotImplementedException: () => { + return dialog('Not implemented yet', '要求された操作は実装されていません。
Misskeyの開発に参加する', [{ + text: 'OK' + }]); + } + }); + + riot.mixin('input-dialog', { + inputDialog: require('./scripts/input-dialog') + }); + + riot.mixin('update-avatar', { + updateAvatar: require('./scripts/update-avatar') + }); + + riot.mixin('update-banner', { + updateBanner: require('./scripts/update-banner') + }); + + riot.mixin('autocomplete', { + Autocomplete: require('./scripts/autocomplete') + }); +}; diff --git a/src/web/app/desktop/mixins.ls b/src/web/app/desktop/mixins.ls deleted file mode 100644 index cb01001cb8..0000000000 --- a/src/web/app/desktop/mixins.ls +++ /dev/null @@ -1,41 +0,0 @@ -riot = require \riot - -module.exports = (me) ~> - if me? - (require './scripts/stream.ls') me - - require './scripts/user-preview.ls' - - require './scripts/open-window.ls' - - riot.mixin \notify do - notify: require './scripts/notify.ls' - - dialog = require './scripts/dialog.ls' - - riot.mixin \dialog do - dialog: dialog - - riot.mixin \NotImplementedException do - NotImplementedException: ~> - dialog do - 'Not implemented yet' - '要求された操作は実装されていません。
Misskeyの開発に参加する' - [ - text: \OK - ] - - riot.mixin \input-dialog do - input-dialog: require './scripts/input-dialog.ls' - - riot.mixin \update-avatar do - update-avatar: require './scripts/update-avatar.ls' - - riot.mixin \update-banner do - update-banner: require './scripts/update-banner.ls' - - riot.mixin \autocomplete do - Autocomplete: require './scripts/autocomplete.ls' - - riot.mixin \follow-scroll do - Follower: require './scripts/follow-scroll.ls' diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js new file mode 100644 index 0000000000..f4d2ec347a --- /dev/null +++ b/src/web/app/desktop/router.js @@ -0,0 +1,74 @@ +/** + * Desktop App Router + */ + +const riot = require('riot'); +const route = require('page'); +let page = null; + +module.exports = me => { + route('/', index); + route('/i>mentions', mentions); + route('/post::post', post); + route('/search::query', search); + route('/:user', user.bind(null, 'home')); + route('/:user/graphs', user.bind(null, 'graphs')); + route('/:user/:post', post); + route('*', notFound); + + function index() { + me ? home() : entrance(); + } + + function home() { + mount(document.createElement('mk-home-page')); + } + + function entrance() { + mount(document.createElement('mk-entrance')); + document.documentElement.setAttribute('data-page', 'entrance'); + } + + function mentions() { + const el = document.createElement('mk-home-page'); + el.setAttribute('mode', 'mentions'); + mount(el); + } + + function search(ctx) { + const el = document.createElement('mk-search-page'); + el.setAttribute('query', ctx.params.query); + mount(el); + } + + function user(page, ctx) { + const el = document.createElement('mk-user-page'); + el.setAttribute('user', ctx.params.user); + el.setAttribute('page', page); + mount(el); + } + + function post(ctx) { + const el = document.createElement('mk-post-page'); + el.setAttribute('post', ctx.params.post); + mount(el); + } + + function notFound() { + mount(document.createElement('mk-not-found')); + } + + riot.mixin('page', { + page: route + }); + + // EXEC + route(); +}; + +function mount(content) { + document.documentElement.removeAttribute('data-page'); + if (page) page.unmount(); + const body = document.getElementById('app'); + page = riot.mount(body.appendChild(content))[0]; +} diff --git a/src/web/app/desktop/router.ls b/src/web/app/desktop/router.ls deleted file mode 100644 index 02a7e11816..0000000000 --- a/src/web/app/desktop/router.ls +++ /dev/null @@ -1,77 +0,0 @@ -# Router -#================================ - -riot = require \riot -route = require \page -page = null - -module.exports = (me) ~> - - # Routing - #-------------------------------- - - route \/ index - route \/i>mentions mentions - route \/post::post post - route \/search::query search - route \/:user user.bind null \home - route \/:user/graphs user.bind null \graphs - route \/:user/:post post - route \* not-found - - # Handlers - #-------------------------------- - - function index - if me? then home! else entrance! - - function home - mount document.create-element \mk-home-page - - function entrance - mount document.create-element \mk-entrance - document.document-element.set-attribute \data-page \entrance - - function mentions - document.create-element \mk-home-page - ..set-attribute \mode \mentions - .. |> mount - - function search ctx - document.create-element \mk-search-page - ..set-attribute \query ctx.params.query - .. |> mount - - function user page, ctx - document.create-element \mk-user-page - ..set-attribute \user ctx.params.user - ..set-attribute \page page - .. |> mount - - function post ctx - document.create-element \mk-post-page - ..set-attribute \post ctx.params.post - .. |> mount - - function not-found - mount document.create-element \mk-not-found - - # Register mixin - #-------------------------------- - - riot.mixin \page do - page: route - - # Exec - #-------------------------------- - - route! - -# Mount -#================================ - -function mount content - document.document-element.remove-attribute \data-page - if page? then page.unmount! - body = document.get-element-by-id \app - page := riot.mount body.append-child content .0 diff --git a/src/web/app/desktop/script.js b/src/web/app/desktop/script.js index 845e6c4951..b240e9ab98 100644 --- a/src/web/app/desktop/script.js +++ b/src/web/app/desktop/script.js @@ -5,10 +5,10 @@ require('chart.js'); require('./tags'); const riot = require('riot'); -const boot = require('../boot.js'); -const mixins = require('./mixins.ls'); -const route = require('./router.ls'); -const fuckAdBlock = require('./scripts/fuck-ad-block.ls'); +const boot = require('../boot'); +const mixins = require('./mixins'); +const route = require('./router'); +const fuckAdBlock = require('./scripts/fuck-ad-block'); /** * Boot diff --git a/src/web/app/desktop/scripts/autocomplete.js b/src/web/app/desktop/scripts/autocomplete.js new file mode 100644 index 0000000000..54985874d6 --- /dev/null +++ b/src/web/app/desktop/scripts/autocomplete.js @@ -0,0 +1,124 @@ +const getCaretCoordinates = require('textarea-caret'); +const riot = require('riot'); + +/** + * オートコンプリートを管理するクラス。 + */ +class Autocomplete { + + /** + * 対象のテキストエリアを与えてインスタンスを初期化します。 + */ + constructor(textarea) { + this.suggestion = null; + this.textarea = textarea; + } + + /** + * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。 + */ + attach() { + this.textarea.addEventListener('input', this.onInput); + } + + /** + * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。 + */ + detach() { + this.textarea.removeEventListener('input', this.onInput); + this.close(); + } + + /** + * [Private] テキスト入力時 + */ + onInput() { + this.close(); + + const caret = this.textarea.selectionStart; + const text = this.textarea.value.substr(0, caret); + + const mentionIndex = text.lastIndexOf('@'); + + if (mentionIndex == -1) return; + + const username = text.substr(mentionIndex + 1); + + if (!username.match(/^[a-zA-Z0-9-]+$/)) return; + + this.open('user', username); + } + + /** + * [Private] サジェストを提示します。 + */ + open(type, q) { + // 既に開いているサジェストは閉じる + this.close(); + + // サジェスト要素作成 + const suggestion = document.createElement('mk-autocomplete-suggestion'); + + // ~ サジェストを表示すべき位置を計算 ~ + + const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); + + const rect = this.textarea.getBoundingClientRect(); + + const x = rect.left + window.pageXOffset + caretPosition.left; + const y = rect.top + window.pageYOffset + caretPosition.top; + + suggestion.style.left = x + 'px'; + suggestion.style.top = y + 'px'; + + // 要素追加 + const el = document.body.appendChild(suggestion); + + // マウント + this.suggestion = riot.mount(el, { + textarea: this.textarea, + complete: this.complete, + close: this.close, + type: type, + q: q + })[0]; + } + + /** + * [Private] サジェストを閉じます。 + */ + close() { + if (this.suggestion == nul) return; + + this.suggestion.unmount(); + this.suggestion = null; + + this.textarea.focus(); + } + + /** + * [Private] オートコンプリートする + */ + complete(user) { + this.close(); + + const value = user.username; + + const caret = this.textarea.selectionStart; + const source = this.textarea.value; + + const before = source.substr(0, caret); + const trimedBefore = before.substring(0, before.lastIndexOf('@')); + const after = source.substr(caret); + + // 結果を挿入する + this.textarea.value = trimedBefore + '@' + value + ' ' + after; + + // キャレットを戻す + this.textarea.focus(); + const pos = caret + value.length; + this.textarea.setSelectionRange(pos, pos); + } +} + +module.exports = Autocomplete; diff --git a/src/web/app/desktop/scripts/autocomplete.ls b/src/web/app/desktop/scripts/autocomplete.ls deleted file mode 100644 index 391fb312e3..0000000000 --- a/src/web/app/desktop/scripts/autocomplete.ls +++ /dev/null @@ -1,108 +0,0 @@ -# Autocomplete -#================================ - -get-caret-coordinates = require 'textarea-caret' -riot = require 'riot' - -# オートコンプリートを管理するクラスです。 -class Autocomplete - - @textarea = null - @suggestion = null - - # 対象のテキストエリアを与えてインスタンスを初期化します。 - (textarea) ~> - @textarea = textarea - - # このインスタンスにあるテキストエリアの入力のキャプチャを開始します。 - attach: ~> - @textarea.add-event-listener \input @on-input - - # このインスタンスにあるテキストエリアの入力のキャプチャを解除します。 - detach: ~> - @textarea.remove-event-listener \input @on-input - @close! - - # テキスト入力時 - on-input: ~> - @close! - - caret = @textarea.selection-start - text = @textarea.value.substr 0 caret - - mention-index = text.last-index-of \@ - - if mention-index == -1 - return - - username = text.substr mention-index + 1 - - if not username.match /^[a-zA-Z0-9-]+$/ - return - - @open \user username - - # サジェストを提示します。 - open: (type, q) ~> - # 既に開いているサジェストは閉じる - @close! - - # サジェスト要素作成 - suggestion = document.create-element \mk-autocomplete-suggestion - - # ~ サジェストを表示すべき位置を計算 ~ - - caret-position = get-caret-coordinates @textarea, @textarea.selection-start - - rect = @textarea.get-bounding-client-rect! - - x = rect.left + window.page-x-offset + caret-position.left - y = rect.top + window.page-y-offset + caret-position.top - - suggestion.style.left = x + \px - suggestion.style.top = y + \px - - # 要素追加 - el = document.body.append-child suggestion - - # マウント - mounted = riot.mount el, do - textarea: @textarea - complete: @complete - close: @close - type: type - q: q - - @suggestion = mounted.0 - - # サジェストを閉じます。 - close: ~> - if !@suggestion? - return - - @suggestion.unmount! - @suggestion = null - - @textarea.focus! - - # オートコンプリートする - complete: (user) ~> - @close! - value = user.username - - caret = @textarea.selection-start - source = @textarea.value - - before = source.substr 0 caret - trimed-before = before.substring 0 before.last-index-of \@ - after = source.substr caret - - # 結果を挿入する - @textarea.value = trimed-before + \@ + value + ' ' + after - - # キャレットを戻す - @textarea.focus! - pos = caret + value.length - @textarea.set-selection-range pos, pos - -module.exports = Autocomplete diff --git a/src/web/app/desktop/scripts/dialog.js b/src/web/app/desktop/scripts/dialog.js new file mode 100644 index 0000000000..6fe7b6e8d7 --- /dev/null +++ b/src/web/app/desktop/scripts/dialog.js @@ -0,0 +1,16 @@ +const riot = require('riot'); + +module.exports = (title, text, buttons, canThrough, onThrough) => { + const dialog = document.body.appendChild(document.createElement('mk-dialog')); + const controller = riot.observable(); + riot.mount(dialog, { + controller: controller, + title: title, + text: text, + buttons: buttons, + canThrough: canThrough, + onThrough: onThrough + }); + controller.trigger('open'); + return controller; +}; diff --git a/src/web/app/desktop/scripts/dialog.ls b/src/web/app/desktop/scripts/dialog.ls deleted file mode 100644 index f3dd6cea1b..0000000000 --- a/src/web/app/desktop/scripts/dialog.ls +++ /dev/null @@ -1,17 +0,0 @@ -# Dialog -#================================ - -riot = require 'riot' - -module.exports = (title, text, buttons, can-through, on-through) ~> - dialog = document.body.append-child document.create-element \mk-dialog - controller = riot.observable! - riot.mount dialog, do - controller: controller - title: title - text: text - buttons: buttons - can-through: can-through - on-through: on-through - controller.trigger \open - return controller diff --git a/src/web/app/desktop/scripts/follow-scroll.ls b/src/web/app/desktop/scripts/follow-scroll.ls deleted file mode 100644 index 5072e9c583..0000000000 --- a/src/web/app/desktop/scripts/follow-scroll.ls +++ /dev/null @@ -1,56 +0,0 @@ -class Follower - (el) -> - @follower = el - @last-scroll-top = window.scroll-y - @initial-follower-top = @follower.get-bounding-client-rect!.top - @page-top = 48 - - follow: -> - window-height = window.inner-height - follower-height = @follower.offset-height - - scroll-top = window.scroll-y - scroll-bottom = scroll-top + window-height - - follower-top = @follower.get-bounding-client-rect!.top + scroll-top - follower-bottom = follower-top + follower-height - - height-delta = Math.abs window-height - follower-height - scroll-delta = @last-scroll-top - scroll-top - - is-scrolling-down = (scroll-top > @last-scroll-top) - is-window-larger = (window-height > follower-height) - - console.log @initial-follower-top - - if (is-window-larger && scroll-top > @initial-follower-top) || (!is-window-larger && scroll-top > @initial-follower-top + height-delta) - @follower.class-list.add \fixed - else if !is-scrolling-down && scroll-top + @page-top <= @initial-follower-top - @follower.class-list.remove \fixed - @follower.style.top = 0 - return - - drag-bottom-down = (follower-bottom <= scroll-bottom && is-scrolling-down) - drag-top-up = (follower-top >= scroll-top + @page-top && !is-scrolling-down) - - if drag-bottom-down - console.log \down - @follower.style.top = if is-window-larger then 0 else -height-delta + \px - else if drag-top-up - console.log \up - @follower.style.top = @page-top + \px - else if @follower.class-list.contains \fixed - console.log \- - current-top = parse-int @follower.style.top, 10 - - min-top = -height-delta - scrolled-top = current-top + scroll-delta - - is-page-at-bottom = (scroll-top + window-height >= document.body.offset-height) - new-top = if is-page-at-bottom then min-top else scrolled-top - - @follower.style.top = new-top + \px - - @last-scroll-top = scroll-top - -module.exports = Follower diff --git a/src/web/app/desktop/scripts/fuck-ad-block.js b/src/web/app/desktop/scripts/fuck-ad-block.js new file mode 100644 index 0000000000..38208d34c0 --- /dev/null +++ b/src/web/app/desktop/scripts/fuck-ad-block.js @@ -0,0 +1,18 @@ +require('fuckadblock'); +const dialog = require('./dialog'); + +module.exports = () => { + if (fuckAdBlock === undefined) { + adBlockDetected(); + } else { + fuckAdBlock.onDetected(adBlockDetected); + } +}; + +function adBlockDetected() { + dialog('広告ブロッカーを無効にしてください', + 'Misskeyは広告を掲載していませんが、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。', + [{ + text: 'OK' + }]); +} diff --git a/src/web/app/desktop/scripts/fuck-ad-block.ls b/src/web/app/desktop/scripts/fuck-ad-block.ls deleted file mode 100644 index 7990f58f1f..0000000000 --- a/src/web/app/desktop/scripts/fuck-ad-block.ls +++ /dev/null @@ -1,19 +0,0 @@ -# FUCK AD BLOCK -#================================ - -require \fuckadblock -dialog = require './dialog.ls' - -module.exports = ~> - if fuck-ad-block == undefined - ad-block-detected! - else - fuck-ad-block.on-detected ad-block-detected - -function ad-block-detected - dialog do - '広告ブロッカーを無効にしてください' - 'Misskeyは広告を掲載していませんが、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。' - [ - text: \OK - ] diff --git a/src/web/app/desktop/scripts/input-dialog.js b/src/web/app/desktop/scripts/input-dialog.js new file mode 100644 index 0000000000..ab9c57401f --- /dev/null +++ b/src/web/app/desktop/scripts/input-dialog.js @@ -0,0 +1,12 @@ +const riot = require('riot'); + +module.exports = (title, placeholder, defaultValue, onOk, onCancel) => { + const dialog = document.body.appendChild(document.createElement('mk-input-dialog')); + return riot.mount(dialog, { + title: title, + placeholder: placeholder, + 'default': defaultValue, + onOk: onOk, + onCancel: onCancel + }); +}; diff --git a/src/web/app/desktop/scripts/input-dialog.ls b/src/web/app/desktop/scripts/input-dialog.ls deleted file mode 100644 index f75b12dd01..0000000000 --- a/src/web/app/desktop/scripts/input-dialog.ls +++ /dev/null @@ -1,13 +0,0 @@ -# Input Dialog -#================================ - -riot = require 'riot' - -module.exports = (title, placeholder, default-value, on-ok, on-cancel) ~> - dialog = document.body.append-child document.create-element \mk-input-dialog - riot.mount dialog, do - title: title - placeholder: placeholder - default: default-value - on-ok: on-ok - on-cancel: on-cancel diff --git a/src/web/app/desktop/scripts/notify.js b/src/web/app/desktop/scripts/notify.js new file mode 100644 index 0000000000..048f22aea7 --- /dev/null +++ b/src/web/app/desktop/scripts/notify.js @@ -0,0 +1,8 @@ +const riot = require('riot'); + +module.exports = message => { + const notification = document.body.appendChild(document.createElement('mk-ui-notification')); + riot.mount(notification, { + message: message + }); +}; diff --git a/src/web/app/desktop/scripts/notify.ls b/src/web/app/desktop/scripts/notify.ls deleted file mode 100644 index 919bbc3dcf..0000000000 --- a/src/web/app/desktop/scripts/notify.ls +++ /dev/null @@ -1,6 +0,0 @@ -riot = require \riot - -module.exports = (message) ~> - notification = document.body.append-child document.create-element \mk-ui-notification - riot.mount notification, do - message: message diff --git a/src/web/app/desktop/scripts/open-window.js b/src/web/app/desktop/scripts/open-window.js new file mode 100644 index 0000000000..3f7cc424e0 --- /dev/null +++ b/src/web/app/desktop/scripts/open-window.js @@ -0,0 +1,8 @@ +const riot = require('riot'); + +riot.mixin('open-window', { + openWindow: (name, opts) => { + const window = document.body.appendChild(document.createElement(name)); + return riot.mount(window, opts)[0]; + } +}); diff --git a/src/web/app/desktop/scripts/open-window.ls b/src/web/app/desktop/scripts/open-window.ls deleted file mode 100644 index 4388272ecf..0000000000 --- a/src/web/app/desktop/scripts/open-window.ls +++ /dev/null @@ -1,8 +0,0 @@ -riot = require \riot - -function open(name, opts) - window = document.body.append-child document.create-element name - riot.mount window, opts - -riot.mixin \open-window do - open-window: open diff --git a/src/web/app/desktop/scripts/stream.js b/src/web/app/desktop/scripts/stream.js new file mode 100644 index 0000000000..ea1548ecfd --- /dev/null +++ b/src/web/app/desktop/scripts/stream.js @@ -0,0 +1,45 @@ +const stream = require('../../common/scripts/stream'); +const getPostSummary = require('../../common/scripts/get-post-summary'); +const riot = require('riot'); + +module.exports = me => { + const s = stream(me); + + s.event.on('drive_file_created', file => { + const n = new Notification('ファイルがアップロードされました', { + body: file.name, + icon: file.url + '?thumbnail&size=64' + }); + setTimeout(n.close.bind(n), 5000); + }); + + s.event.on('mention', post => { + const n = new Notification(post.user.name + "さんから:", { + body: getPostSummary(post), + icon: post.user.avatar_url + '?thumbnail&size=64' + }); + setTimeout(n.close.bind(n), 6000); + }); + + s.event.on('reply', post => { + const n = new Notification(post.user.name + "さんから返信:", { + body: getPostSummary(post), + icon: post.user.avatar_url + '?thumbnail&size=64' + }); + setTimeout(n.close.bind(n), 6000); + }); + + s.event.on('quote', post => { + const n = new Notification(post.user.name + "さんが引用:", { + body: getPostSummary(post), + icon: post.user.avatar_url + '?thumbnail&size=64' + }); + setTimeout(n.close.bind(n), 6000); + }); + + riot.mixin('stream', { + stream: s.event, + getStreamState: s.getState, + streamStateEv: s.stateEv + }); +}; diff --git a/src/web/app/desktop/scripts/stream.ls b/src/web/app/desktop/scripts/stream.ls deleted file mode 100644 index f84d6097a7..0000000000 --- a/src/web/app/desktop/scripts/stream.ls +++ /dev/null @@ -1,38 +0,0 @@ -# Stream -#================================ - -stream = require '../../common/scripts/stream.ls' -get-post-summary = require '../../common/scripts/get-post-summary.ls' -riot = require \riot - -module.exports = (me) ~> - s = stream me - - s.event.on \drive_file_created (file) ~> - n = new Notification 'ファイルがアップロードされました' do - body: file.name - icon: file.url + '?thumbnail&size=64' - set-timeout (n.close.bind n), 5000ms - - s.event.on \mention (post) ~> - n = new Notification "#{post.user.name}さんから:" do - body: get-post-summary post - icon: post.user.avatar_url + '?thumbnail&size=64' - set-timeout (n.close.bind n), 6000ms - - s.event.on \reply (post) ~> - n = new Notification "#{post.user.name}さんから返信:" do - body: get-post-summary post - icon: post.user.avatar_url + '?thumbnail&size=64' - set-timeout (n.close.bind n), 6000ms - - s.event.on \quote (post) ~> - n = new Notification "#{post.user.name}さんが引用:" do - body: get-post-summary post - icon: post.user.avatar_url + '?thumbnail&size=64' - set-timeout (n.close.bind n), 6000ms - - riot.mixin \stream do - stream: s.event - get-stream-state: s.get-state - stream-state-ev: s.state-ev diff --git a/src/web/app/desktop/scripts/update-avatar.js b/src/web/app/desktop/scripts/update-avatar.js new file mode 100644 index 0000000000..ad971f1b11 --- /dev/null +++ b/src/web/app/desktop/scripts/update-avatar.js @@ -0,0 +1,86 @@ +const riot = require('riot'); +const dialog = require('./dialog'); +const api = require('../../common/scripts/api'); + +module.exports = (I, cb, file = null) => { + const fileSelected = file => { + const cropper = riot.mount(document.body.appendChild(document.createElement('mk-crop-window')), { + file: file, + title: 'アバターとして表示する部分を選択', + aspectRatio: 1 / 1 + })[0]; + + cropper.on('cropped', blob => { + const data = new FormData(); + data.append('i', I.token); + data.append('file', blob, file.name + '.cropped.png'); + + api(I, 'drive/folders/find', { + name: 'アイコン' + }).then(iconFolder => { + if (iconFolder.length === 0) { + api(I, 'drive/folders/create', { + name: 'アイコン' + }).then(iconFolder => { + uplaod(data, iconFolder); + }); + } else { + uplaod(data, iconFolder[0]); + } + }); + }); + + cropper.on('skiped', () => { + set(file); + }); + }; + + const uplaod = (data, folder) => { + const progress = riot.mount(document.body.appendChild(document.createElement('mk-progress-dialog')), { + title: '新しいアバターをアップロードしています' + })[0]; + + if (folder) data.append('folder_id', folder.id); + + const xhr = new XMLHttpRequest(); + xhr.open('POST', CONFIG.api.url + '/drive/files/create', true); + xhr.onload = e => { + const file = JSON.parse(e.target.response); + progress.close(); + set(file); + }; + + xhr.upload.onprogress = e => { + if (e.lengthComputable) progress.updateProgress(e.loaded, e.total); + }; + + xhr.send(data); + }; + + const set = file => { + api(I, 'i/update', { + avatar_id: file.id + }).then(i => { + dialog('アバターを更新しました', + '新しいアバターが反映されるまで時間がかかる場合があります。', + [{ + text: 'わかった' + }]); + + if (cb) cb(i); + }); + }; + + if (file) { + fileSelected(file); + } else { + const browser = riot.mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), { + multiple: false, + title: 'アバターにする画像を選択' + })[0]; + + browser.one('selected', file => { + fileSelected(file); + }); + } +}; diff --git a/src/web/app/desktop/scripts/update-avatar.ls b/src/web/app/desktop/scripts/update-avatar.ls deleted file mode 100644 index 351e54fe51..0000000000 --- a/src/web/app/desktop/scripts/update-avatar.ls +++ /dev/null @@ -1,81 +0,0 @@ -# Update Avatar -#================================ - -riot = require 'riot' -dialog = require './dialog.ls' -api = require '../../common/scripts/api' - -module.exports = (I, cb, file = null) ~> - - @file-selected = (file) ~> - cropper = document.body.append-child document.create-element \mk-crop-window - cropper = riot.mount cropper, do - file: file - title: 'アバターとして表示する部分を選択' - aspect-ratio: 1 / 1 - .0 - cropper.on \cropped (blob) ~> - data = new FormData! - data.append \i I.token - data.append \file blob, file.name + '.cropped.png' - api I, \drive/folders/find do - name: 'アイコン' - .then (icon-folder) ~> - if icon-folder.length == 0 - api I, \drive/folders/create do - name: 'アイコン' - .then (icon-folder) ~> - @uplaod data, icon-folder - else - @uplaod data, icon-folder.0 - cropper.on \skiped ~> - @set file - - @uplaod = (data, folder) ~> - - progress = document.body.append-child document.create-element \mk-progress-dialog - progress = riot.mount progress, do - title: '新しいアバターをアップロードしています' - .0 - - if folder? - data.append \folder_id folder.id - - xhr = new XMLHttpRequest! - xhr.open \POST CONFIG.api.url + \/drive/files/create true - xhr.onload = (e) ~> - file = JSON.parse e.target.response - progress.close! - @set file - - xhr.upload.onprogress = (e) ~> - if e.length-computable - progress.update-progress e.loaded, e.total - - xhr.send data - - @set = (file) ~> - api I, \i/update do - avatar_id: file.id - .then (i) ~> - dialog do - 'アバターを更新しました' - '新しいアバターが反映されるまで時間がかかる場合があります。' - [ - text: \わかった - ] - if cb? then cb i - .catch (err) ~> - console.error err - #@opts.ui.trigger \notification 'Error!' - - if file? - @file-selected file - else - browser = document.body.append-child document.create-element \mk-select-file-from-drive-window - browser = riot.mount browser, do - multiple: false - title: 'アバターにする画像を選択' - .0 - browser.one \selected (file) ~> - @file-selected file diff --git a/src/web/app/desktop/scripts/update-banner.js b/src/web/app/desktop/scripts/update-banner.js new file mode 100644 index 0000000000..9242adcea5 --- /dev/null +++ b/src/web/app/desktop/scripts/update-banner.js @@ -0,0 +1,86 @@ +const riot = require('riot'); +const dialog = require('./dialog'); +const api = require('../../common/scripts/api'); + +module.exports = (I, cb, file = null) => { + const fileSelected = file => { + const cropper = riot.mount(document.body.appendChild(document.createElement('mk-crop-window')), { + file: file, + title: 'バナーとして表示する部分を選択', + aspectRatio: 16 / 9 + })[0]; + + cropper.on('cropped', blob => { + const data = new FormData(); + data.append('i', I.token); + data.append('file', blob, file.name + '.cropped.png'); + + api(I, 'drive/folders/find', { + name: 'バナー' + }).then(iconFolder => { + if (iconFolder.length === 0) { + api(I, 'drive/folders/create', { + name: 'バナー' + }).then(iconFolder => { + uplaod(data, iconFolder); + }); + } else { + uplaod(data, iconFolder[0]); + } + }); + }); + + cropper.on('skiped', () => { + set(file); + }); + }; + + const uplaod = (data, folder) => { + const progress = riot.mount(document.body.appendChild(document.createElement('mk-progress-dialog')), { + title: '新しいバナーをアップロードしています' + })[0]; + + if (folder) data.append('folder_id', folder.id); + + const xhr = new XMLHttpRequest(); + xhr.open('POST', CONFIG.api.url + '/drive/files/create', true); + xhr.onload = e => { + const file = JSON.parse(e.target.response); + progress.close(); + set(file); + }; + + xhr.upload.onprogress = e => { + if (e.lengthComputable) progress.updateProgress(e.loaded, e.total); + }; + + xhr.send(data); + }; + + const set = file => { + api(I, 'i/update', { + banner_id: file.id + }).then(i => { + dialog('バナーを更新しました', + '新しいバナーが反映されるまで時間がかかる場合があります。', + [{ + text: 'わかりました。' + }]); + + if (cb) cb(i); + }); + }; + + if (file) { + fileSelected(file); + } else { + const browser = riot.mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), { + multiple: false, + title: 'バナーにする画像を選択' + })[0]; + + browser.one('selected', file => { + fileSelected(file); + }); + } +}; diff --git a/src/web/app/desktop/scripts/update-banner.ls b/src/web/app/desktop/scripts/update-banner.ls deleted file mode 100644 index 2417b8ab2a..0000000000 --- a/src/web/app/desktop/scripts/update-banner.ls +++ /dev/null @@ -1,81 +0,0 @@ -# Update Banner -#================================ - -riot = require 'riot' -dialog = require './dialog.ls' -api = require '../../common/scripts/api' - -module.exports = (I, cb, file = null) ~> - - @file-selected = (file) ~> - cropper = document.body.append-child document.create-element \mk-crop-window - cropper = riot.mount cropper, do - file: file - title: 'バナーとして表示する部分を選択' - aspect-ratio: 16 / 9 - .0 - cropper.on \cropped (blob) ~> - data = new FormData! - data.append \i I.token - data.append \file blob, file.name + '.cropped.png' - api I, \drive/folders/find do - name: 'バナー' - .then (banner-folder) ~> - if banner-folder.length == 0 - api I, \drive/folders/create do - name: 'バナー' - .then (banner-folder) ~> - @uplaod data, banner-folder - else - @uplaod data, banner-folder.0 - cropper.on \skiped ~> - @set file - - @uplaod = (data, folder) ~> - - progress = document.body.append-child document.create-element \mk-progress-dialog - progress = riot.mount progress, do - title: '新しいバナーをアップロードしています' - .0 - - if folder? - data.append \folder_id folder.id - - xhr = new XMLHttpRequest! - xhr.open \POST CONFIG.api.url + \/drive/files/create true - xhr.onload = (e) ~> - file = JSON.parse e.target.response - progress.close! - @set file - - xhr.upload.onprogress = (e) ~> - if e.length-computable - progress.update-progress e.loaded, e.total - - xhr.send data - - @set = (file) ~> - api I, \i/update do - banner_id: file.id - .then (i) ~> - dialog do - 'バナーを更新しました' - '新しいバナーが反映されるまで時間がかかる場合があります。' - [ - text: \わかりました。 - ] - if cb? then cb i - .catch (err) ~> - console.error err - #@opts.ui.trigger \notification 'Error!' - - if file? - @file-selected file - else - browser = document.body.append-child document.create-element \mk-select-file-from-drive-window - browser = riot.mount browser, do - multiple: false - title: 'バナーにする画像を選択' - .0 - browser.one \selected (file) ~> - @file-selected file diff --git a/src/web/app/desktop/scripts/user-preview.js b/src/web/app/desktop/scripts/user-preview.js new file mode 100644 index 0000000000..8351f0e75a --- /dev/null +++ b/src/web/app/desktop/scripts/user-preview.js @@ -0,0 +1,66 @@ +const riot = require('riot'); + +riot.mixin('user-preview', { + init: () => { + const scan = () => { + this.root.querySelectorAll('[data-user-preview]:not([data-user-preview-attached])') + .forEach(attach.bind(this)); + }; + this.on('mount', scan); + this.on('updated', scan); + } +}); + +function attach(el) { + el.setAttribute('data-user-preview-attached', true); + + const user = el.getAttribute('data-user-preview'); + let tag = null; + let showTimer = null; + let hideTimer = null; + + el.addEventListener('mouseover', () => { + clearTimeout(showTimer); + clearTimeout(hideTimer); + showTimer = setTimeout(show, 500); + }); + + el.addEventListener('mouseleave', () => { + clearTimeout(showTimer); + clearTimeout(hideTimer); + hideTimer = setTimeout(close, 500); + }); + + this.on('unmount', () => { + clearTimeout(showTimer); + clearTimeout(hideTimer); + close(); + }); + + const show = () => { + if (tag) return; + const preview = document.createElement('mk-user-preview'); + const rect = el.getBoundingClientRect(); + const x = rect.left + el.offsetWidth + window.pageXOffset; + const y = rect.top + window.pageYOffset; + preview.style.top = y + 'px'; + preview.style.left = x + 'px'; + preview.addEventListener('mouseover', () => { + clearTimeout(hideTimer); + }); + preview.addEventListener('mouseleave', () => { + clearTimeout(showTimer); + hideTimer = setTimeout(close, 500); + }); + tag = riot.mount(document.body.appendChild(preview), { + user: user + })[0]; + }; + + const close = () => { + if (tag) { + tag.close(); + tag = null; + } + }; +} diff --git a/src/web/app/desktop/scripts/user-preview.ls b/src/web/app/desktop/scripts/user-preview.ls deleted file mode 100644 index 0c5a67aedb..0000000000 --- a/src/web/app/desktop/scripts/user-preview.ls +++ /dev/null @@ -1,74 +0,0 @@ -# User Preview -#================================ - -riot = require \riot - -riot.mixin \user-preview do - init: -> - @on \mount ~> - scan.call @ - @on \updated ~> - scan.call @ - - function scan - elems = @root.query-selector-all '[data-user-preview]:not([data-user-preview-attached])' - elems.for-each attach.bind @ - -function attach el - el.set-attribute \data-user-preview-attached true - user = el.get-attribute \data-user-preview - - tag = null - - show-timer = null - hide-timer = null - - el.add-event-listener \mouseover ~> - clear-timeout show-timer - clear-timeout hide-timer - show-timer := set-timeout ~> - show! - , 500ms - - el.add-event-listener \mouseleave ~> - clear-timeout show-timer - clear-timeout hide-timer - hide-timer := set-timeout ~> - close! - , 500ms - - @on \unmount ~> - clear-timeout show-timer - clear-timeout hide-timer - close! - - function show - if tag? - return - - preview = document.create-element \mk-user-preview - - rect = el.get-bounding-client-rect! - x = rect.left + el.offset-width + window.page-x-offset - y = rect.top + window.page-y-offset - - preview.style.top = y + \px - preview.style.left = x + \px - - preview.add-event-listener \mouseover ~> - clear-timeout hide-timer - - preview.add-event-listener \mouseleave ~> - clear-timeout show-timer - hide-timer := set-timeout ~> - close! - , 500ms - - tag := riot.mount (document.body.append-child preview), do - user: user - .0 - - function close - if tag? - tag.close! - tag := null diff --git a/src/web/app/dev/router.js b/src/web/app/dev/router.js new file mode 100644 index 0000000000..71c098463b --- /dev/null +++ b/src/web/app/dev/router.js @@ -0,0 +1,42 @@ +const route = require('page'); +let page = null; + +module.exports = me => { + route('/', index); + route('/apps', apps); + route('/app/new', newApp); + route('/app/:app', app); + route('*', notFound); + + function index() { + mount(document.createElement('mk-index')); + } + + function apps() { + mount(document.createElement('mk-apps-page')); + } + + function newApp() { + mount(document.createElement('mk-new-app-page')); + } + + function app(ctx) { + const el = document.createElement('mk-app-page'); + el.setAttribute('app', ctx.params.app); + mount(el); + } + + function notFound() { + mount(document.createElement('mk-not-found')); + } + + // EXEC + route(); +}; + +const riot = require('riot'); +function mount(content) { + if (page) page.unmount(); + const body = document.getElementById('app'); + page = riot.mount(body.appendChild(content))[0]; +} diff --git a/src/web/app/dev/router.ls b/src/web/app/dev/router.ls deleted file mode 100644 index ac408b36ed..0000000000 --- a/src/web/app/dev/router.ls +++ /dev/null @@ -1,51 +0,0 @@ -# Router -#================================ - -route = require \page -page = null - -module.exports = (me) ~> - - # Routing - #-------------------------------- - - route \/ index - route \/apps apps - route \/app/new new-app - route \/app/:app app - route \* not-found - - # Handlers - #-------------------------------- - - function index - mount document.create-element \mk-index - - function apps - mount document.create-element \mk-apps-page - - function new-app - mount document.create-element \mk-new-app-page - - function app ctx - document.create-element \mk-app-page - ..set-attribute \app ctx.params.app - .. |> mount - - function not-found - mount document.create-element \mk-not-found - - # Exec - #-------------------------------- - - route! - -# Mount -#================================ - -riot = require \riot - -function mount content - if page? then page.unmount! - body = document.get-element-by-id \app - page := riot.mount body.append-child content .0 diff --git a/src/web/app/dev/script.js b/src/web/app/dev/script.js index c9c5ee51e5..e0644c263b 100644 --- a/src/web/app/dev/script.js +++ b/src/web/app/dev/script.js @@ -3,8 +3,8 @@ */ require('./tags'); -const boot = require('../boot.js'); -const route = require('./router.ls'); +const boot = require('../boot'); +const route = require('./router'); /** * Boot diff --git a/src/web/app/mobile/mixins.js b/src/web/app/mobile/mixins.js new file mode 100644 index 0000000000..98601a1072 --- /dev/null +++ b/src/web/app/mobile/mixins.js @@ -0,0 +1,25 @@ +const riot = require('riot'); + +module.exports = me => { + if (me) { + require('./scripts/stream')(me); + } + + require('./scripts/ui'); + + riot.mixin('open-post-form', { + openPostForm: opts => { + const app = document.getElementById('app'); + app.style.display = 'none'; + + function recover() { + app.style.display = 'block'; + } + + const form = riot.mount(document.body.appendChild(document.createElement('mk-post-form')), opts)[0]; + form + .on('cancel', recover) + .on('post', recover); + } + }); +}; diff --git a/src/web/app/mobile/mixins.ls b/src/web/app/mobile/mixins.ls deleted file mode 100644 index 902774f91a..0000000000 --- a/src/web/app/mobile/mixins.ls +++ /dev/null @@ -1,19 +0,0 @@ -riot = require \riot - -module.exports = (me) ~> - if me? - (require './scripts/stream.ls') me - - require './scripts/ui.ls' - - riot.mixin \open-post-form do - open-post-form: (opts) -> - app = document.get-element-by-id \app - app.style.display = \none - form = document.body.append-child document.create-element \mk-post-form - form = riot.mount form, opts .0 - form.on \cancel recover - form.on \post recover - - function recover - app.style.display = \block diff --git a/src/web/app/mobile/router.js b/src/web/app/mobile/router.js new file mode 100644 index 0000000000..df4871f292 --- /dev/null +++ b/src/web/app/mobile/router.js @@ -0,0 +1,136 @@ +/** + * Mobile App Router + */ + +const riot = require('riot'); +const route = require('page'); +let page = null; + +module.exports = me => { + route('/', index); + route('/i/notifications', notifications); + route('/i/messaging', messaging); + route('/i/messaging/:username', messaging); + route('/i/drive', drive); + route('/i/drive/folder/:folder', drive); + route('/i/drive/file/:file', drive); + route('/i/settings', settings); + route('/i/settings/signin-history', settingsSignin); + route('/i/settings/api', settingsApi); + route('/i/settings/twitter', settingsTwitter); + route('/i/settings/authorized-apps', settingsAuthorizedApps); + route('/post/new', newPost); + route('/post::post', post); + route('/search::query', search); + route('/:user', user.bind(null, 'posts')); + route('/:user/graphs', user.bind(null, 'graphs')); + route('/:user/followers', userFollowers); + route('/:user/following', userFollowing); + route('/:user/:post', post); + route('*', notFound); + + function index() { + me ? home() : entrance(); + } + + function home() { + mount(document.createElement('mk-home-page')); + } + + function entrance() { + mount(document.createElement('mk-entrance')); + } + + function notifications() { + mount(document.createElement('mk-notifications-page')); + } + + function messaging(ctx) { + if (ctx.params.username) { + const el = document.createElement('mk-messaging-room-page'); + el.setAttribute('username', ctx.params.username); + mount(el); + } else { + mount(document.createElement('mk-messaging-page')); + } + } + + function newPost() { + mount(document.createElement('mk-new-post-page')); + } + + function settings() { + mount(document.createElement('mk-settings-page')); + } + + function settingsSignin() { + mount(document.createElement('mk-signin-history-page')); + } + + function settingsApi() { + mount(document.createElement('mk-api-info-page')); + } + + function settingsTwitter() { + mount(document.createElement('mk-twitter-setting-page')); + } + + function settingsAuthorizedApps() { + mount(document.createElement('mk-authorized-apps-page')); + } + + function search(ctx) { + const el = document.createElement('mk-search-page'); + el.setAttribute('query', ctx.params.query); + mount(el); + } + + function user(page, ctx) { + const el = document.createElement('mk-user-page'); + el.setAttribute('user', ctx.params.user); + el.setAttribute('page', page); + mount(el); + } + + function userFollowing(ctx) { + const el = document.createElement('mk-user-following-page'); + el.setAttribute('user', ctx.params.user); + mount(el); + } + + function userFollowers(ctx) { + const el = document.createElement('mk-user-followers-page'); + el.setAttribute('user', ctx.params.user); + mount(el); + } + + function post(ctx) { + const el = document.createElement('mk-post-page'); + el.setAttribute('post', ctx.params.post); + mount(el); + } + + function drive(ctx) { + const el = document.createElement('mk-drive-page'); + if (ctx.params.folder) el.setAttribute('folder', ctx.params.folder); + if (ctx.params.file) el.setAttribute('file', ctx.params.file); + mount(el); + } + + function notFound() { + mount(document.createElement('mk-not-found')); + } + + riot.mixin('page', { + page: route + }); + + // EXEC + route(); +}; + +function mount(content) { + if (page) page.unmount(); + const body = document.getElementById('app'); + page = riot.mount(body.appendChild(content))[0]; +} diff --git a/src/web/app/mobile/router.ls b/src/web/app/mobile/router.ls deleted file mode 100644 index 3c0cb42236..0000000000 --- a/src/web/app/mobile/router.ls +++ /dev/null @@ -1,138 +0,0 @@ -# Router -#================================ - -riot = require \riot -route = require \page -page = null - -module.exports = (me) ~> - - # Routing - #-------------------------------- - - route \/ index - route \/i/notifications notifications - route \/i/messaging messaging - route \/i/messaging/:username messaging - route \/i/drive drive - route \/i/drive/folder/:folder drive - route \/i/drive/file/:file drive - route \/i/settings settings - route \/i/settings/signin-history settings-signin - route \/i/settings/api settings-api - route \/i/settings/twitter settings-twitter - route \/i/settings/authorized-apps settings-authorized-apps - route \/post/new new-post - route \/post::post post - route \/search::query search - route \/:user user.bind null \posts - route \/:user/graphs user.bind null \graphs - route \/:user/followers user-followers - route \/:user/following user-following - route \/:user/:post post - route \* not-found - - # Handlers - #-------------------------------- - - # / - function index - if me? then home! else entrance! - - # ホーム - function home - mount document.create-element \mk-home-page - - # 玄関 - function entrance - mount document.create-element \mk-entrance - - # 通知 - function notifications - mount document.create-element \mk-notifications-page - - # メッセージ - function messaging ctx - if ctx.params.username - p = document.create-element \mk-messaging-room-page - p.set-attribute \username ctx.params.username - mount p - else - mount document.create-element \mk-messaging-page - - # 新規投稿 - function new-post - mount document.create-element \mk-new-post-page - - # 設定 - function settings - mount document.create-element \mk-settings-page - function settings-signin - mount document.create-element \mk-signin-history-page - function settings-api - mount document.create-element \mk-api-info-page - function settings-twitter - mount document.create-element \mk-twitter-setting-page - function settings-authorized-apps - mount document.create-element \mk-authorized-apps-page - - # 検索 - function search ctx - document.create-element \mk-search-page - ..set-attribute \query ctx.params.query - .. |> mount - - # ユーザー - function user page, ctx - document.create-element \mk-user-page - ..set-attribute \user ctx.params.user - ..set-attribute \page page - .. |> mount - - # フォロー一覧 - function user-following ctx - document.create-element \mk-user-following-page - ..set-attribute \user ctx.params.user - .. |> mount - - # フォロワー一覧 - function user-followers ctx - document.create-element \mk-user-followers-page - ..set-attribute \user ctx.params.user - .. |> mount - - # 投稿詳細ページ - function post ctx - document.create-element \mk-post-page - ..set-attribute \post ctx.params.post - .. |> mount - - # ドライブ - function drive ctx - p = document.create-element \mk-drive-page - if ctx.params.folder then p.set-attribute \folder ctx.params.folder - if ctx.params.file then p.set-attribute \file ctx.params.file - mount p - - # not found - function not-found - mount document.create-element \mk-not-found - - # Register mixin - #-------------------------------- - - riot.mixin \page do - page: route - - # Exec - #-------------------------------- - - route! - -# Mount -#================================ - -function mount content - if page? then page.unmount! - body = document.get-element-by-id \app - page := riot.mount body.append-child content .0 diff --git a/src/web/app/mobile/script.js b/src/web/app/mobile/script.js index e22b4b6432..6f732da9cc 100644 --- a/src/web/app/mobile/script.js +++ b/src/web/app/mobile/script.js @@ -3,9 +3,9 @@ */ require('./tags'); -const boot = require('../boot.js'); -const mixins = require('./mixins.ls'); -const route = require('./router.ls'); +const boot = require('../boot'); +const mixins = require('./mixins'); +const route = require('./router'); /** * Boot diff --git a/src/web/app/mobile/scripts/stream.js b/src/web/app/mobile/scripts/stream.js new file mode 100644 index 0000000000..e12788f60b --- /dev/null +++ b/src/web/app/mobile/scripts/stream.js @@ -0,0 +1,11 @@ +const stream = require('../../common/scripts/stream'); +const riot = require('riot'); + +module.exports = me => { + const s = stream(me); + riot.mixin('stream', { + stream: s.event, + getStreamState: s.getState, + streamStateEv: s.stateEv + }); +}; diff --git a/src/web/app/mobile/scripts/stream.ls b/src/web/app/mobile/scripts/stream.ls deleted file mode 100644 index b7810b49ae..0000000000 --- a/src/web/app/mobile/scripts/stream.ls +++ /dev/null @@ -1,13 +0,0 @@ -# Stream -#================================ - -stream = require '../../common/scripts/stream.ls' -riot = require \riot - -module.exports = (me) ~> - s = stream me - - riot.mixin \stream do - stream: s.event - get-stream-state: s.get-state - stream-state-ev: s.state-ev diff --git a/src/web/app/mobile/scripts/ui.js b/src/web/app/mobile/scripts/ui.js new file mode 100644 index 0000000000..51ab6acd2d --- /dev/null +++ b/src/web/app/mobile/scripts/ui.js @@ -0,0 +1,7 @@ +const riot = require('riot'); + +const ui = riot.observable(); + +riot.mixin('ui', { + ui: ui +}); diff --git a/src/web/app/mobile/scripts/ui.ls b/src/web/app/mobile/scripts/ui.ls deleted file mode 100644 index aa94a8b052..0000000000 --- a/src/web/app/mobile/scripts/ui.ls +++ /dev/null @@ -1,6 +0,0 @@ -riot = require \riot - -ui = riot.observable! - -riot.mixin \ui do - ui: ui