diff --git a/gulpfile.ts b/gulpfile.ts index 9586ed36ae..53d6270660 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -3,13 +3,11 @@ */ import * as childProcess from 'child_process'; -import * as fs from 'fs'; import * as Path from 'path'; import * as gulp from 'gulp'; import * as gutil from 'gulp-util'; import * as ts from 'gulp-typescript'; import tslint from 'gulp-tslint'; -import * as glob from 'glob'; import * as es from 'event-stream'; import cssnano = require('gulp-cssnano'); import * as uglify from 'gulp-uglify'; @@ -36,7 +34,6 @@ const constants = require('./src/const.json'); gulp.task('build', [ 'build:js', 'build:ts', - 'build:about:docs', 'build:copy', 'build:client' ]); @@ -57,31 +54,6 @@ gulp.task('build:ts', () => { .pipe(gulp.dest('./built/')); }); -gulp.task('build:about:docs', () => { - function getLicenseHtml(path: string) { - return fs.readFileSync(path, 'utf-8') - .replace(/\r\n/g, '\n') - .replace(/(.)\n(.)/g, '$1 $2') - .replace(/(^|\n)(.*?)($|\n)/g, '

$2

'); - } - - const licenseHtml = getLicenseHtml('./LICENSE'); - const streams = glob.sync('./docs/**/*.pug').map(file => { - const page = file.replace('./docs/', '').replace('.pug', ''); - return gulp.src(file) - .pipe(pug({ - locals: { - path: page, - license: licenseHtml, - themeColor: constants.themeColor - } - })) - .pipe(gulp.dest('./built/web/about/pages/' + Path.parse(page).dir)); - }); - - return es.merge.apply(es, streams); -}); - gulp.task('build:copy', () => es.merge( gulp.src([ @@ -141,7 +113,7 @@ gulp.task('webpack', done => { }); gulp.task('build:client:script', () => - gulp.src('./src/web/app/client/script.js') + gulp.src('./src/web/app/boot.js') .pipe(replace('VERSION', JSON.stringify(version))) .pipe(isProduction ? uglify() : gutil.noop()) .pipe(gulp.dest('./built/web/assets/client/')) as any diff --git a/src/web/about/assets/style.css b/src/web/about/assets/style.css deleted file mode 100644 index 028bffa525..0000000000 --- a/src/web/about/assets/style.css +++ /dev/null @@ -1,208 +0,0 @@ -html { - font-family: sans-serif; -} - -body { - margin: 0; - color: #34495e; -} - -nav { - display: block; - float: left; - width: 210px; -} -nav ul { - display: block; - margin: 0 0 16px 0; - padding: 0 0 0 16px; - list-style: none; -} -nav ul li { - margin: 0; - padding: 0; -} -nav ul li p { - margin: 16px 0 0 0; -} -@media screen and (max-width: 910px) { - nav { - display: none; - } -} - -main { - float: left; - box-sizing: border-box; - padding: 32px; - width: 100%; - max-width: 700px; - overflow-wrap: break-word; -} -@media screen and (max-width: 700px) { - main { - font-size: 8px; - } -} - -footer { - padding: 32px 0 0 0; - margin: 32px 0 0 0; - border-top: solid 1px #eee; -} - -footer .contribution { - margin: 0 0 16px 0; -} - -footer .copyright { - margin: 16px 0 0 0; - color: #aaa; -} - -a { - text-decoration: none; - color: #f76d6c; -} - a:hover { - text-decoration: underline; - } - -hr { - border-top: solid 1px #eee; -} - -section { - margin: 32px 0; -} - -h1 { - margin: 0 0 24px 0; - padding: 16px 0; - font-size: 1.5em; - border-bottom: solid 2px #eee; -} - -h2 { - margin: 0 0 24px 0; - padding: 0 0 16px 0; - font-size: 1.4em; - border-bottom: solid 1px #eee; -} - -h3 { - margin: 0; - padding: 0; - font-size: 1.25em; -} - -h4 { - margin: 0; -} - -p { - margin: 1em 0; - line-height: 1.6em; -} - -p.tip { - position: relative; - padding: 12px 24px 12px 30px; - margin: 1em 0; - font-size: 0.9em; - border-left: 4px solid #f66; - background-color: #f8f8f8; - border-bottom-right-radius: 2px; - border-top-right-radius: 2px; -} - p.tip:before { - position: absolute; - top: 14px; - left: -12px; - background-color: #f66; - color: #fff; - content: "!"; - width: 20px; - height: 20px; - border-radius: 100%; - text-align: center; - line-height: 20px; - font-weight: bold; - font-family: 'Dosis', 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif; - font-size: 14px; - } - -hr { - margin: 1em 0; -} - -table { - width: 100%; - border-spacing: 0; - border-collapse: collapse; -} - -table thead { - font-weight: bold; - border-bottom: solid 2px #eee; -} - -table tbody tr { - border-bottom: dashed 1px #eee; -} - -table th, table td { - padding: 8px 16px; -} - -table.entity tbody tr td:nth-child(1) { - font-family: Consolas, 'Courier New', Courier, Monaco, monospace; -} - -table.entity tbody tr td:nth-child(2) { - font-style: italic; -} - -table.entity tr td:nth-child(3):after { - margin-left: 8px; - opacity: 0.7; -} - -table.entity tr.nullable td:nth-child(2):after { - content: "?"; - opacity: 0.7; -} -table.entity tr.nullable td:nth-child(3):after { - content: "(Null許容)"; -} - -table.entity tr.optional { - opacity: 0.7; -} -table.entity tr.optional td:nth-child(3):after { - content: "(省略可能)"; -} - -table.entity tr.nullable.optional td:nth-child(3):after { - content: "(Null許容かつ省略可能)"; -} - -pre, code, var, samp, kbd { - font-family: Consolas, 'Courier New', Courier, Monaco, monospace; -} - -code { - display: inline-block; - margin: 0 4px; - padding: 0 8px; - color: #525252; - background: #f8f8f8; - border-radius: 2px; -} - -pre code { - display: block; - overflow: auto; - margin: 0; - padding: 32px; -} diff --git a/src/web/about/index.ts b/src/web/about/index.ts deleted file mode 100644 index 9fedeb6273..0000000000 --- a/src/web/about/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as express from 'express'; -import ms = require('ms'); - -const router = express.Router(); - -router.use('/@/about/assets', express.static(`${__dirname}/assets`, { - maxAge: ms('7 days') -})); - -router.get('/@/about/', (req, res) => { - res.sendFile(`${__dirname}/pages/index.html`); -}); - -router.get('/@/about/:page(*)', (req, res) => { - res.sendFile(`${__dirname}/pages/${req.params.page}.html`); -}); - -module.exports = router; diff --git a/src/web/app/auth/script.js b/src/web/app/auth/script.js index 19391b2b9e..fe7f9befe8 100644 --- a/src/web/app/auth/script.js +++ b/src/web/app/auth/script.js @@ -7,14 +7,14 @@ import './style.styl'; import * as riot from 'riot'; require('./tags'); -import boot from '../boot'; +import init from '../init'; document.title = 'Misskey | アプリの連携'; /** - * Boot + * init */ -boot(me => { +init(me => { mount(document.createElement('mk-index')); }); diff --git a/src/web/app/auth/view.pug b/src/web/app/auth/view.pug deleted file mode 100644 index afa1e408f8..0000000000 --- a/src/web/app/auth/view.pug +++ /dev/null @@ -1,5 +0,0 @@ -extends ../base - -block head - meta(name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no') - script(src=`/assets/auth.${version}.ja.js` async defer) diff --git a/src/web/app/base.pug b/src/web/app/base.pug index 912157a2e8..fd57f55931 100644 --- a/src/web/app/base.pug +++ b/src/web/app/base.pug @@ -9,11 +9,15 @@ html meta(name='application-name' content='Misskey') meta(name='theme-color' content=themeColor) meta(name='referrer' content='origin') + title Misskey + style include ./../../../built/web/assets/init.css + script + include ./../../../built/web/assets/boot.js + script(src='https://use.fontawesome.com/22aba0df4f.js' async) - block head body noscript: p diff --git a/src/web/app/boot.js b/src/web/app/boot.js index 242df13c77..4df44ed793 100644 --- a/src/web/app/boot.js +++ b/src/web/app/boot.js @@ -1,186 +1,35 @@ /** - * boot loader + * MISSKEY ENTRY POINT */ -"use strict"; +const Url = new URL(location.href); -import * as riot from 'riot'; -import api from './common/scripts/api'; -import signout from './common/scripts/signout'; -import checkForUpdate from './common/scripts/check-for-update'; -import mixin from './common/mixins'; -import generateDefaultUserdata from './common/scripts/generate-default-userdata'; -import CONFIG from './common/scripts/config'; -require('./common/tags'); +let app = Url.host.split('.')[0]; -/** - * MISSKEY ENTRY POINT! - */ +// Detect user language +let lang = navigator.language.split('-')[0]; +if (!/^(en|ja)$/.test(lang)) lang = 'en'; -console.info(`Misskey v${VERSION}`); +// Detect user agent +const ua = navigator.userAgent.toLowerCase(); +const isMobile = /mobile|iphone|ipad|android/.test(ua); -document.domain = CONFIG.host; +const head = document.getElementsByTagName('head')[0]; -// Set global configuration -riot.mixin({ CONFIG }); - -// ↓ NodeList、HTMLCollection、FileList、DataTransferItemListで forEach を使えるようにする -if (NodeList.prototype.forEach === undefined) { - NodeList.prototype.forEach = Array.prototype.forEach; -} -if (HTMLCollection.prototype.forEach === undefined) { - HTMLCollection.prototype.forEach = Array.prototype.forEach; -} -if (FileList.prototype.forEach === undefined) { - FileList.prototype.forEach = Array.prototype.forEach; -} -if (window.DataTransferItemList && DataTransferItemList.prototype.forEach === undefined) { - DataTransferItemList.prototype.forEach = Array.prototype.forEach; +if (isMobile) { + const meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', 'width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no'); + head.appendChild(meta); } -// iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする -try { - localStorage.setItem('kyoppie', 'yuppie'); -} catch (e) { - Storage.prototype.setItem = () => { }; // noop +if (app == 'misskey') { + app = isMobile ? 'mobile' : 'desktop'; } -// クライアントを更新すべきならする -if (localStorage.getItem('should-refresh') == 'true') { - localStorage.removeItem('should-refresh'); - location.reload(true); -} - -// 更新チェック -setTimeout(checkForUpdate, 3000); - -// ユーザーをフェッチしてコールバックする -export default callback => { - // Get cached account data - let cachedMe = JSON.parse(localStorage.getItem('me')); - - if (cachedMe) { - fetched(cachedMe); - - // 後から新鮮なデータをフェッチ - fetchme(cachedMe.token, freshData => { - Object.assign(cachedMe, freshData); - cachedMe.trigger('updated'); - }); - } else { - // Get token from cookie - const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1]; - - fetchme(i, fetched); - } - - // フェッチが完了したとき - function fetched(me) { - if (me) { - riot.observable(me); - - // この me オブジェクトを更新するメソッド - me.update = data => { - if (data) Object.assign(me, data); - me.trigger('updated'); - }; - - // ローカルストレージにキャッシュ - localStorage.setItem('me', JSON.stringify(me)); - - me.on('updated', () => { - // キャッシュ更新 - localStorage.setItem('me', JSON.stringify(me)); - }); - } - - // ミックスイン初期化 - mixin(me); - - // ローディング画面クリア - const ini = document.getElementById('ini'); - ini.parentNode.removeChild(ini); - - // アプリ基底要素マウント - const app = document.createElement('div'); - app.setAttribute('id', 'app'); - document.body.appendChild(app); - - try { - callback(me); - } catch (e) { - panic(e); - } - } -}; - -// ユーザーをフェッチしてコールバックする -function fetchme(token, cb) { - let me = null; - - // Return when not signed in - if (token == null) { - return done(); - } - - // Fetch user - fetch(`${CONFIG.apiUrl}/i`, { - method: 'POST', - body: JSON.stringify({ - i: token - }) - }).then(res => { // When success - // When failed to authenticate user - if (res.status !== 200) { - return signout(); - } - - res.json().then(i => { - me = i; - me.token = token; - - // initialize it if user data is empty - me.data ? done() : init(); - }); - }, () => { // When failure - // Display error screen - riot.mount(document.body.appendChild( - document.createElement('mk-error'))); - }); - - function done() { - if (cb) cb(me); - } - - // Initialize user data - function init() { - const data = generateDefaultUserdata(); - api(token, 'i/appdata/set', { - data - }).then(() => { - me.data = data; - done(); - }); - } -} - -// BSoD -function panic(e) { - console.error(e); - - // Display blue screen - document.body.innerHTML = - `
-

:( 致命的な問題が発生しました。

-

お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。

-
-

エラーコード: ${e.toString()}

-

ブラウザ バージョン: ${navigator.userAgent}

-

クライアント バージョン: ${VERSION}

-
-

問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。

-

Thank you for using Misskey.

-
`; - - // TODO: Report the bug -} +// Load app script +const script = document.createElement('script'); +script.setAttribute('src', `/assets/${app}.${VERSION}.${lang}.js`); +script.setAttribute('async', 'true'); +script.setAttribute('defer', 'true'); +head.appendChild(script); diff --git a/src/web/app/client/script.js b/src/web/app/client/script.js deleted file mode 100644 index 465d3edb38..0000000000 --- a/src/web/app/client/script.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * MISSKEY CLIENT ENTRY POINT - */ - -const head = document.getElementsByTagName('head')[0]; - -// Detect user language -let lang = navigator.language.split('-')[0]; -if (!/^(en|ja)$/.test(lang)) lang = 'en'; - -// Detect user agent -const ua = navigator.userAgent.toLowerCase(); -const isMobile = /mobile|iphone|ipad|android/.test(ua); - -const app = isMobile ? 'mobile' : 'desktop'; - -// Load app script -const script = document.createElement('script'); -script.setAttribute('src', `/assets/${app}.${VERSION}.${lang}.js`); -script.setAttribute('async', 'true'); -script.setAttribute('defer', 'true'); -head.appendChild(script); - -if (isMobile) { - const meta = document.createElement('meta'); - meta.setAttribute('name', 'viewport'); - meta.setAttribute('content', 'width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no'); - head.appendChild(meta); -} diff --git a/src/web/app/client/view.pug b/src/web/app/client/view.pug deleted file mode 100644 index 3441631cd7..0000000000 --- a/src/web/app/client/view.pug +++ /dev/null @@ -1,5 +0,0 @@ -extends ../base - -block head - script - include ./../../../../built/web/assets/client/script.js diff --git a/src/web/app/desktop/script.js b/src/web/app/desktop/script.js index af986b53bd..b3691f7ff2 100644 --- a/src/web/app/desktop/script.js +++ b/src/web/app/desktop/script.js @@ -8,14 +8,14 @@ import './style.styl'; require('./tags'); require('./mixins'); import * as riot from 'riot'; -import boot from '../boot'; +import init from '../init'; import route from './router'; import fuckAdBlock from './scripts/fuck-ad-block'; /** - * Boot + * init */ -boot(me => { +init(me => { /** * Fuck AD Block */ diff --git a/src/web/app/dev/script.js b/src/web/app/dev/script.js index 38e864af0b..32f0285e17 100644 --- a/src/web/app/dev/script.js +++ b/src/web/app/dev/script.js @@ -6,13 +6,13 @@ import './style.styl'; require('./tags'); -import boot from '../boot'; +import init from '../init'; const route = require('./router'); /** - * Boot + * init */ -boot(me => { +init(me => { // Start routing route(me); }); diff --git a/src/web/app/dev/view.pug b/src/web/app/dev/view.pug deleted file mode 100644 index 1a9a0e5bbd..0000000000 --- a/src/web/app/dev/view.pug +++ /dev/null @@ -1,4 +0,0 @@ -extends ../base - -block head - script(src=`/assets/dev.${version}.js` async defer) diff --git a/src/web/app/init.js b/src/web/app/init.js new file mode 100644 index 0000000000..e480b2b774 --- /dev/null +++ b/src/web/app/init.js @@ -0,0 +1,186 @@ +/** + * App initializer + */ + +"use strict"; + +import * as riot from 'riot'; +import api from './common/scripts/api'; +import signout from './common/scripts/signout'; +import checkForUpdate from './common/scripts/check-for-update'; +import mixin from './common/mixins'; +import generateDefaultUserdata from './common/scripts/generate-default-userdata'; +import CONFIG from './common/scripts/config'; +require('./common/tags'); + +/** + * APP ENTRY POINT! + */ + +console.info(`Misskey v${VERSION}`); + +document.domain = CONFIG.host; + +// Set global configuration +riot.mixin({ CONFIG }); + +// ↓ NodeList、HTMLCollection、FileList、DataTransferItemListで forEach を使えるようにする +if (NodeList.prototype.forEach === undefined) { + NodeList.prototype.forEach = Array.prototype.forEach; +} +if (HTMLCollection.prototype.forEach === undefined) { + HTMLCollection.prototype.forEach = Array.prototype.forEach; +} +if (FileList.prototype.forEach === undefined) { + FileList.prototype.forEach = Array.prototype.forEach; +} +if (window.DataTransferItemList && DataTransferItemList.prototype.forEach === undefined) { + DataTransferItemList.prototype.forEach = Array.prototype.forEach; +} + +// iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする +try { + localStorage.setItem('kyoppie', 'yuppie'); +} catch (e) { + Storage.prototype.setItem = () => { }; // noop +} + +// クライアントを更新すべきならする +if (localStorage.getItem('should-refresh') == 'true') { + localStorage.removeItem('should-refresh'); + location.reload(true); +} + +// 更新チェック +setTimeout(checkForUpdate, 3000); + +// ユーザーをフェッチしてコールバックする +export default callback => { + // Get cached account data + let cachedMe = JSON.parse(localStorage.getItem('me')); + + if (cachedMe) { + fetched(cachedMe); + + // 後から新鮮なデータをフェッチ + fetchme(cachedMe.token, freshData => { + Object.assign(cachedMe, freshData); + cachedMe.trigger('updated'); + }); + } else { + // Get token from cookie + const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1]; + + fetchme(i, fetched); + } + + // フェッチが完了したとき + function fetched(me) { + if (me) { + riot.observable(me); + + // この me オブジェクトを更新するメソッド + me.update = data => { + if (data) Object.assign(me, data); + me.trigger('updated'); + }; + + // ローカルストレージにキャッシュ + localStorage.setItem('me', JSON.stringify(me)); + + me.on('updated', () => { + // キャッシュ更新 + localStorage.setItem('me', JSON.stringify(me)); + }); + } + + // ミックスイン初期化 + mixin(me); + + // ローディング画面クリア + const ini = document.getElementById('ini'); + ini.parentNode.removeChild(ini); + + // アプリ基底要素マウント + const app = document.createElement('div'); + app.setAttribute('id', 'app'); + document.body.appendChild(app); + + try { + callback(me); + } catch (e) { + panic(e); + } + } +}; + +// ユーザーをフェッチしてコールバックする +function fetchme(token, cb) { + let me = null; + + // Return when not signed in + if (token == null) { + return done(); + } + + // Fetch user + fetch(`${CONFIG.apiUrl}/i`, { + method: 'POST', + body: JSON.stringify({ + i: token + }) + }).then(res => { // When success + // When failed to authenticate user + if (res.status !== 200) { + return signout(); + } + + res.json().then(i => { + me = i; + me.token = token; + + // initialize it if user data is empty + me.data ? done() : init(); + }); + }, () => { // When failure + // Display error screen + riot.mount(document.body.appendChild( + document.createElement('mk-error'))); + }); + + function done() { + if (cb) cb(me); + } + + // Initialize user data + function init() { + const data = generateDefaultUserdata(); + api(token, 'i/appdata/set', { + data + }).then(() => { + me.data = data; + done(); + }); + } +} + +// BSoD +function panic(e) { + console.error(e); + + // Display blue screen + document.body.innerHTML = + `
+

:( 致命的な問題が発生しました。

+

お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。

+
+

エラーコード: ${e.toString()}

+

ブラウザ バージョン: ${navigator.userAgent}

+

クライアント バージョン: ${VERSION}

+
+

問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。

+

Thank you for using Misskey.

+
`; + + // TODO: Report the bug +} diff --git a/src/web/app/mobile/script.js b/src/web/app/mobile/script.js index 22150f46ad..503e0fd673 100644 --- a/src/web/app/mobile/script.js +++ b/src/web/app/mobile/script.js @@ -6,13 +6,13 @@ import './style.styl'; require('./tags'); -import boot from '../boot'; +import init from '../init'; import route from './router'; /** - * Boot + * init */ -boot(me => { +init(me => { // http://qiita.com/junya/items/3ff380878f26ca447f85 document.body.setAttribute('ontouchstart', ''); diff --git a/src/web/serve-app.ts b/src/web/serve-app.ts deleted file mode 100644 index a298d593aa..0000000000 --- a/src/web/serve-app.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as path from 'path'; -import * as express from 'express'; -import ms = require('ms'); - -export default (name: string) => (req: express.Request, res: express.Response) => { - res.sendFile(path.resolve(`${__dirname}/app/${name}/view.html`), { - maxAge: ms('7 days') - }); -}; diff --git a/src/web/server.ts b/src/web/server.ts index 17e455b172..d701d83b28 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -2,6 +2,7 @@ * Web Server */ +import * as path from 'path'; import ms = require('ms'); // express modules @@ -9,8 +10,6 @@ import * as express from 'express'; import * as bodyParser from 'body-parser'; import * as favicon from 'serve-favicon'; import * as compression from 'compression'; -const subdomain = require('subdomain'); -import serveApp from './serve-app'; import config from '../conf'; @@ -61,20 +60,13 @@ app.get('/config.json', (req, res) => { }); }); -/** - * Subdomain - */ -app.use(subdomain({ - base: config.host, - prefix: '@' -})); - /** * Routing */ -app.use(require('./about')); // about docs -app.get('/@/auth/*', serveApp('auth')); // authorize form -app.get('/@/dev/*', serveApp('dev')); // developer center -app.get('*', serveApp('client')); // client +app.get('*', (req, res) => { + res.sendFile(path.resolve(`${__dirname}/app/base.html`), { + maxAge: ms('7 days') + }); +}); module.exports = app;