summaryrefslogtreecommitdiff
path: root/src/web
diff options
context:
space:
mode:
authorha-dai <contact@haradai.net>2017-11-27 03:41:47 +0900
committerha-dai <contact@haradai.net>2017-11-27 03:41:47 +0900
commit6c75bc6d5188cbbf80fe1086fa0e8828f4edb873 (patch)
tree3ffedcc3a06e53269e92d2990cf0b3bb247ac04a /src/web
parentMerge branch 'master' of https://github.com/syuilo/misskey (diff)
parentUpdate dependencies :rocket: (diff)
downloadmisskey-6c75bc6d5188cbbf80fe1086fa0e8828f4edb873.tar.gz
misskey-6c75bc6d5188cbbf80fe1086fa0e8828f4edb873.tar.bz2
misskey-6c75bc6d5188cbbf80fe1086fa0e8828f4edb873.zip
Merge branch 'master' of github.com:syuilo/misskey
Diffstat (limited to 'src/web')
-rw-r--r--src/web/app/auth/script.ts (renamed from src/web/app/auth/script.js)2
-rw-r--r--src/web/app/auth/tags/index.ts (renamed from src/web/app/auth/tags/index.js)0
-rw-r--r--src/web/app/base.pug1
-rw-r--r--src/web/app/boot.js4
-rw-r--r--src/web/app/ch/router.ts (renamed from src/web/app/ch/router.js)6
-rw-r--r--src/web/app/ch/script.ts (renamed from src/web/app/ch/script.js)4
-rw-r--r--src/web/app/ch/tags/channel.tag19
-rw-r--r--src/web/app/ch/tags/header.tag6
-rw-r--r--src/web/app/ch/tags/index.tag4
-rw-r--r--src/web/app/ch/tags/index.ts (renamed from src/web/app/ch/tags/index.js)0
-rw-r--r--src/web/app/common/mios.ts351
-rw-r--r--src/web/app/common/mixins.ts40
-rw-r--r--src/web/app/common/mixins/api.js8
-rw-r--r--src/web/app/common/mixins/i.js20
-rw-r--r--src/web/app/common/mixins/index.js9
-rw-r--r--src/web/app/common/mixins/stream.js5
-rw-r--r--src/web/app/common/scripts/api.ts (renamed from src/web/app/common/scripts/api.js)8
-rw-r--r--src/web/app/common/scripts/bytes-to-size.ts (renamed from src/web/app/common/scripts/bytes-to-size.js)4
-rw-r--r--src/web/app/common/scripts/check-for-update.js14
-rw-r--r--src/web/app/common/scripts/check-for-update.ts12
-rw-r--r--src/web/app/common/scripts/compose-notification.ts60
-rw-r--r--src/web/app/common/scripts/config.js25
-rw-r--r--src/web/app/common/scripts/contains.ts (renamed from src/web/app/common/scripts/contains.js)0
-rw-r--r--src/web/app/common/scripts/copy-to-clipboard.ts (renamed from src/web/app/common/scripts/copy-to-clipboard.js)0
-rw-r--r--src/web/app/common/scripts/date-stringify.ts (renamed from src/web/app/common/scripts/date-stringify.js)0
-rw-r--r--src/web/app/common/scripts/gcd.ts (renamed from src/web/app/common/scripts/gcd.js)0
-rw-r--r--src/web/app/common/scripts/generate-default-userdata.js45
-rw-r--r--src/web/app/common/scripts/get-kao.ts (renamed from src/web/app/common/scripts/get-kao.js)2
-rw-r--r--src/web/app/common/scripts/get-median.ts11
-rw-r--r--src/web/app/common/scripts/is-promise.ts (renamed from src/web/app/common/scripts/is-promise.js)0
-rw-r--r--src/web/app/common/scripts/loading.ts (renamed from src/web/app/common/scripts/loading.js)0
-rw-r--r--src/web/app/common/scripts/signout.js7
-rw-r--r--src/web/app/common/scripts/signout.ts7
-rw-r--r--src/web/app/common/scripts/streaming/channel-stream.ts (renamed from src/web/app/common/scripts/channel-stream.js)6
-rw-r--r--src/web/app/common/scripts/streaming/drive-stream-manager.ts20
-rw-r--r--src/web/app/common/scripts/streaming/drive-stream.ts12
-rw-r--r--src/web/app/common/scripts/streaming/home-stream-manager.ts20
-rw-r--r--src/web/app/common/scripts/streaming/home-stream.ts (renamed from src/web/app/common/scripts/home-stream.js)11
-rw-r--r--src/web/app/common/scripts/streaming/messaging-index-stream-manager.ts20
-rw-r--r--src/web/app/common/scripts/streaming/messaging-index-stream.ts12
-rw-r--r--src/web/app/common/scripts/streaming/messaging-stream.ts (renamed from src/web/app/common/scripts/messaging-stream.js)8
-rw-r--r--src/web/app/common/scripts/streaming/requests-stream-manager.ts12
-rw-r--r--src/web/app/common/scripts/streaming/requests-stream.ts10
-rw-r--r--src/web/app/common/scripts/streaming/server-stream-manager.ts12
-rw-r--r--src/web/app/common/scripts/streaming/server-stream.ts (renamed from src/web/app/common/scripts/server-stream.js)6
-rw-r--r--src/web/app/common/scripts/streaming/stream-manager.ts89
-rw-r--r--src/web/app/common/scripts/streaming/stream.ts (renamed from src/web/app/common/scripts/stream.js)54
-rw-r--r--src/web/app/common/scripts/text-compiler.ts (renamed from src/web/app/common/scripts/text-compiler.js)9
-rw-r--r--src/web/app/common/scripts/uuid.js18
-rw-r--r--src/web/app/common/tags/error.tag4
-rw-r--r--src/web/app/common/tags/index.ts (renamed from src/web/app/common/tags/index.js)1
-rw-r--r--src/web/app/common/tags/introduction.tag2
-rw-r--r--src/web/app/common/tags/messaging/index.tag92
-rw-r--r--src/web/app/common/tags/messaging/message.tag2
-rw-r--r--src/web/app/common/tags/messaging/room.tag2
-rw-r--r--src/web/app/common/tags/nav-links.tag7
-rw-r--r--src/web/app/common/tags/raw.tag4
-rw-r--r--src/web/app/common/tags/signin-history.tag8
-rw-r--r--src/web/app/common/tags/signup.tag26
-rw-r--r--src/web/app/common/tags/stream-indicator.tag45
-rw-r--r--src/web/app/common/tags/twitter-setting.tag12
-rw-r--r--src/web/app/common/tags/uploader.tag2
-rw-r--r--src/web/app/desktop/assets/grid.svg150
-rw-r--r--src/web/app/desktop/assets/index.jpgbin0 -> 410409 bytes
-rw-r--r--src/web/app/desktop/mixins/index.ts (renamed from src/web/app/desktop/mixins/index.js)1
-rw-r--r--src/web/app/desktop/mixins/user-preview.ts (renamed from src/web/app/desktop/mixins/user-preview.js)2
-rw-r--r--src/web/app/desktop/mixins/widget.ts31
-rw-r--r--src/web/app/desktop/router.ts (renamed from src/web/app/desktop/router.js)50
-rw-r--r--src/web/app/desktop/script.js91
-rw-r--r--src/web/app/desktop/script.ts108
-rw-r--r--src/web/app/desktop/scripts/autocomplete.ts (renamed from src/web/app/desktop/scripts/autocomplete.js)26
-rw-r--r--src/web/app/desktop/scripts/dialog.ts (renamed from src/web/app/desktop/scripts/dialog.js)4
-rw-r--r--src/web/app/desktop/scripts/fuck-ad-block.ts (renamed from src/web/app/desktop/scripts/fuck-ad-block.js)2
-rw-r--r--src/web/app/desktop/scripts/input-dialog.ts (renamed from src/web/app/desktop/scripts/input-dialog.js)2
-rw-r--r--src/web/app/desktop/scripts/not-implemented-exception.ts (renamed from src/web/app/desktop/scripts/not-implemented-exception.js)0
-rw-r--r--src/web/app/desktop/scripts/notify.ts (renamed from src/web/app/desktop/scripts/notify.js)2
-rw-r--r--src/web/app/desktop/scripts/password-dialog.ts (renamed from src/web/app/desktop/scripts/password-dialog.js)2
-rw-r--r--src/web/app/desktop/scripts/scroll-follower.ts61
-rw-r--r--src/web/app/desktop/scripts/update-avatar.ts (renamed from src/web/app/desktop/scripts/update-avatar.js)13
-rw-r--r--src/web/app/desktop/scripts/update-banner.ts (renamed from src/web/app/desktop/scripts/update-banner.js)13
-rw-r--r--src/web/app/desktop/style.styl6
-rw-r--r--src/web/app/desktop/tags/analog-clock.tag2
-rw-r--r--src/web/app/desktop/tags/autocomplete-suggestion.tag2
-rw-r--r--src/web/app/desktop/tags/big-follow-button.tag12
-rw-r--r--src/web/app/desktop/tags/donation.tag7
-rw-r--r--src/web/app/desktop/tags/drive/browser-window.tag13
-rw-r--r--src/web/app/desktop/tags/drive/browser.tag57
-rw-r--r--src/web/app/desktop/tags/drive/file.tag7
-rw-r--r--src/web/app/desktop/tags/drive/folder.tag4
-rw-r--r--src/web/app/desktop/tags/drive/nav-folder.tag2
-rw-r--r--src/web/app/desktop/tags/follow-button.tag12
-rw-r--r--src/web/app/desktop/tags/home-widgets/access-log.tag95
-rw-r--r--src/web/app/desktop/tags/home-widgets/activity.tag234
-rw-r--r--src/web/app/desktop/tags/home-widgets/broadcast.tag80
-rw-r--r--src/web/app/desktop/tags/home-widgets/calendar.tag23
-rw-r--r--src/web/app/desktop/tags/home-widgets/channel.tag318
-rw-r--r--src/web/app/desktop/tags/home-widgets/donation.tag8
-rw-r--r--src/web/app/desktop/tags/home-widgets/mentions.tag2
-rw-r--r--src/web/app/desktop/tags/home-widgets/messaging.tag52
-rw-r--r--src/web/app/desktop/tags/home-widgets/nav.tag8
-rw-r--r--src/web/app/desktop/tags/home-widgets/notifications.tag19
-rw-r--r--src/web/app/desktop/tags/home-widgets/photo-stream.tag39
-rw-r--r--src/web/app/desktop/tags/home-widgets/post-form.tag103
-rw-r--r--src/web/app/desktop/tags/home-widgets/profile.tag61
-rw-r--r--src/web/app/desktop/tags/home-widgets/recommended-polls.tag19
-rw-r--r--src/web/app/desktop/tags/home-widgets/rss-reader.tag19
-rw-r--r--src/web/app/desktop/tags/home-widgets/server.tag59
-rw-r--r--src/web/app/desktop/tags/home-widgets/slideshow.tag151
-rw-r--r--src/web/app/desktop/tags/home-widgets/timeline.tag42
-rw-r--r--src/web/app/desktop/tags/home-widgets/timemachine.tag23
-rw-r--r--src/web/app/desktop/tags/home-widgets/tips.tag24
-rw-r--r--src/web/app/desktop/tags/home-widgets/trends.tag19
-rw-r--r--src/web/app/desktop/tags/home-widgets/user-recommendation.tag19
-rw-r--r--src/web/app/desktop/tags/home-widgets/version.tag6
-rw-r--r--src/web/app/desktop/tags/home.tag389
-rw-r--r--src/web/app/desktop/tags/index.ts (renamed from src/web/app/desktop/tags/index.js)19
-rw-r--r--src/web/app/desktop/tags/messaging/room-window.tag4
-rw-r--r--src/web/app/desktop/tags/notifications.tag12
-rw-r--r--src/web/app/desktop/tags/pages/drive.tag37
-rw-r--r--src/web/app/desktop/tags/pages/entrance.tag332
-rw-r--r--src/web/app/desktop/tags/pages/entrance/signin.tag134
-rw-r--r--src/web/app/desktop/tags/pages/entrance/signup.tag47
-rw-r--r--src/web/app/desktop/tags/pages/home-customize.tag12
-rw-r--r--src/web/app/desktop/tags/pages/home.tag10
-rw-r--r--src/web/app/desktop/tags/pages/messaging-room.tag37
-rw-r--r--src/web/app/desktop/tags/pages/post.tag1
-rw-r--r--src/web/app/desktop/tags/pages/selectdrive.tag9
-rw-r--r--src/web/app/desktop/tags/post-detail-sub.tag2
-rw-r--r--src/web/app/desktop/tags/post-detail.tag22
-rw-r--r--src/web/app/desktop/tags/post-form.tag21
-rw-r--r--src/web/app/desktop/tags/select-folder-from-drive-window.tag112
-rw-r--r--src/web/app/desktop/tags/settings.tag1
-rw-r--r--src/web/app/desktop/tags/sub-post-content.tag2
-rw-r--r--src/web/app/desktop/tags/timeline.tag22
-rw-r--r--src/web/app/desktop/tags/ui.tag105
-rw-r--r--src/web/app/desktop/tags/user-graphs.tag41
-rw-r--r--src/web/app/desktop/tags/user-header.tag147
-rw-r--r--src/web/app/desktop/tags/user-home.tag46
-rw-r--r--src/web/app/desktop/tags/user-photos.tag91
-rw-r--r--src/web/app/desktop/tags/user-profile.tag102
-rw-r--r--src/web/app/desktop/tags/user-timeline.tag9
-rw-r--r--src/web/app/desktop/tags/user.tag807
-rw-r--r--src/web/app/desktop/tags/widgets/activity.tag246
-rw-r--r--src/web/app/desktop/tags/widgets/calendar.tag241
-rw-r--r--src/web/app/desktop/tags/window.tag74
-rw-r--r--src/web/app/dev/router.ts (renamed from src/web/app/dev/router.js)6
-rw-r--r--src/web/app/dev/script.ts (renamed from src/web/app/dev/script.js)4
-rw-r--r--src/web/app/dev/tags/index.ts (renamed from src/web/app/dev/tags/index.js)0
-rw-r--r--src/web/app/init.js206
-rw-r--r--src/web/app/init.ts105
-rw-r--r--src/web/app/mobile/router.ts (renamed from src/web/app/mobile/router.js)11
-rw-r--r--src/web/app/mobile/script.ts (renamed from src/web/app/mobile/script.js)7
-rw-r--r--src/web/app/mobile/scripts/open-post-form.ts (renamed from src/web/app/mobile/scripts/open-post-form.js)0
-rw-r--r--src/web/app/mobile/scripts/ui-event.ts (renamed from src/web/app/mobile/scripts/ui-event.js)0
-rw-r--r--src/web/app/mobile/tags/drive.tag24
-rw-r--r--src/web/app/mobile/tags/drive/file-viewer.tag4
-rw-r--r--src/web/app/mobile/tags/drive/file.tag5
-rw-r--r--src/web/app/mobile/tags/follow-button.tag12
-rw-r--r--src/web/app/mobile/tags/home-timeline.tag16
-rw-r--r--src/web/app/mobile/tags/index.ts (renamed from src/web/app/mobile/tags/index.js)0
-rw-r--r--src/web/app/mobile/tags/notifications.tag10
-rw-r--r--src/web/app/mobile/tags/page/entrance/signin.tag1
-rw-r--r--src/web/app/mobile/tags/page/home.tag8
-rw-r--r--src/web/app/mobile/tags/page/settings.tag4
-rw-r--r--src/web/app/mobile/tags/post-detail.tag2
-rw-r--r--src/web/app/mobile/tags/post-form.tag4
-rw-r--r--src/web/app/mobile/tags/sub-post-content.tag2
-rw-r--r--src/web/app/mobile/tags/timeline.tag20
-rw-r--r--src/web/app/mobile/tags/ui.tag46
-rw-r--r--src/web/app/safe.js23
-rw-r--r--src/web/app/stats/script.ts (renamed from src/web/app/stats/script.js)2
-rw-r--r--src/web/app/stats/tags/index.tag2
-rw-r--r--src/web/app/stats/tags/index.ts (renamed from src/web/app/stats/tags/index.js)0
-rw-r--r--src/web/app/status/script.ts (renamed from src/web/app/status/script.js)2
-rw-r--r--src/web/app/status/tags/index.tag6
-rw-r--r--src/web/app/status/tags/index.ts (renamed from src/web/app/status/tags/index.js)0
-rw-r--r--src/web/app/sw.js33
-rw-r--r--src/web/assets/manifest.json8
-rw-r--r--src/web/server.ts24
179 files changed, 5135 insertions, 1901 deletions
diff --git a/src/web/app/auth/script.js b/src/web/app/auth/script.ts
index fe7f9befe8..dd598d1ed6 100644
--- a/src/web/app/auth/script.js
+++ b/src/web/app/auth/script.ts
@@ -14,7 +14,7 @@ document.title = 'Misskey | アプリの連携';
/**
* init
*/
-init(me => {
+init(() => {
mount(document.createElement('mk-index'));
});
diff --git a/src/web/app/auth/tags/index.js b/src/web/app/auth/tags/index.ts
index 42dffe67d9..42dffe67d9 100644
--- a/src/web/app/auth/tags/index.js
+++ b/src/web/app/auth/tags/index.ts
diff --git a/src/web/app/base.pug b/src/web/app/base.pug
index b1ca80deb9..3c3546d50d 100644
--- a/src/web/app/base.pug
+++ b/src/web/app/base.pug
@@ -9,6 +9,7 @@ html
meta(name='application-name' content='Misskey')
meta(name='theme-color' content=themeColor)
meta(name='referrer' content='origin')
+ link(rel='manifest' href='/manifest.json')
title Misskey
diff --git a/src/web/app/boot.js b/src/web/app/boot.js
index ac6c18d649..4a8ea030a1 100644
--- a/src/web/app/boot.js
+++ b/src/web/app/boot.js
@@ -27,7 +27,9 @@
// misskey.alice => misskey
// misskey.strawberry.pasta => misskey
// dev.misskey.arisu.tachibana => dev
- let app = url.host.split('.')[0];
+ let app = url.host == 'localhost'
+ ? 'misskey'
+ : url.host.split('.')[0];
// Detect the user language
// Note: The default language is English
diff --git a/src/web/app/ch/router.js b/src/web/app/ch/router.ts
index 424158f403..f10c4acdf0 100644
--- a/src/web/app/ch/router.js
+++ b/src/web/app/ch/router.ts
@@ -1,8 +1,8 @@
import * as riot from 'riot';
-const route = require('page');
+import * as route from 'page';
let page = null;
-export default me => {
+export default () => {
route('/', index);
route('/:channel', channel);
route('*', notFound);
@@ -22,7 +22,7 @@ export default me => {
}
// EXEC
- route();
+ (route as any)();
};
function mount(content) {
diff --git a/src/web/app/ch/script.js b/src/web/app/ch/script.ts
index 760d405c52..e23558037c 100644
--- a/src/web/app/ch/script.js
+++ b/src/web/app/ch/script.ts
@@ -12,7 +12,7 @@ import route from './router';
/**
* init
*/
-init(me => {
+init(() => {
// Start routing
- route(me);
+ route();
});
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 4ae62e7b39..716d61cde4 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -26,11 +26,11 @@
<hr>
<mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/>
<div if={ !SIGNIN }>
- <p>参加するには<a href={ CONFIG.url }>ログインまたは新規登録</a>してください</p>
+ <p>参加するには<a href={ _URL_ }>ログインまたは新規登録</a>してください</p>
</div>
<hr>
<footer>
- <small><a href={ CONFIG.url }>Misskey</a> ver { version } (葵 aoi)</small>
+ <small><a href={ _URL_ }>Misskey</a> ver { _VERSION_ } (葵 aoi)</small>
</footer>
</main>
<style>
@@ -55,7 +55,7 @@
</style>
<script>
import Progress from '../../common/scripts/loading';
- import ChannelStream from '../../common/scripts/channel-stream';
+ import ChannelStream from '../../common/scripts/streaming/channel-stream';
this.mixin('i');
this.mixin('api');
@@ -66,7 +66,6 @@
this.channel = null;
this.posts = null;
this.connection = new ChannelStream(this.id);
- this.version = VERSION;
this.unreadCount = 0;
this.on('mount', () => {
@@ -166,7 +165,7 @@
<mk-channel-post>
<header>
<a class="index" onclick={ reply }>{ post.index }:</a>
- <a class="name" href={ CONFIG.url + '/' + post.user.username }><b>{ post.user.name }</b></a>
+ <a class="name" href={ _URL_ + '/' + post.user.username }><b>{ post.user.name }</b></a>
<mk-time time={ post.created_at }/>
<mk-time time={ post.created_at } mode="detail"/>
<span>ID:<i>{ post.user.username }</i></span>
@@ -284,8 +283,6 @@
</style>
<script>
- import CONFIG from '../../common/scripts/config';
-
this.mixin('api');
this.channel = this.opts.channel;
@@ -343,7 +340,7 @@
};
this.changeFile = () => {
- this.refs.file.files.forEach(this.upload);
+ Array.from(this.refs.file.files).forEach(this.upload);
};
this.selectFile = () => {
@@ -357,7 +354,7 @@
});
};
- window.open(CONFIG.url + '/selectdrive?multiple=true',
+ window.open(_URL_ + '/selectdrive?multiple=true',
'drive_window',
'height=500,width=800');
};
@@ -367,7 +364,7 @@
};
this.onpaste = e => {
- e.clipboardData.items.forEach(item => {
+ Array.from(e.clipboardData.items).forEach(item => {
if (item.kind == 'file') {
this.upload(item.getAsFile());
}
@@ -390,7 +387,7 @@
</mk-twitter-button>
<mk-line-button>
- <div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ CONFIG.chUrl } style="display: none;"></div>
+ <div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ _CH_URL_ } style="display: none;"></div>
<script>
this.on('mount', () => {
const head = document.getElementsByTagName('head')[0];
diff --git a/src/web/app/ch/tags/header.tag b/src/web/app/ch/tags/header.tag
index 5cdcbd09cc..dec83c9a5b 100644
--- a/src/web/app/ch/tags/header.tag
+++ b/src/web/app/ch/tags/header.tag
@@ -1,10 +1,10 @@
<mk-header>
<div>
- <a href={ CONFIG.chUrl }>Index</a> | <a href={ CONFIG.url }>Misskey</a>
+ <a href={ _CH_URL_ }>Index</a> | <a href={ _URL_ }>Misskey</a>
</div>
<div>
- <a if={ !SIGNIN } href={ CONFIG.url }>ログイン(新規登録)</a>
- <a if={ SIGNIN } href={ CONFIG.url + '/' + I.username }>{ I.username }</a>
+ <a if={ !SIGNIN } href={ _URL_ }>ログイン(新規登録)</a>
+ <a if={ SIGNIN } href={ _URL_ + '/' + I.username }>{ I.username }</a>
</div>
<style>
:scope
diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag
index 50ccc0d91c..5f3871802a 100644
--- a/src/web/app/ch/tags/index.tag
+++ b/src/web/app/ch/tags/index.tag
@@ -15,7 +15,9 @@
this.mixin('api');
this.on('mount', () => {
- this.api('channels').then(channels => {
+ this.api('channels', {
+ limit: 100
+ }).then(channels => {
this.update({
channels: channels
});
diff --git a/src/web/app/ch/tags/index.js b/src/web/app/ch/tags/index.ts
index 12ffdaeb84..12ffdaeb84 100644
--- a/src/web/app/ch/tags/index.js
+++ b/src/web/app/ch/tags/index.ts
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
new file mode 100644
index 0000000000..6ee42ea8a7
--- /dev/null
+++ b/src/web/app/common/mios.ts
@@ -0,0 +1,351 @@
+import { EventEmitter } from 'eventemitter3';
+import * as riot from 'riot';
+import signout from './scripts/signout';
+import Progress from './scripts/loading';
+import HomeStreamManager from './scripts/streaming/home-stream-manager';
+import api from './scripts/api';
+
+//#region environment variables
+declare const _VERSION_: string;
+declare const _LANG_: string;
+declare const _API_URL_: string;
+declare const _SW_PUBLICKEY_: string;
+//#endregion
+
+/**
+ * Misskey Operating System
+ */
+export default class MiOS extends EventEmitter {
+ /**
+ * Misskeyの /meta で取得できるメタ情報
+ */
+ private meta: {
+ data: { [x: string]: any };
+ chachedAt: Date;
+ };
+
+ private isMetaFetching = false;
+
+ /**
+ * A signing user
+ */
+ public i: { [x: string]: any };
+
+ /**
+ * Whether signed in
+ */
+ public get isSignedin() {
+ return this.i != null;
+ }
+
+ /**
+ * Whether is debug mode
+ */
+ public get debug() {
+ return localStorage.getItem('debug') == 'true';
+ }
+
+ /**
+ * A connection manager of home stream
+ */
+ public stream: HomeStreamManager;
+
+ /**
+ * A registration of service worker
+ */
+ private swRegistration: ServiceWorkerRegistration = null;
+
+ /**
+ * Whether should register ServiceWorker
+ */
+ private shouldRegisterSw: boolean;
+
+ /**
+ * MiOSインスタンスを作成します
+ * @param shouldRegisterSw ServiceWorkerを登録するかどうか
+ */
+ constructor(shouldRegisterSw = false) {
+ super();
+
+ this.shouldRegisterSw = shouldRegisterSw;
+
+ //#region BIND
+ this.log = this.log.bind(this);
+ this.logInfo = this.logInfo.bind(this);
+ this.logWarn = this.logWarn.bind(this);
+ this.logError = this.logError.bind(this);
+ this.init = this.init.bind(this);
+ this.api = this.api.bind(this);
+ this.getMeta = this.getMeta.bind(this);
+ this.registerSw = this.registerSw.bind(this);
+ //#endregion
+ }
+
+ public log(...args) {
+ if (!this.debug) return;
+ console.log.apply(null, args);
+ }
+
+ public logInfo(...args) {
+ if (!this.debug) return;
+ console.info.apply(null, args);
+ }
+
+ public logWarn(...args) {
+ if (!this.debug) return;
+ console.warn.apply(null, args);
+ }
+
+ public logError(...args) {
+ if (!this.debug) return;
+ console.error.apply(null, args);
+ }
+
+ /**
+ * Initialize MiOS (boot)
+ * @param callback A function that call when initialized
+ */
+ public async init(callback) {
+ // ユーザーをフェッチしてコールバックする
+ const fetchme = (token, cb) => {
+ let me = null;
+
+ // Return when not signed in
+ if (token == null) {
+ return done();
+ }
+
+ // Fetch user
+ fetch(`${_API_URL_}/i`, {
+ method: 'POST',
+ body: JSON.stringify({
+ i: token
+ })
+ })
+ // When success
+ .then(res => {
+ // When failed to authenticate user
+ if (res.status !== 200) {
+ return signout();
+ }
+
+ // Parse response
+ res.json().then(i => {
+ me = i;
+ me.token = token;
+ done();
+ });
+ })
+ // When failure
+ .catch(() => {
+ // Render the error screen
+ document.body.innerHTML = '<mk-error />';
+ riot.mount('*');
+
+ Progress.done();
+ });
+
+ function done() {
+ if (cb) cb(me);
+ }
+ };
+
+ // フェッチが完了したとき
+ const 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));
+ });
+ }
+
+ this.i = me;
+
+ // Init home stream manager
+ this.stream = this.isSignedin
+ ? new HomeStreamManager(this.i)
+ : null;
+
+ // Finish init
+ callback();
+
+ //#region Post
+
+ // Init service worker
+ if (this.shouldRegisterSw) this.registerSw();
+
+ //#endregion
+ };
+
+ // Get cached account data
+ const cachedMe = JSON.parse(localStorage.getItem('me'));
+
+ // キャッシュがあったとき
+ if (cachedMe) {
+ // とりあえずキャッシュされたデータでお茶を濁して(?)おいて、
+ fetched(cachedMe);
+
+ // 後から新鮮なデータをフェッチ
+ fetchme(cachedMe.token, freshData => {
+ Object.assign(cachedMe, freshData);
+ cachedMe.trigger('updated');
+ cachedMe.trigger('refreshed');
+ });
+ } else {
+ // Get token from cookie
+ const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1];
+
+ fetchme(i, fetched);
+ }
+ }
+
+ /**
+ * Register service worker
+ */
+ private registerSw() {
+ // Check whether service worker and push manager supported
+ const isSwSupported =
+ ('serviceWorker' in navigator) && ('PushManager' in window);
+
+ // Reject when browser not service worker supported
+ if (!isSwSupported) return;
+
+ // Reject when not signed in to Misskey
+ if (!this.isSignedin) return;
+
+ // When service worker activated
+ navigator.serviceWorker.ready.then(registration => {
+ this.log('[sw] ready: ', registration);
+
+ this.swRegistration = registration;
+
+ // Options of pushManager.subscribe
+ // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
+ const opts = {
+ // A boolean indicating that the returned push subscription
+ // will only be used for messages whose effect is made visible to the user.
+ userVisibleOnly: true,
+
+ // A public key your push server will use to send
+ // messages to client apps via a push server.
+ applicationServerKey: urlBase64ToUint8Array(_SW_PUBLICKEY_)
+ };
+
+ // Subscribe push notification
+ this.swRegistration.pushManager.subscribe(opts).then(subscription => {
+ this.log('[sw] Subscribe OK:', subscription);
+
+ function encode(buffer: ArrayBuffer) {
+ return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
+ }
+
+ // Register
+ this.api('sw/register', {
+ endpoint: subscription.endpoint,
+ auth: encode(subscription.getKey('auth')),
+ publickey: encode(subscription.getKey('p256dh'))
+ });
+ })
+ // When subscribe failed
+ .catch(async (err: Error) => {
+ this.logError('[sw] Subscribe Error:', err);
+
+ // 通知が許可されていなかったとき
+ if (err.name == 'NotAllowedError') {
+ this.logError('[sw] Subscribe failed due to notification not allowed');
+ return;
+ }
+
+ // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
+ // 既に存在していることが原因でエラーになった可能性があるので、
+ // そのサブスクリプションを解除しておく
+ const subscription = await this.swRegistration.pushManager.getSubscription();
+ if (subscription) subscription.unsubscribe();
+ });
+ });
+
+ // The path of service worker script
+ const sw = `/sw.${_VERSION_}.${_LANG_}.js`;
+
+ // Register service worker
+ navigator.serviceWorker.register(sw).then(registration => {
+ // 登録成功
+ this.logInfo('[sw] Registration successful with scope: ', registration.scope);
+ }).catch(err => {
+ // 登録失敗 :(
+ this.logError('[sw] Registration failed: ', err);
+ });
+ }
+
+ /**
+ * Misskey APIにリクエストします
+ * @param endpoint エンドポイント名
+ * @param data パラメータ
+ */
+ public api(endpoint: string, data?: { [x: string]: any }) {
+ return api(this.i, endpoint, data);
+ }
+
+ /**
+ * Misskeyのメタ情報を取得します
+ * @param force キャッシュを無視するか否か
+ */
+ public getMeta(force = false) {
+ return new Promise<{ [x: string]: any }>(async (res, rej) => {
+ if (this.isMetaFetching) {
+ this.once('_meta_fetched_', () => {
+ res(this.meta.data);
+ });
+ return;
+ }
+
+ const expire = 1000 * 60; // 1min
+
+ // forceが有効, meta情報を保持していない or 期限切れ
+ if (force || this.meta == null || Date.now() - this.meta.chachedAt.getTime() > expire) {
+ this.isMetaFetching = true;
+ const meta = await this.api('meta');
+ this.meta = {
+ data: meta,
+ chachedAt: new Date()
+ };
+ this.isMetaFetching = false;
+ this.emit('_meta_fetched_');
+ res(meta);
+ } else {
+ res(this.meta.data);
+ }
+ });
+ }
+}
+
+/**
+ * Convert the URL safe base64 string to a Uint8Array
+ * @param base64String base64 string
+ */
+function urlBase64ToUint8Array(base64String: string): Uint8Array {
+ const padding = '='.repeat((4 - base64String.length % 4) % 4);
+ const base64 = (base64String + padding)
+ .replace(/\-/g, '+')
+ .replace(/_/g, '/');
+
+ const rawData = window.atob(base64);
+ const outputArray = new Uint8Array(rawData.length);
+
+ for (let i = 0; i < rawData.length; ++i) {
+ outputArray[i] = rawData.charCodeAt(i);
+ }
+ return outputArray;
+}
diff --git a/src/web/app/common/mixins.ts b/src/web/app/common/mixins.ts
new file mode 100644
index 0000000000..e9c3625937
--- /dev/null
+++ b/src/web/app/common/mixins.ts
@@ -0,0 +1,40 @@
+import * as riot from 'riot';
+
+import MiOS from './mios';
+import ServerStreamManager from './scripts/streaming/server-stream-manager';
+import RequestsStreamManager from './scripts/streaming/requests-stream-manager';
+import MessagingIndexStreamManager from './scripts/streaming/messaging-index-stream-manager';
+import DriveStreamManager from './scripts/streaming/drive-stream-manager';
+
+export default (mios: MiOS) => {
+ (riot as any).mixin('os', {
+ mios: mios
+ });
+
+ (riot as any).mixin('i', {
+ init: function() {
+ this.I = mios.i;
+ this.SIGNIN = mios.isSignedin;
+
+ if (this.SIGNIN) {
+ this.on('mount', () => {
+ mios.i.on('updated', this.update);
+ });
+ this.on('unmount', () => {
+ mios.i.off('updated', this.update);
+ });
+ }
+ },
+ me: mios.i
+ });
+
+ (riot as any).mixin('api', {
+ api: mios.api
+ });
+
+ (riot as any).mixin('stream', { stream: mios.stream });
+ (riot as any).mixin('drive-stream', { driveStream: new DriveStreamManager(mios.i) });
+ (riot as any).mixin('server-stream', { serverStream: new ServerStreamManager() });
+ (riot as any).mixin('requests-stream', { requestsStream: new RequestsStreamManager() });
+ (riot as any).mixin('messaging-index-stream', { messagingIndexStream: new MessagingIndexStreamManager(mios.i) });
+};
diff --git a/src/web/app/common/mixins/api.js b/src/web/app/common/mixins/api.js
deleted file mode 100644
index 42d96db559..0000000000
--- a/src/web/app/common/mixins/api.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import * as riot from 'riot';
-import api from '../scripts/api';
-
-export default me => {
- riot.mixin('api', {
- api: api.bind(null, me ? me.token : null)
- });
-};
diff --git a/src/web/app/common/mixins/i.js b/src/web/app/common/mixins/i.js
deleted file mode 100644
index 5225147766..0000000000
--- a/src/web/app/common/mixins/i.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as riot from 'riot';
-
-export default me => {
- riot.mixin('i', {
- init: function() {
- 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/mixins/index.js b/src/web/app/common/mixins/index.js
deleted file mode 100644
index 9718ee949b..0000000000
--- a/src/web/app/common/mixins/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import activateMe from './i';
-import activateApi from './api';
-import activateStream from './stream';
-
-export default (me, stream) => {
- activateMe(me);
- activateApi(me);
- activateStream(stream);
-};
diff --git a/src/web/app/common/mixins/stream.js b/src/web/app/common/mixins/stream.js
deleted file mode 100644
index 4706042b04..0000000000
--- a/src/web/app/common/mixins/stream.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as riot from 'riot';
-
-export default stream => {
- riot.mixin('stream', { stream });
-};
diff --git a/src/web/app/common/scripts/api.js b/src/web/app/common/scripts/api.ts
index 4855f736c7..e62447b0a0 100644
--- a/src/web/app/common/scripts/api.js
+++ b/src/web/app/common/scripts/api.ts
@@ -2,7 +2,7 @@
* API Request
*/
-import CONFIG from './config';
+declare const _API_URL_: string;
let spinner = null;
let pending = 0;
@@ -14,7 +14,7 @@ let pending = 0;
* @param {any} [data={}] Data
* @return {Promise<any>} Response
*/
-export default (i, endpoint, data = {}) => {
+export default (i, endpoint, data = {}): Promise<{ [x: string]: any }> => {
if (++pending === 1) {
spinner = document.createElement('div');
spinner.setAttribute('id', 'wait');
@@ -22,11 +22,11 @@ export default (i, endpoint, data = {}) => {
}
// Append the credential
- if (i != null) data.i = typeof i === 'object' ? i.token : i;
+ if (i != null) (data as any).i = typeof i === 'object' ? i.token : i;
return new Promise((resolve, reject) => {
// Send request
- fetch(endpoint.indexOf('://') > -1 ? endpoint : `${CONFIG.apiUrl}/${endpoint}`, {
+ fetch(endpoint.indexOf('://') > -1 ? endpoint : `${_API_URL_}/${endpoint}`, {
method: 'POST',
body: JSON.stringify(data),
credentials: endpoint === 'signin' ? 'include' : 'omit'
diff --git a/src/web/app/common/scripts/bytes-to-size.js b/src/web/app/common/scripts/bytes-to-size.ts
index af0268dbd0..1d2b1e7ce3 100644
--- a/src/web/app/common/scripts/bytes-to-size.js
+++ b/src/web/app/common/scripts/bytes-to-size.ts
@@ -1,6 +1,6 @@
export default (bytes, digits = 0) => {
- var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == 0) return '0Byte';
- var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
};
diff --git a/src/web/app/common/scripts/check-for-update.js b/src/web/app/common/scripts/check-for-update.js
deleted file mode 100644
index 7cb7839d29..0000000000
--- a/src/web/app/common/scripts/check-for-update.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import CONFIG from './config';
-
-export default function() {
- fetch(CONFIG.apiUrl + '/meta', {
- method: 'POST'
- }).then(res => {
- res.json().then(meta => {
- if (meta.version != VERSION) {
- localStorage.setItem('should-refresh', 'true');
- alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', VERSION));
- }
- });
- });
-};
diff --git a/src/web/app/common/scripts/check-for-update.ts b/src/web/app/common/scripts/check-for-update.ts
new file mode 100644
index 0000000000..c447a517fa
--- /dev/null
+++ b/src/web/app/common/scripts/check-for-update.ts
@@ -0,0 +1,12 @@
+import MiOS from '../mios';
+
+declare const _VERSION_: string;
+
+export default async function(mios: MiOS) {
+ const meta = await mios.getMeta();
+
+ if (meta.version != _VERSION_) {
+ localStorage.setItem('should-refresh', 'true');
+ alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', _VERSION_));
+ }
+}
diff --git a/src/web/app/common/scripts/compose-notification.ts b/src/web/app/common/scripts/compose-notification.ts
new file mode 100644
index 0000000000..d0e0c2098d
--- /dev/null
+++ b/src/web/app/common/scripts/compose-notification.ts
@@ -0,0 +1,60 @@
+import getPostSummary from '../../../../common/get-post-summary';
+import getReactionEmoji from '../../../../common/get-reaction-emoji';
+
+type Notification = {
+ title: string;
+ body: string;
+ icon: string;
+ onclick?: any;
+};
+
+// TODO: i18n
+
+export default function(type, data): Notification {
+ switch (type) {
+ case 'drive_file_created':
+ return {
+ title: 'ファイルがアップロードされました',
+ body: data.name,
+ icon: data.url + '?thumbnail&size=64'
+ };
+
+ case 'mention':
+ return {
+ title: `${data.user.name}さんから:`,
+ body: getPostSummary(data),
+ icon: data.user.avatar_url + '?thumbnail&size=64'
+ };
+
+ case 'reply':
+ return {
+ title: `${data.user.name}さんから返信:`,
+ body: getPostSummary(data),
+ icon: data.user.avatar_url + '?thumbnail&size=64'
+ };
+
+ case 'quote':
+ return {
+ title: `${data.user.name}さんが引用:`,
+ body: getPostSummary(data),
+ icon: data.user.avatar_url + '?thumbnail&size=64'
+ };
+
+ case 'reaction':
+ return {
+ title: `${data.user.name}: ${getReactionEmoji(data.reaction)}:`,
+ body: getPostSummary(data.post),
+ icon: data.user.avatar_url + '?thumbnail&size=64'
+ };
+
+ case 'unread_messaging_message':
+ return {
+ title: `${data.user.name}さんからメッセージ:`,
+ body: data.text, // TODO: getMessagingMessageSummary(data),
+ icon: data.user.avatar_url + '?thumbnail&size=64'
+ };
+
+ default:
+ return null;
+ }
+}
diff --git a/src/web/app/common/scripts/config.js b/src/web/app/common/scripts/config.js
deleted file mode 100644
index c5015622f0..0000000000
--- a/src/web/app/common/scripts/config.js
+++ /dev/null
@@ -1,25 +0,0 @@
-const Url = new URL(location.href);
-
-const isRoot = Url.host.split('.')[0] == 'misskey';
-
-const host = isRoot ? Url.host : Url.host.substring(Url.host.indexOf('.') + 1, Url.host.length);
-const scheme = Url.protocol;
-const url = `${scheme}//${host}`;
-const apiUrl = `${scheme}//api.${host}`;
-const chUrl = `${scheme}//ch.${host}`;
-const devUrl = `${scheme}//dev.${host}`;
-const aboutUrl = `${scheme}//about.${host}`;
-const statsUrl = `${scheme}//stats.${host}`;
-const statusUrl = `${scheme}//status.${host}`;
-
-export default {
- host,
- scheme,
- url,
- apiUrl,
- chUrl,
- devUrl,
- aboutUrl,
- statsUrl,
- statusUrl
-};
diff --git a/src/web/app/common/scripts/contains.js b/src/web/app/common/scripts/contains.ts
index a5071b3f25..a5071b3f25 100644
--- a/src/web/app/common/scripts/contains.js
+++ b/src/web/app/common/scripts/contains.ts
diff --git a/src/web/app/common/scripts/copy-to-clipboard.js b/src/web/app/common/scripts/copy-to-clipboard.ts
index 3d2741f8d7..3d2741f8d7 100644
--- a/src/web/app/common/scripts/copy-to-clipboard.js
+++ b/src/web/app/common/scripts/copy-to-clipboard.ts
diff --git a/src/web/app/common/scripts/date-stringify.js b/src/web/app/common/scripts/date-stringify.ts
index e51de8833d..e51de8833d 100644
--- a/src/web/app/common/scripts/date-stringify.js
+++ b/src/web/app/common/scripts/date-stringify.ts
diff --git a/src/web/app/common/scripts/gcd.js b/src/web/app/common/scripts/gcd.ts
index 9a19f9da66..9a19f9da66 100644
--- a/src/web/app/common/scripts/gcd.js
+++ b/src/web/app/common/scripts/gcd.ts
diff --git a/src/web/app/common/scripts/generate-default-userdata.js b/src/web/app/common/scripts/generate-default-userdata.js
deleted file mode 100644
index 1200563e1a..0000000000
--- a/src/web/app/common/scripts/generate-default-userdata.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import uuid from './uuid';
-
-const home = {
- left: [
- 'profile',
- 'calendar',
- 'rss-reader',
- 'photo-stream',
- 'version'
- ],
- right: [
- 'broadcast',
- 'notifications',
- 'user-recommendation',
- 'donation',
- 'nav',
- 'tips'
- ]
-};
-
-export default () => {
- 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 = {
- home: JSON.stringify(homeData)
- };
-
- return data;
-};
diff --git a/src/web/app/common/scripts/get-kao.js b/src/web/app/common/scripts/get-kao.ts
index 0b77ee285a..2168c5be88 100644
--- a/src/web/app/common/scripts/get-kao.js
+++ b/src/web/app/common/scripts/get-kao.ts
@@ -1,5 +1,5 @@
export default () => [
'(=^・・^=)',
'v(‘ω’)v',
- '🐡( '-' 🐡 )フグパンチ!!!!'
+ '🐡( \'-\' 🐡 )フグパンチ!!!!'
][Math.floor(Math.random() * 3)];
diff --git a/src/web/app/common/scripts/get-median.ts b/src/web/app/common/scripts/get-median.ts
new file mode 100644
index 0000000000..91a415d5b2
--- /dev/null
+++ b/src/web/app/common/scripts/get-median.ts
@@ -0,0 +1,11 @@
+/**
+ * 中央値を求めます
+ * @param samples サンプル
+ */
+export default function(samples) {
+ if (!samples.length) return 0;
+ const numbers = samples.slice(0).sort((a, b) => a - b);
+ const middle = Math.floor(numbers.length / 2);
+ const isEven = numbers.length % 2 === 0;
+ return isEven ? (numbers[middle] + numbers[middle - 1]) / 2 : numbers[middle];
+}
diff --git a/src/web/app/common/scripts/is-promise.js b/src/web/app/common/scripts/is-promise.ts
index 3b4cd70b49..3b4cd70b49 100644
--- a/src/web/app/common/scripts/is-promise.js
+++ b/src/web/app/common/scripts/is-promise.ts
diff --git a/src/web/app/common/scripts/loading.js b/src/web/app/common/scripts/loading.ts
index c48e626648..c48e626648 100644
--- a/src/web/app/common/scripts/loading.js
+++ b/src/web/app/common/scripts/loading.ts
diff --git a/src/web/app/common/scripts/signout.js b/src/web/app/common/scripts/signout.js
deleted file mode 100644
index 6c95cfbc9c..0000000000
--- a/src/web/app/common/scripts/signout.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import CONFIG from './config';
-
-export default () => {
- 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.ts b/src/web/app/common/scripts/signout.ts
new file mode 100644
index 0000000000..2923196549
--- /dev/null
+++ b/src/web/app/common/scripts/signout.ts
@@ -0,0 +1,7 @@
+declare const _HOST_: string;
+
+export default () => {
+ localStorage.removeItem('me');
+ document.cookie = `i=; domain=.${_HOST_}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+ location.href = '/';
+};
diff --git a/src/web/app/common/scripts/channel-stream.js b/src/web/app/common/scripts/streaming/channel-stream.ts
index 17944dbe45..434b108b9e 100644
--- a/src/web/app/common/scripts/channel-stream.js
+++ b/src/web/app/common/scripts/streaming/channel-stream.ts
@@ -1,16 +1,12 @@
-'use strict';
-
import Stream from './stream';
/**
* Channel stream connection
*/
-class Connection extends Stream {
+export default class Connection extends Stream {
constructor(channelId) {
super('channel', {
channel: channelId
});
}
}
-
-export default Connection;
diff --git a/src/web/app/common/scripts/streaming/drive-stream-manager.ts b/src/web/app/common/scripts/streaming/drive-stream-manager.ts
new file mode 100644
index 0000000000..8acdd7cbba
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/drive-stream-manager.ts
@@ -0,0 +1,20 @@
+import StreamManager from './stream-manager';
+import Connection from './drive-stream';
+
+export default class DriveStreamManager extends StreamManager<Connection> {
+ private me;
+
+ constructor(me) {
+ super();
+
+ this.me = me;
+ }
+
+ public getConnection() {
+ if (this.connection == null) {
+ this.connection = new Connection(this.me);
+ }
+
+ return this.connection;
+ }
+}
diff --git a/src/web/app/common/scripts/streaming/drive-stream.ts b/src/web/app/common/scripts/streaming/drive-stream.ts
new file mode 100644
index 0000000000..0da3f12554
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/drive-stream.ts
@@ -0,0 +1,12 @@
+import Stream from './stream';
+
+/**
+ * Drive stream connection
+ */
+export default class Connection extends Stream {
+ constructor(me) {
+ super('drive', {
+ i: me.token
+ });
+ }
+}
diff --git a/src/web/app/common/scripts/streaming/home-stream-manager.ts b/src/web/app/common/scripts/streaming/home-stream-manager.ts
new file mode 100644
index 0000000000..ad1dc870eb
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/home-stream-manager.ts
@@ -0,0 +1,20 @@
+import StreamManager from './stream-manager';
+import Connection from './home-stream';
+
+export default class HomeStreamManager extends StreamManager<Connection> {
+ private me;
+
+ constructor(me) {
+ super();
+
+ this.me = me;
+ }
+
+ public getConnection() {
+ if (this.connection == null) {
+ this.connection = new Connection(this.me);
+ }
+
+ return this.connection;
+ }
+}
diff --git a/src/web/app/common/scripts/home-stream.js b/src/web/app/common/scripts/streaming/home-stream.ts
index de9ceb3b51..11ad754ef0 100644
--- a/src/web/app/common/scripts/home-stream.js
+++ b/src/web/app/common/scripts/streaming/home-stream.ts
@@ -1,12 +1,10 @@
-'use strict';
-
import Stream from './stream';
-import signout from './signout';
+import signout from '../signout';
/**
* Home stream connection
*/
-class Connection extends Stream {
+export default class Connection extends Stream {
constructor(me) {
super('', {
i: me.token
@@ -17,13 +15,14 @@ class Connection extends Stream {
this.send({ type: 'alive' });
}, 1000 * 60);
+ // 自分の情報が更新されたとき
this.on('i_updated', me.update);
+ // トークンが再生成されたとき
+ // このままではAPIが利用できないので強制的にサインアウトさせる
this.on('my_token_regenerated', () => {
alert('%i18n:common.my-token-regenerated%');
signout();
});
}
}
-
-export default Connection;
diff --git a/src/web/app/common/scripts/streaming/messaging-index-stream-manager.ts b/src/web/app/common/scripts/streaming/messaging-index-stream-manager.ts
new file mode 100644
index 0000000000..0f08b01481
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/messaging-index-stream-manager.ts
@@ -0,0 +1,20 @@
+import StreamManager from './stream-manager';
+import Connection from './messaging-index-stream';
+
+export default class MessagingIndexStreamManager extends StreamManager<Connection> {
+ private me;
+
+ constructor(me) {
+ super();
+
+ this.me = me;
+ }
+
+ public getConnection() {
+ if (this.connection == null) {
+ this.connection = new Connection(this.me);
+ }
+
+ return this.connection;
+ }
+}
diff --git a/src/web/app/common/scripts/streaming/messaging-index-stream.ts b/src/web/app/common/scripts/streaming/messaging-index-stream.ts
new file mode 100644
index 0000000000..8015c840b4
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/messaging-index-stream.ts
@@ -0,0 +1,12 @@
+import Stream from './stream';
+
+/**
+ * Messaging index stream connection
+ */
+export default class Connection extends Stream {
+ constructor(me) {
+ super('messaging-index', {
+ i: me.token
+ });
+ }
+}
diff --git a/src/web/app/common/scripts/messaging-stream.js b/src/web/app/common/scripts/streaming/messaging-stream.ts
index 261525d5f6..68dfc5ec09 100644
--- a/src/web/app/common/scripts/messaging-stream.js
+++ b/src/web/app/common/scripts/streaming/messaging-stream.ts
@@ -1,23 +1,19 @@
-'use strict';
-
import Stream from './stream';
/**
* Messaging stream connection
*/
-class Connection extends Stream {
+export default class Connection extends Stream {
constructor(me, otherparty) {
super('messaging', {
i: me.token,
otherparty
});
- this.on('_connected_', () => {
+ (this as any).on('_connected_', () => {
this.send({
i: me.token
});
});
}
}
-
-export default Connection;
diff --git a/src/web/app/common/scripts/streaming/requests-stream-manager.ts b/src/web/app/common/scripts/streaming/requests-stream-manager.ts
new file mode 100644
index 0000000000..44db913e78
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/requests-stream-manager.ts
@@ -0,0 +1,12 @@
+import StreamManager from './stream-manager';
+import Connection from './requests-stream';
+
+export default class RequestsStreamManager extends StreamManager<Connection> {
+ public getConnection() {
+ if (this.connection == null) {
+ this.connection = new Connection();
+ }
+
+ return this.connection;
+ }
+}
diff --git a/src/web/app/common/scripts/streaming/requests-stream.ts b/src/web/app/common/scripts/streaming/requests-stream.ts
new file mode 100644
index 0000000000..22ecea6c07
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/requests-stream.ts
@@ -0,0 +1,10 @@
+import Stream from './stream';
+
+/**
+ * Requests stream connection
+ */
+export default class Connection extends Stream {
+ constructor() {
+ super('requests');
+ }
+}
diff --git a/src/web/app/common/scripts/streaming/server-stream-manager.ts b/src/web/app/common/scripts/streaming/server-stream-manager.ts
new file mode 100644
index 0000000000..a170daebb9
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/server-stream-manager.ts
@@ -0,0 +1,12 @@
+import StreamManager from './stream-manager';
+import Connection from './server-stream';
+
+export default class ServerStreamManager extends StreamManager<Connection> {
+ public getConnection() {
+ if (this.connection == null) {
+ this.connection = new Connection();
+ }
+
+ return this.connection;
+ }
+}
diff --git a/src/web/app/common/scripts/server-stream.js b/src/web/app/common/scripts/streaming/server-stream.ts
index a1c466b35d..b9e0684465 100644
--- a/src/web/app/common/scripts/server-stream.js
+++ b/src/web/app/common/scripts/streaming/server-stream.ts
@@ -1,14 +1,10 @@
-'use strict';
-
import Stream from './stream';
/**
* Server stream connection
*/
-class Connection extends Stream {
+export default class Connection extends Stream {
constructor() {
super('server');
}
}
-
-export default Connection;
diff --git a/src/web/app/common/scripts/streaming/stream-manager.ts b/src/web/app/common/scripts/streaming/stream-manager.ts
new file mode 100644
index 0000000000..5bb0dc701c
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/stream-manager.ts
@@ -0,0 +1,89 @@
+import { EventEmitter } from 'eventemitter3';
+import * as uuid from 'uuid';
+import Connection from './stream';
+
+/**
+ * ストリーム接続を管理するクラス
+ * 複数の場所から同じストリームを利用する際、接続をまとめたりする
+ */
+export default abstract class StreamManager<T extends Connection> extends EventEmitter {
+ private _connection: T = null;
+
+ private disposeTimerId: any;
+
+ /**
+ * コネクションを必要としているユーザー
+ */
+ private users = [];
+
+ protected set connection(connection: T) {
+ this._connection = connection;
+
+ if (this._connection == null) {
+ this.emit('disconnected');
+ } else {
+ this.emit('connected', this._connection);
+ }
+ }
+
+ protected get connection() {
+ return this._connection;
+ }
+
+ /**
+ * コネクションを持っているか否か
+ */
+ public get hasConnection() {
+ return this._connection != null;
+ }
+
+ /**
+ * コネクションを要求します
+ */
+ public abstract getConnection(): T;
+
+ /**
+ * 現在接続しているコネクションを取得します
+ */
+ public borrow() {
+ return this._connection;
+ }
+
+ /**
+ * コネクションを要求するためのユーザーIDを発行します
+ */
+ public use() {
+ // タイマー解除
+ if (this.disposeTimerId) {
+ clearTimeout(this.disposeTimerId);
+ this.disposeTimerId = null;
+ }
+
+ // ユーザーID生成
+ const userId = uuid();
+
+ this.users.push(userId);
+
+ return userId;
+ }
+
+ /**
+ * コネクションを利用し終わってもう必要ないことを通知します
+ * @param userId use で発行したユーザーID
+ */
+ public dispose(userId) {
+ this.users = this.users.filter(id => id != userId);
+
+ // 誰もコネクションの利用者がいなくなったら
+ if (this.users.length == 0) {
+ // また直ぐに再利用される可能性があるので、一定時間待ち、
+ // 新たな利用者が現れなければコネクションを切断する
+ this.disposeTimerId = setTimeout(() => {
+ this.disposeTimerId = null;
+
+ this.connection.close();
+ this.connection = null;
+ }, 3000);
+ }
+ }
+}
diff --git a/src/web/app/common/scripts/stream.js b/src/web/app/common/scripts/streaming/stream.ts
index 981118b5de..770d77510f 100644
--- a/src/web/app/common/scripts/stream.js
+++ b/src/web/app/common/scripts/streaming/stream.ts
@@ -1,35 +1,38 @@
-'use strict';
+declare const _API_URL_: string;
-const ReconnectingWebSocket = require('reconnecting-websocket');
-import * as riot from 'riot';
-import CONFIG from './config';
+import { EventEmitter } from 'eventemitter3';
+import * as ReconnectingWebsocket from 'reconnecting-websocket';
/**
* Misskey stream connection
*/
-class Connection {
- constructor(endpoint, params) {
- // BIND -----------------------------------
+export default class Connection extends EventEmitter {
+ private state: string;
+ private buffer: any[];
+ private socket: ReconnectingWebsocket;
+
+ constructor(endpoint, params?) {
+ super();
+
+ //#region BIND
this.onOpen = this.onOpen.bind(this);
this.onClose = this.onClose.bind(this);
this.onMessage = this.onMessage.bind(this);
this.send = this.send.bind(this);
this.close = this.close.bind(this);
- // ----------------------------------------
-
- riot.observable(this);
+ //#endregion
this.state = 'initializing';
this.buffer = [];
- const host = CONFIG.apiUrl.replace('http', 'ws');
+ const host = _API_URL_.replace('http', 'ws');
const query = params
? Object.keys(params)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&')
: null;
- this.socket = new ReconnectingWebSocket(`${host}/${endpoint}${query ? '?' + query : ''}`);
+ this.socket = new ReconnectingWebsocket(`${host}/${endpoint}${query ? '?' + query : ''}`);
this.socket.addEventListener('open', this.onOpen);
this.socket.addEventListener('close', this.onClose);
this.socket.addEventListener('message', this.onMessage);
@@ -37,11 +40,10 @@ class Connection {
/**
* Callback of when open connection
- * @private
*/
- onOpen() {
+ private onOpen() {
this.state = 'connected';
- this.trigger('_connected_');
+ this.emit('_connected_');
// バッファーを処理
const _buffer = [].concat(this.buffer); // Shallow copy
@@ -53,48 +55,42 @@ class Connection {
/**
* Callback of when close connection
- * @private
*/
- onClose() {
+ private onClose() {
this.state = 'reconnecting';
- this.trigger('_closed_');
+ this.emit('_closed_');
}
/**
* Callback of when received a message from connection
- * @private
*/
- onMessage(message) {
+ private onMessage(message) {
try {
const msg = JSON.parse(message.data);
- if (msg.type) this.trigger(msg.type, msg.body);
- } catch(e) {
+ if (msg.type) this.emit(msg.type, msg.body);
+ } catch (e) {
// noop
}
}
/**
* Send a message to connection
- * @public
*/
- send(message) {
+ public send(message) {
// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
if (this.state != 'connected') {
this.buffer.push(message);
return;
- };
+ }
this.socket.send(JSON.stringify(message));
}
/**
* Close this connection
- * @public
*/
- close() {
+ public close() {
this.socket.removeEventListener('open', this.onOpen);
this.socket.removeEventListener('message', this.onMessage);
}
}
-
-export default Connection;
diff --git a/src/web/app/common/scripts/text-compiler.js b/src/web/app/common/scripts/text-compiler.ts
index 0a9b8022df..e0ea47df26 100644
--- a/src/web/app/common/scripts/text-compiler.js
+++ b/src/web/app/common/scripts/text-compiler.ts
@@ -1,6 +1,7 @@
+declare const _URL_: string;
+
import * as riot from 'riot';
-const pictograph = require('pictograph');
-import CONFIG from './config';
+import * as pictograph from 'pictograph';
const escape = text =>
text
@@ -12,7 +13,7 @@ export default (tokens, shouldBreak) => {
shouldBreak = true;
}
- const me = riot.mixin('i').me;
+ const me = (riot as any).mixin('i').me;
let text = tokens.map(token => {
switch (token.type) {
@@ -26,7 +27,7 @@ export default (tokens, shouldBreak) => {
case 'link':
return `<a class="link" href="${escape(token.url)}" target="_blank" title="${escape(token.url)}">${escape(token.title)}</a>`;
case 'mention':
- return `<a href="${CONFIG.url + '/' + escape(token.username)}" target="_blank" data-user-preview="${token.content}" ${me && me.username == token.username ? 'data-is-me' : ''}>${token.content}</a>`;
+ return `<a href="${_URL_ + '/' + escape(token.username)}" target="_blank" data-user-preview="${token.content}" ${me && me.username == token.username ? 'data-is-me' : ''}>${token.content}</a>`;
case 'hashtag': // TODO
return `<a>${escape(token.content)}</a>`;
case 'code':
diff --git a/src/web/app/common/scripts/uuid.js b/src/web/app/common/scripts/uuid.js
deleted file mode 100644
index ff016e18ad..0000000000
--- a/src/web/app/common/scripts/uuid.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Generate a UUID
- */
-export default () => {
- let uuid = '';
-
- for (let i = 0; i < 32; i++) {
- const random = Math.random() * 16 | 0;
-
- if (i == 8 || i == 12 || i == 16 || i == 20) {
- uuid += '-';
- }
-
- uuid += (i == 12 ? 4 : (i == 16 ? (random & 3 | 8) : random)).toString(16);
- }
-
- return uuid;
-};
diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/tags/error.tag
index 62f4563e5c..51c2a6c13c 100644
--- a/src/web/app/common/tags/error.tag
+++ b/src/web/app/common/tags/error.tag
@@ -170,8 +170,6 @@
</style>
<script>
- import CONFIG from '../../common/scripts/config';
-
this.on('mount', () => {
this.update({
network: navigator.onLine
@@ -193,7 +191,7 @@
});
// Check misskey server is available
- fetch(`${CONFIG.apiUrl}/meta`).then(() => {
+ fetch(`${_API_URL_}/meta`).then(() => {
this.update({
end: true,
server: true
diff --git a/src/web/app/common/tags/index.js b/src/web/app/common/tags/index.ts
index 35a9f4586e..2f4e1181d4 100644
--- a/src/web/app/common/tags/index.js
+++ b/src/web/app/common/tags/index.ts
@@ -28,3 +28,4 @@ require('./reaction-picker.tag');
require('./reactions-viewer.tag');
require('./reaction-icon.tag');
require('./post-menu.tag');
+require('./nav-links.tag');
diff --git a/src/web/app/common/tags/introduction.tag b/src/web/app/common/tags/introduction.tag
index fa1b1e247a..3256688d10 100644
--- a/src/web/app/common/tags/introduction.tag
+++ b/src/web/app/common/tags/introduction.tag
@@ -3,7 +3,7 @@
<h1>Misskeyとは?</h1>
<p><ruby>Misskey<rt>みすきー</rt></ruby>は、<a href="http://syuilo.com" target="_blank">syuilo</a>が2014年くらいから<a href="https://github.com/syuilo/misskey" target="_blank">オープンソースで</a>開発・運営を行っている、ミニブログベースのSNSです。</p>
<p>無料で誰でも利用でき、広告も掲載していません。</p>
- <p><a href={ CONFIG.aboutUrl } target="_blank">もっと知りたい方はこちら</a></p>
+ <p><a href={ _ABOUT_URL_ } target="_blank">もっと知りたい方はこちら</a></p>
</article>
<style>
:scope
diff --git a/src/web/app/common/tags/messaging/index.tag b/src/web/app/common/tags/messaging/index.tag
index 731c9da2c7..aebcec9d8d 100644
--- a/src/web/app/common/tags/messaging/index.tag
+++ b/src/web/app/common/tags/messaging/index.tag
@@ -1,5 +1,5 @@
-<mk-messaging>
- <div class="search">
+<mk-messaging data-compact={ opts.compact }>
+ <div class="search" if={ !opts.compact }>
<div class="form">
<label for="search-input"><i class="fa fa-search"></i></label>
<input ref="search" type="search" oninput={ search } onkeydown={ onSearchKeydown } placeholder="%i18n:common.tags.mk-messaging.search-user%"/>
@@ -31,11 +31,37 @@
</a>
</virtual>
</div>
- <p class="no-history" if={ history.length == 0 }>%i18n:common.tags.mk-messaging.no-history%</p>
+ <p class="no-history" if={ !fetching && history.length == 0 }>%i18n:common.tags.mk-messaging.no-history%</p>
+ <p class="fetching" if={ fetching }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p>
<style>
:scope
display block
+ &[data-compact]
+ font-size 0.8em
+
+ > .history
+ > a
+ &:last-child
+ border-bottom none
+
+ &:not([data-is-me]):not([data-is-read])
+ > div
+ background-image none
+ border-left solid 4px #3aa2dc
+
+ > div
+ padding 16px
+
+ > header
+ > mk-time
+ font-size 1em
+
+ > .avatar
+ width 42px
+ height 42px
+ margin 0 12px 0 0
+
> .search
display block
position -webkit-sticky
@@ -75,7 +101,7 @@
> input
margin 0
- padding 0 12px 0 38px
+ padding 0 0 0 38px
width 100%
font-size 1em
line-height 38px
@@ -272,6 +298,15 @@
color #999
font-weight 500
+ > .fetching
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > i
+ margin-right 4px
+
// TODO: element base media query
@media (max-width 400px)
> .search
@@ -299,22 +334,61 @@
this.mixin('i');
this.mixin('api');
+ this.mixin('messaging-index-stream');
+ this.connection = this.messagingIndexStream.getConnection();
+ this.connectionId = this.messagingIndexStream.use();
+
this.searchResult = [];
+ this.history = [];
+ this.fetching = true;
+
+ this.registerMessage = message => {
+ message.is_me = message.user_id == this.I.id;
+ message._click = () => {
+ this.trigger('navigate-user', message.is_me ? message.recipient : message.user);
+ };
+ };
this.on('mount', () => {
+ this.connection.on('message', this.onMessage);
+ this.connection.on('read', this.onRead);
+
this.api('messaging/history').then(history => {
- this.isLoading = false;
+ this.fetching = false;
history.forEach(message => {
- message.is_me = message.user_id == this.I.id
- message._click = () => {
- this.trigger('navigate-user', message.is_me ? message.recipient : message.user);
- };
+ this.registerMessage(message);
});
this.history = history;
this.update();
});
});
+ this.on('unmount', () => {
+ this.connection.off('message', this.onMessage);
+ this.connection.off('read', this.onRead);
+ this.messagingIndexStream.dispose(this.connectionId);
+ });
+
+ this.onMessage = message => {
+ this.history = this.history.filter(m => !(
+ (m.recipient_id == message.recipient_id && m.user_id == message.user_id) ||
+ (m.recipient_id == message.user_id && m.user_id == message.recipient_id)));
+
+ this.registerMessage(message);
+
+ this.history.unshift(message);
+ this.update();
+ };
+
+ this.onRead = ids => {
+ ids.forEach(id => {
+ const found = this.history.find(m => m.id == id);
+ if (found) found.is_read = true;
+ });
+
+ this.update();
+ };
+
this.search = () => {
const q = this.refs.search.value;
if (q == '') {
diff --git a/src/web/app/common/tags/messaging/message.tag b/src/web/app/common/tags/messaging/message.tag
index d6db9070e2..ea1ea2310b 100644
--- a/src/web/app/common/tags/messaging/message.tag
+++ b/src/web/app/common/tags/messaging/message.tag
@@ -219,7 +219,7 @@
this.refs.text.innerHTML = compile(tokens);
- this.refs.text.children.forEach(e => {
+ Array.from(this.refs.text.children).forEach(e => {
if (e.tagName == 'MK-URL') riot.mount(e);
});
diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag
index b1082e26be..a930327841 100644
--- a/src/web/app/common/tags/messaging/room.tag
+++ b/src/web/app/common/tags/messaging/room.tag
@@ -162,7 +162,7 @@
</style>
<script>
- import MessagingStreamConnection from '../../scripts/messaging-stream';
+ import MessagingStreamConnection from '../../scripts/streaming/messaging-stream';
this.mixin('i');
this.mixin('api');
diff --git a/src/web/app/common/tags/nav-links.tag b/src/web/app/common/tags/nav-links.tag
new file mode 100644
index 0000000000..6043f128fa
--- /dev/null
+++ b/src/web/app/common/tags/nav-links.tag
@@ -0,0 +1,7 @@
+<mk-nav-links>
+ <a href={ _ABOUT_URL_ }>%i18n:common.tags.mk-nav-links.about%</a><i>・</i><a href={ _STATS_URL_ }>%i18n:common.tags.mk-nav-links.stats%</a><i>・</i><a href={ _STATUS_URL_ }>%i18n:common.tags.mk-nav-links.status%</a><i>・</i><a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a><i>・</i><a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a><i>・</i><a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a><i>・</i><a href={ _DEV_URL_ }>%i18n:common.tags.mk-nav-links.develop%</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on <i class="fa fa-twitter"></i></a>
+ <style>
+ :scope
+ display inline
+ </style>
+</mk-nav-links>
diff --git a/src/web/app/common/tags/raw.tag b/src/web/app/common/tags/raw.tag
index e1285694e4..adc6de5a3b 100644
--- a/src/web/app/common/tags/raw.tag
+++ b/src/web/app/common/tags/raw.tag
@@ -5,5 +5,9 @@
</style>
<script>
this.root.innerHTML = this.opts.content;
+
+ this.on('updated', () => {
+ this.root.innerHTML = this.opts.content;
+ });
</script>
</mk-raw>
diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag
index 9c96746249..b9bd859851 100644
--- a/src/web/app/common/tags/signin-history.tag
+++ b/src/web/app/common/tags/signin-history.tag
@@ -50,7 +50,10 @@
<script>
this.mixin('i');
this.mixin('api');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.history = [];
this.fetching = true;
@@ -63,11 +66,12 @@
});
});
- this.stream.on('signin', this.onSignin);
+ this.connection.on('signin', this.onSignin);
});
this.on('unmount', () => {
- this.stream.off('signin', this.onSignin);
+ this.connection.off('signin', this.onSignin);
+ this.stream.dispose(this.connectionId);
});
this.onSignin = signin => {
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag
index 17de0347f5..6fec46ff31 100644
--- a/src/web/app/common/tags/signup.tag
+++ b/src/web/app/common/tags/signup.tag
@@ -3,7 +3,7 @@
<label class="username">
<p class="caption"><i class="fa fa-at"></i>%i18n:common.tags.mk-signup.username%</p>
<input ref="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required="required" onkeyup={ onChangeUsername }/>
- <p class="profile-page-url-preview" if={ refs.username.value != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange' }>{ CONFIG.url + '/' + refs.username.value }</p>
+ <p class="profile-page-url-preview" if={ refs.username.value != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange' }>{ _URL_ + '/' + refs.username.value }</p>
<p class="info" if={ usernameState == 'wait' } style="color:#999"><i class="fa fa-fw fa-spinner fa-pulse"></i>%i18n:common.tags.mk-signup.checking%</p>
<p class="info" if={ usernameState == 'ok' } style="color:#3CB7B5"><i class="fa fa-fw fa-check"></i>%i18n:common.tags.mk-signup.available%</p>
<p class="info" if={ usernameState == 'unavailable' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>%i18n:common.tags.mk-signup.unavailable%</p>
@@ -30,7 +30,7 @@
</label>
<label class="recaptcha">
<p class="caption"><i class="fa fa-toggle-on" if={ recaptchaed }></i><i class="fa fa-toggle-off" if={ !recaptchaed }></i>%i18n:common.tags.mk-signup.recaptcha%</p>
- <div if={ recaptcha } class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" data-sitekey={ recaptcha.siteKey }></div>
+ <div if={ recaptcha } class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" data-sitekey={ recaptcha.site_key }></div>
</label>
<label class="agree-tou">
<input name="agree-tou" type="checkbox" autocomplete="off" required="required"/>
@@ -193,20 +193,16 @@
};
this.on('mount', () => {
- fetch('/config.json').then(res => {
- res.json().then(conf => {
- this.update({
- recaptcha: {
- siteKey: conf.recaptcha.siteKey
- }
- });
-
- const head = document.getElementsByTagName('head')[0];
- const script = document.createElement('script');
- script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
- head.appendChild(script);
- });
+ this.update({
+ recaptcha: {
+ site_key: _RECAPTCHA_SITEKEY_
+ }
});
+
+ const head = document.getElementsByTagName('head')[0];
+ const script = document.createElement('script');
+ script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
+ head.appendChild(script);
});
this.onChangeUsername = () => {
diff --git a/src/web/app/common/tags/stream-indicator.tag b/src/web/app/common/tags/stream-indicator.tag
index ea1c437035..0d74985c88 100644
--- a/src/web/app/common/tags/stream-indicator.tag
+++ b/src/web/app/common/tags/stream-indicator.tag
@@ -1,13 +1,13 @@
<mk-stream-indicator>
- <p if={ stream.state == 'initializing' }>
+ <p if={ connection.state == 'initializing' }>
<i class="fa fa-spinner fa-spin"></i>
<span>%i18n:common.tags.mk-stream-indicator.connecting%<mk-ellipsis/></span>
</p>
- <p if={ stream.state == 'reconnecting' }>
+ <p if={ connection.state == 'reconnecting' }>
<i class="fa fa-spinner fa-spin"></i>
<span>%i18n:common.tags.mk-stream-indicator.reconnecting%<mk-ellipsis/></span>
</p>
- <p if={ stream.state == 'connected' }>
+ <p if={ connection.state == 'connected' }>
<i class="fa fa-check"></i>
<span>%i18n:common.tags.mk-stream-indicator.connected%</span>
</p>
@@ -38,34 +38,41 @@
import anime from 'animejs';
this.mixin('i');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.on('before-mount', () => {
- if (this.stream.state == 'connected') {
+ if (this.connection.state == 'connected') {
this.root.style.opacity = 0;
}
- });
- this.stream.on('_connected_', () => {
- this.update();
- setTimeout(() => {
+ this.connection.on('_connected_', () => {
+ this.update();
+ setTimeout(() => {
+ anime({
+ targets: this.root,
+ opacity: 0,
+ easing: 'linear',
+ duration: 200
+ });
+ }, 1000);
+ });
+
+ this.connection.on('_closed_', () => {
+ this.update();
anime({
targets: this.root,
- opacity: 0,
+ opacity: 1,
easing: 'linear',
- duration: 200
+ duration: 100
});
- }, 1000);
+ });
});
- this.stream.on('_closed_', () => {
- this.update();
- anime({
- targets: this.root,
- opacity: 1,
- easing: 'linear',
- duration: 100
- });
+ this.on('unmount', () => {
+ this.stream.dispose(this.connectionId);
});
</script>
</mk-stream-indicator>
diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/tags/twitter-setting.tag
index 470426700c..3b70505ba2 100644
--- a/src/web/app/common/tags/twitter-setting.tag
+++ b/src/web/app/common/tags/twitter-setting.tag
@@ -1,10 +1,10 @@
<mk-twitter-setting>
- <p>%i18n:common.tags.mk-twitter-setting.description%<a href={ CONFIG.aboutUrl + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
+ <p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _ABOUT_URL_ + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
<p class="account" if={ I.twitter } title={ 'Twitter ID: ' + I.twitter.user_id }>%i18n:common.tags.mk-twitter-setting.connected-to%: <a href={ 'https://twitter.com/' + I.twitter.screen_name } target="_blank">@{ I.twitter.screen_name }</a></p>
<p>
- <a href={ CONFIG.apiUrl + '/connect/twitter' } target="_blank" onclick={ connect }>{ I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }</a>
+ <a href={ _API_URL_ + '/connect/twitter' } target="_blank" onclick={ connect }>{ I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }</a>
<span if={ I.twitter }> or </span>
- <a href={ CONFIG.apiUrl + '/disconnect/twitter' } target="_blank" if={ I.twitter } onclick={ disconnect }>%i18n:common.tags.mk-twitter-setting.disconnect%</a>
+ <a href={ _API_URL_ + '/disconnect/twitter' } target="_blank" if={ I.twitter } onclick={ disconnect }>%i18n:common.tags.mk-twitter-setting.disconnect%</a>
</p>
<p class="id" if={ I.twitter }>Twitter ID: { I.twitter.user_id }</p>
<style>
@@ -25,8 +25,6 @@
color #8899a6
</style>
<script>
- import CONFIG from '../scripts/config';
-
this.mixin('i');
this.form = null;
@@ -47,7 +45,7 @@
this.connect = e => {
e.preventDefault();
- this.form = window.open(CONFIG.apiUrl + '/connect/twitter',
+ this.form = window.open(_API_URL_ + '/connect/twitter',
'twitter_connect_window',
'height=570,width=520');
return false;
@@ -55,7 +53,7 @@
this.disconnect = e => {
e.preventDefault();
- window.open(CONFIG.apiUrl + '/disconnect/twitter',
+ window.open(_API_URL_ + '/disconnect/twitter',
'twitter_disconnect_window',
'height=570,width=520');
return false;
diff --git a/src/web/app/common/tags/uploader.tag b/src/web/app/common/tags/uploader.tag
index da97957a2c..1453391696 100644
--- a/src/web/app/common/tags/uploader.tag
+++ b/src/web/app/common/tags/uploader.tag
@@ -172,7 +172,7 @@
if (folder) data.append('folder_id', folder);
const xhr = new XMLHttpRequest();
- xhr.open('POST', this.CONFIG.apiUrl + '/drive/files/create', true);
+ xhr.open('POST', _API_URL_ + '/drive/files/create', true);
xhr.onload = e => {
const driveFile = JSON.parse(e.target.response);
diff --git a/src/web/app/desktop/assets/grid.svg b/src/web/app/desktop/assets/grid.svg
new file mode 100644
index 0000000000..d1d72cd8ce
--- /dev/null
+++ b/src/web/app/desktop/assets/grid.svg
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32"
+ height="32"
+ viewBox="0 0 8.4666665 8.4666669"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.1 r15371"
+ sodipodi:docname="grid.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="22.4"
+ inkscape:cx="14.687499"
+ inkscape:cy="14.558219"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ units="px"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1017"
+ inkscape:window-x="-8"
+ inkscape:window-y="1072"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid3680"
+ empspacing="8"
+ empcolor="#ff3fff"
+ empopacity="0.41176471" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="レイヤー 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-288.53331)">
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 0,296.99998 v -8.46667 h 8.4666666 l 10e-8,0.26458 H 0.26458333 l 0,8.20209 z"
+ id="path3684"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 4.2333334,292.23748 h 0.2645833 v 0.52916 h 0.5291667 l 0,0.26459 H 4.4979167 v 0.52917 H 4.2333334 v -0.52917 H 3.7041667 l 0,-0.26459 h 0.5291667 z"
+ id="path4491"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 3.4395833,292.76664 0,0.26459 H 2.38125 l 0,-0.26459 z"
+ id="path4493"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 6.3499999,292.76664 10e-8,0.26459 H 5.2916667 l -1e-7,-0.26459 z"
+ id="path4493-2"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 7.6729167,292.76664 v 0.26459 H 6.6145834 v -0.26459 z"
+ id="path4493-6"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 2.1166666,292.76664 1e-7,0.26459 H 1.0583334 l -1e-7,-0.26459 z"
+ id="path4493-1"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 4.2333333,291.97289 0.2645834,0 v -1.05833 l -0.2645834,0 z"
+ id="path4522"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 4.2333334,290.64997 0.2645833,1e-5 v -1.05833 l -0.2645833,-1e-5 z"
+ id="path4522-7"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 4.2333334,294.88331 h 0.2645833 v -1.05833 H 4.2333334 Z"
+ id="path4522-5"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 4.2333333,296.20622 h 0.2645833 v -1.05833 H 4.2333333 Z"
+ id="path4522-74"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 4.2333334,289.32706 0.2645834,10e-6 -10e-8,-0.52918 -0.2645834,-10e-6 z"
+ id="path4522-7-4"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 4.2333332,296.99998 h 0.2645835 l 0,-0.52917 H 4.2333333 Z"
+ id="path4522-7-4-4"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 0.79375,292.76664 -3e-8,0.26459 -0.52916667,0 3e-8,-0.26459 z"
+ id="path4493-1-7"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.05;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 8.4666667,292.76664 v 0.26459 l -0.5291667,0 v -0.26459 z"
+ id="path4493-1-7-2"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ </g>
+</svg>
diff --git a/src/web/app/desktop/assets/index.jpg b/src/web/app/desktop/assets/index.jpg
new file mode 100644
index 0000000000..10c412efe2
--- /dev/null
+++ b/src/web/app/desktop/assets/index.jpg
Binary files differ
diff --git a/src/web/app/desktop/mixins/index.js b/src/web/app/desktop/mixins/index.ts
index a7a3eb9485..e0c94ec5ee 100644
--- a/src/web/app/desktop/mixins/index.js
+++ b/src/web/app/desktop/mixins/index.ts
@@ -1 +1,2 @@
require('./user-preview');
+require('./widget');
diff --git a/src/web/app/desktop/mixins/user-preview.js b/src/web/app/desktop/mixins/user-preview.ts
index 3f483beb3a..614de72bea 100644
--- a/src/web/app/desktop/mixins/user-preview.js
+++ b/src/web/app/desktop/mixins/user-preview.ts
@@ -52,7 +52,7 @@ function attach(el) {
clearTimeout(showTimer);
hideTimer = setTimeout(close, 500);
});
- tag = riot.mount(document.body.appendChild(preview), {
+ tag = (riot as any).mount(document.body.appendChild(preview), {
user: user
})[0];
};
diff --git a/src/web/app/desktop/mixins/widget.ts b/src/web/app/desktop/mixins/widget.ts
new file mode 100644
index 0000000000..04131cd8f0
--- /dev/null
+++ b/src/web/app/desktop/mixins/widget.ts
@@ -0,0 +1,31 @@
+import * as riot from 'riot';
+
+// ミックスインにオプションを渡せないのアレ
+// SEE: https://github.com/riot/riot/issues/2434
+
+(riot as any).mixin('widget', {
+ init: function() {
+ this.mixin('i');
+ this.mixin('api');
+
+ this.id = this.opts.id;
+ this.place = this.opts.place;
+
+ if (this.data) {
+ Object.keys(this.data).forEach(prop => {
+ this.data[prop] = this.opts.data.hasOwnProperty(prop) ? this.opts.data[prop] : this.data[prop];
+ });
+ }
+ },
+
+ save: function() {
+ this.update();
+ this.api('i/update_home', {
+ id: this.id,
+ data: this.data
+ }).then(() => {
+ this.I.client_settings.home.find(w => w.id == this.id).data = this.data;
+ this.I.update();
+ });
+ }
+});
diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.ts
index 977e3fa9a6..27b63ab2ef 100644
--- a/src/web/app/desktop/router.js
+++ b/src/web/app/desktop/router.ts
@@ -3,28 +3,37 @@
*/
import * as riot from 'riot';
-const route = require('page');
+import * as route from 'page';
+import MiOS from '../common/mios';
let page = null;
-export default me => {
- route('/', index);
- route('/selectdrive', selectDrive);
- 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);
+export default (mios: MiOS) => {
+ route('/', index);
+ route('/selectdrive', selectDrive);
+ route('/i/customize-home', customizeHome);
+ route('/i/drive', drive);
+ route('/i/drive/folder/:folder', drive);
+ route('/i/messaging/:user', messaging);
+ 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();
+ mios.isSignedin ? home() : entrance();
}
function home() {
mount(document.createElement('mk-home-page'));
}
+ function customizeHome() {
+ mount(document.createElement('mk-home-customize-page'));
+ }
+
function entrance() {
mount(document.createElement('mk-entrance'));
document.documentElement.setAttribute('data-page', 'entrance');
@@ -59,20 +68,31 @@ export default me => {
mount(document.createElement('mk-selectdrive-page'));
}
+ function drive(ctx) {
+ const el = document.createElement('mk-drive-page');
+ if (ctx.params.folder) el.setAttribute('folder', ctx.params.folder);
+ mount(el);
+ }
+
+ function messaging(ctx) {
+ const el = document.createElement('mk-messaging-room-page');
+ el.setAttribute('user', ctx.params.user);
+ mount(el);
+ }
+
function notFound() {
mount(document.createElement('mk-not-found'));
}
- riot.mixin('page', {
+ (riot as any).mixin('page', {
page: route
});
// EXEC
- route();
+ (route as any)();
};
function mount(content) {
- document.documentElement.style.background = '#313a42';
document.documentElement.removeAttribute('data-page');
if (page) page.unmount();
const body = document.getElementById('app');
diff --git a/src/web/app/desktop/script.js b/src/web/app/desktop/script.js
deleted file mode 100644
index 46a7fce700..0000000000
--- a/src/web/app/desktop/script.js
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * Desktop Client
- */
-
-// Style
-import './style.styl';
-
-require('./tags');
-require('./mixins');
-import * as riot from 'riot';
-import init from '../init';
-import route from './router';
-import fuckAdBlock from './scripts/fuck-ad-block';
-import getPostSummary from '../../../common/get-post-summary.ts';
-
-/**
- * init
- */
-init(async (me, stream) => {
- /**
- * Fuck AD Block
- */
- fuckAdBlock();
-
- /**
- * Init Notification
- */
- if ('Notification' in window) {
- // 許可を得ていなかったらリクエスト
- if (Notification.permission == 'default') {
- await Notification.requestPermission();
- }
-
- if (Notification.permission == 'granted') {
- registerNotifications(stream);
- }
- }
-
- // Start routing
- route(me);
-});
-
-function registerNotifications(stream) {
- if (stream == null) return;
-
- stream.on('drive_file_created', file => {
- const n = new Notification('ファイルがアップロードされました', {
- body: file.name,
- icon: file.url + '?thumbnail&size=64'
- });
- setTimeout(n.close.bind(n), 5000);
- });
-
- stream.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);
- });
-
- stream.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);
- });
-
- stream.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);
- });
-
- stream.on('unread_messaging_message', message => {
- const n = new Notification(`${message.user.name}さんからメッセージ:`, {
- body: message.text, // TODO: getMessagingMessageSummary(message),
- icon: message.user.avatar_url + '?thumbnail&size=64'
- });
- n.onclick = () => {
- n.close();
- riot.mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
- user: message.user
- });
- };
- setTimeout(n.close.bind(n), 7000);
- });
-}
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
new file mode 100644
index 0000000000..b06cb180e1
--- /dev/null
+++ b/src/web/app/desktop/script.ts
@@ -0,0 +1,108 @@
+/**
+ * Desktop Client
+ */
+
+// Style
+import './style.styl';
+
+require('./tags');
+require('./mixins');
+import * as riot from 'riot';
+import init from '../init';
+import route from './router';
+import fuckAdBlock from './scripts/fuck-ad-block';
+import MiOS from '../common/mios';
+import HomeStreamManager from '../common/scripts/streaming/home-stream-manager';
+import composeNotification from '../common/scripts/compose-notification';
+
+/**
+ * init
+ */
+init(async (mios: MiOS) => {
+ /**
+ * Fuck AD Block
+ */
+ fuckAdBlock();
+
+ /**
+ * Init Notification
+ */
+ if ('Notification' in window) {
+ // 許可を得ていなかったらリクエスト
+ if ((Notification as any).permission == 'default') {
+ await Notification.requestPermission();
+ }
+
+ if ((Notification as any).permission == 'granted') {
+ registerNotifications(mios.stream);
+ }
+ }
+
+ // Start routing
+ route(mios);
+}, true);
+
+function registerNotifications(stream: HomeStreamManager) {
+ if (stream == null) return;
+
+ if (stream.hasConnection) {
+ attach(stream.borrow());
+ }
+
+ stream.on('connected', connection => {
+ attach(connection);
+ });
+
+ function attach(connection) {
+ connection.on('drive_file_created', file => {
+ const _n = composeNotification('drive_file_created', file);
+ const n = new Notification(_n.title, {
+ body: _n.body,
+ icon: _n.icon
+ });
+ setTimeout(n.close.bind(n), 5000);
+ });
+
+ connection.on('mention', post => {
+ const _n = composeNotification('mention', post);
+ const n = new Notification(_n.title, {
+ body: _n.body,
+ icon: _n.icon
+ });
+ setTimeout(n.close.bind(n), 6000);
+ });
+
+ connection.on('reply', post => {
+ const _n = composeNotification('reply', post);
+ const n = new Notification(_n.title, {
+ body: _n.body,
+ icon: _n.icon
+ });
+ setTimeout(n.close.bind(n), 6000);
+ });
+
+ connection.on('quote', post => {
+ const _n = composeNotification('quote', post);
+ const n = new Notification(_n.title, {
+ body: _n.body,
+ icon: _n.icon
+ });
+ setTimeout(n.close.bind(n), 6000);
+ });
+
+ connection.on('unread_messaging_message', message => {
+ const _n = composeNotification('unread_messaging_message', message);
+ const n = new Notification(_n.title, {
+ body: _n.body,
+ icon: _n.icon
+ });
+ n.onclick = () => {
+ n.close();
+ (riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
+ user: message.user
+ });
+ };
+ setTimeout(n.close.bind(n), 7000);
+ });
+ }
+}
diff --git a/src/web/app/desktop/scripts/autocomplete.js b/src/web/app/desktop/scripts/autocomplete.ts
index 8ca516e2a9..9df7aae08d 100644
--- a/src/web/app/desktop/scripts/autocomplete.js
+++ b/src/web/app/desktop/scripts/autocomplete.ts
@@ -1,10 +1,12 @@
-const getCaretCoordinates = require('textarea-caret');
+import getCaretCoordinates = require('textarea-caret');
import * as riot from 'riot';
/**
* オートコンプリートを管理するクラス。
*/
class Autocomplete {
+ private suggestion: any;
+ private textarea: any;
/**
* 対象のテキストエリアを与えてインスタンスを初期化します。
@@ -23,22 +25,22 @@ class Autocomplete {
/**
* このインスタンスにあるテキストエリアの入力のキャプチャを開始します。
*/
- attach() {
+ public attach() {
this.textarea.addEventListener('input', this.onInput);
}
/**
* このインスタンスにあるテキストエリアの入力のキャプチャを解除します。
*/
- detach() {
+ public detach() {
this.textarea.removeEventListener('input', this.onInput);
this.close();
}
/**
- * [Private] テキスト入力時
+ * テキスト入力時
*/
- onInput() {
+ private onInput() {
this.close();
const caret = this.textarea.selectionStart;
@@ -56,9 +58,9 @@ class Autocomplete {
}
/**
- * [Private] サジェストを提示します。
+ * サジェストを提示します。
*/
- open(type, q) {
+ private open(type, q) {
// 既に開いているサジェストは閉じる
this.close();
@@ -81,7 +83,7 @@ class Autocomplete {
const el = document.body.appendChild(tag);
// マウント
- this.suggestion = riot.mount(el, {
+ this.suggestion = (riot as any).mount(el, {
textarea: this.textarea,
complete: this.complete,
close: this.close,
@@ -91,9 +93,9 @@ class Autocomplete {
}
/**
- * [Private] サジェストを閉じます。
+ * サジェストを閉じます。
*/
- close() {
+ private close() {
if (this.suggestion == null) return;
this.suggestion.unmount();
@@ -103,9 +105,9 @@ class Autocomplete {
}
/**
- * [Private] オートコンプリートする
+ * オートコンプリートする
*/
- complete(user) {
+ private complete(user) {
this.close();
const value = user.username;
diff --git a/src/web/app/desktop/scripts/dialog.js b/src/web/app/desktop/scripts/dialog.ts
index c502d3fcb8..816ba4b5f5 100644
--- a/src/web/app/desktop/scripts/dialog.js
+++ b/src/web/app/desktop/scripts/dialog.ts
@@ -1,9 +1,9 @@
import * as riot from 'riot';
-export default (title, text, buttons, canThrough, onThrough) => {
+export default (title, text, buttons, canThrough?, onThrough?) => {
const dialog = document.body.appendChild(document.createElement('mk-dialog'));
const controller = riot.observable();
- riot.mount(dialog, {
+ (riot as any).mount(dialog, {
controller: controller,
title: title,
text: text,
diff --git a/src/web/app/desktop/scripts/fuck-ad-block.js b/src/web/app/desktop/scripts/fuck-ad-block.ts
index ccfc43ce6e..8be3c80ea1 100644
--- a/src/web/app/desktop/scripts/fuck-ad-block.js
+++ b/src/web/app/desktop/scripts/fuck-ad-block.ts
@@ -1,6 +1,8 @@
require('fuckadblock');
import dialog from './dialog';
+declare const fuckAdBlock: any;
+
export default () => {
if (fuckAdBlock === undefined) {
adBlockDetected();
diff --git a/src/web/app/desktop/scripts/input-dialog.js b/src/web/app/desktop/scripts/input-dialog.ts
index 954fabfb67..b06d011c6b 100644
--- a/src/web/app/desktop/scripts/input-dialog.js
+++ b/src/web/app/desktop/scripts/input-dialog.ts
@@ -2,7 +2,7 @@ import * as riot from 'riot';
export default (title, placeholder, defaultValue, onOk, onCancel) => {
const dialog = document.body.appendChild(document.createElement('mk-input-dialog'));
- return riot.mount(dialog, {
+ return (riot as any).mount(dialog, {
title: title,
placeholder: placeholder,
'default': defaultValue,
diff --git a/src/web/app/desktop/scripts/not-implemented-exception.js b/src/web/app/desktop/scripts/not-implemented-exception.ts
index dd00c7662f..dd00c7662f 100644
--- a/src/web/app/desktop/scripts/not-implemented-exception.js
+++ b/src/web/app/desktop/scripts/not-implemented-exception.ts
diff --git a/src/web/app/desktop/scripts/notify.js b/src/web/app/desktop/scripts/notify.ts
index e58a8e4d36..2e6cbdeed8 100644
--- a/src/web/app/desktop/scripts/notify.js
+++ b/src/web/app/desktop/scripts/notify.ts
@@ -2,7 +2,7 @@ import * as riot from 'riot';
export default message => {
const notification = document.body.appendChild(document.createElement('mk-ui-notification'));
- riot.mount(notification, {
+ (riot as any).mount(notification, {
message: message
});
};
diff --git a/src/web/app/desktop/scripts/password-dialog.js b/src/web/app/desktop/scripts/password-dialog.ts
index 2bdc93e421..39d7f3db7a 100644
--- a/src/web/app/desktop/scripts/password-dialog.js
+++ b/src/web/app/desktop/scripts/password-dialog.ts
@@ -2,7 +2,7 @@ import * as riot from 'riot';
export default (title, onOk, onCancel) => {
const dialog = document.body.appendChild(document.createElement('mk-input-dialog'));
- return riot.mount(dialog, {
+ return (riot as any).mount(dialog, {
title: title,
type: 'password',
onOk: onOk,
diff --git a/src/web/app/desktop/scripts/scroll-follower.ts b/src/web/app/desktop/scripts/scroll-follower.ts
new file mode 100644
index 0000000000..05072958ce
--- /dev/null
+++ b/src/web/app/desktop/scripts/scroll-follower.ts
@@ -0,0 +1,61 @@
+/**
+ * 要素をスクロールに追従させる
+ */
+export default class ScrollFollower {
+ private follower: Element;
+ private containerTop: number;
+ private topPadding: number;
+
+ constructor(follower: Element, topPadding: number) {
+ //#region
+ this.follow = this.follow.bind(this);
+ //#endregion
+
+ this.follower = follower;
+ this.containerTop = follower.getBoundingClientRect().top;
+ this.topPadding = topPadding;
+
+ window.addEventListener('scroll', this.follow);
+ window.addEventListener('resize', this.follow);
+ }
+
+ /**
+ * 追従解除
+ */
+ public dispose() {
+ window.removeEventListener('scroll', this.follow);
+ window.removeEventListener('resize', this.follow);
+ }
+
+ private follow() {
+ const windowBottom = window.scrollY + window.innerHeight;
+ const windowTop = window.scrollY + this.topPadding;
+
+ const rect = this.follower.getBoundingClientRect();
+ const followerBottom = (rect.top + window.scrollY) + rect.height;
+ const screenHeight = window.innerHeight - this.topPadding;
+
+ // スクロールの上部(+余白)がフォロワーコンテナの上部よりも上方にある
+ if (window.scrollY + this.topPadding < this.containerTop) {
+ // フォロワーをコンテナの最上部に合わせる
+ (this.follower.parentNode as any).style.marginTop = '0px';
+ return;
+ }
+
+ // スクロールの下部がフォロワーの下部よりも下方にある かつ 表示領域の縦幅がフォロワーの縦幅よりも狭い
+ if (windowBottom > followerBottom && rect.height > screenHeight) {
+ // フォロワーの下部をスクロール下部に合わせる
+ const top = (windowBottom - rect.height) - this.containerTop;
+ (this.follower.parentNode as any).style.marginTop = `${top}px`;
+ return;
+ }
+
+ // スクロールの上部(+余白)がフォロワーの上部よりも上方にある または 表示領域の縦幅がフォロワーの縦幅よりも広い
+ if (windowTop < rect.top + window.scrollY || rect.height < screenHeight) {
+ // フォロワーの上部をスクロール上部(+余白)に合わせる
+ const top = windowTop - this.containerTop;
+ (this.follower.parentNode as any).style.marginTop = `${top}px`;
+ return;
+ }
+ }
+}
diff --git a/src/web/app/desktop/scripts/update-avatar.js b/src/web/app/desktop/scripts/update-avatar.ts
index 165c90567c..356f4e6f9d 100644
--- a/src/web/app/desktop/scripts/update-avatar.js
+++ b/src/web/app/desktop/scripts/update-avatar.ts
@@ -1,11 +1,12 @@
+declare const _API_URL_: string;
+
import * as riot from 'riot';
-import CONFIG from '../../common/scripts/config';
import dialog from './dialog';
import api from '../../common/scripts/api';
export default (I, cb, file = null) => {
const fileSelected = file => {
- const cropper = riot.mount(document.body.appendChild(document.createElement('mk-crop-window')), {
+ const cropper = (riot as any).mount(document.body.appendChild(document.createElement('mk-crop-window')), {
file: file,
title: 'アバターとして表示する部分を選択',
aspectRatio: 1 / 1
@@ -37,16 +38,16 @@ export default (I, cb, file = null) => {
};
const upload = (data, folder) => {
- const progress = riot.mount(document.body.appendChild(document.createElement('mk-progress-dialog')), {
+ const progress = (riot as any).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.apiUrl + '/drive/files/create', true);
+ xhr.open('POST', _API_URL_ + '/drive/files/create', true);
xhr.onload = e => {
- const file = JSON.parse(e.target.response);
+ const file = JSON.parse((e.target as any).response);
progress.close();
set(file);
};
@@ -75,7 +76,7 @@ export default (I, cb, file = null) => {
if (file) {
fileSelected(file);
} else {
- const browser = riot.mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), {
+ const browser = (riot as any).mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), {
multiple: false,
title: '<i class="fa fa-picture-o"></i>アバターにする画像を選択'
})[0];
diff --git a/src/web/app/desktop/scripts/update-banner.js b/src/web/app/desktop/scripts/update-banner.ts
index d83b2bf1b1..1996b75642 100644
--- a/src/web/app/desktop/scripts/update-banner.js
+++ b/src/web/app/desktop/scripts/update-banner.ts
@@ -1,11 +1,12 @@
+declare const _API_URL_: string;
+
import * as riot from 'riot';
-import CONFIG from '../../common/scripts/config';
import dialog from './dialog';
import api from '../../common/scripts/api';
export default (I, cb, file = null) => {
const fileSelected = file => {
- const cropper = riot.mount(document.body.appendChild(document.createElement('mk-crop-window')), {
+ const cropper = (riot as any).mount(document.body.appendChild(document.createElement('mk-crop-window')), {
file: file,
title: 'バナーとして表示する部分を選択',
aspectRatio: 16 / 9
@@ -37,16 +38,16 @@ export default (I, cb, file = null) => {
};
const upload = (data, folder) => {
- const progress = riot.mount(document.body.appendChild(document.createElement('mk-progress-dialog')), {
+ const progress = (riot as any).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.apiUrl + '/drive/files/create', true);
+ xhr.open('POST', _API_URL_ + '/drive/files/create', true);
xhr.onload = e => {
- const file = JSON.parse(e.target.response);
+ const file = JSON.parse((e.target as any).response);
progress.close();
set(file);
};
@@ -75,7 +76,7 @@ export default (I, cb, file = null) => {
if (file) {
fileSelected(file);
} else {
- const browser = riot.mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), {
+ const browser = (riot as any).mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), {
multiple: false,
title: '<i class="fa fa-picture-o"></i>バナーにする画像を選択'
})[0];
diff --git a/src/web/app/desktop/style.styl b/src/web/app/desktop/style.styl
index 4597dffdb3..d99e5df2b4 100644
--- a/src/web/app/desktop/style.styl
+++ b/src/web/app/desktop/style.styl
@@ -40,8 +40,7 @@
background rgba(0, 0, 0, 0.2)
html
- //background #2f3e42
- background #313a42
+ background #f7f7f7
// ↓ workaround of https://github.com/riot/riot/issues/2134
&[data-page='entrance']
@@ -49,9 +48,6 @@ html
right auto
left 15px
-html[theme='dark']
- background #100f0f
-
button
font-family sans-serif
diff --git a/src/web/app/desktop/tags/analog-clock.tag b/src/web/app/desktop/tags/analog-clock.tag
index 6cd7103c6e..c0489d3feb 100644
--- a/src/web/app/desktop/tags/analog-clock.tag
+++ b/src/web/app/desktop/tags/analog-clock.tag
@@ -72,7 +72,7 @@
const length = Math.min(canvW, canvH) / 4;
const uv = new Vec2(Math.sin(angle), -Math.cos(angle));
ctx.beginPath();
- ctx.strokeStyle = THEME_COLOR;
+ ctx.strokeStyle = _THEME_COLOR_;
ctx.lineWidth = 2;
ctx.moveTo(canvW / 2 - uv.x * length / 5, canvH / 2 - uv.y * length / 5);
ctx.lineTo(canvW / 2 + uv.x * length, canvH / 2 + uv.y * length);
diff --git a/src/web/app/desktop/tags/autocomplete-suggestion.tag b/src/web/app/desktop/tags/autocomplete-suggestion.tag
index b936360402..7311606694 100644
--- a/src/web/app/desktop/tags/autocomplete-suggestion.tag
+++ b/src/web/app/desktop/tags/autocomplete-suggestion.tag
@@ -177,7 +177,7 @@
};
this.applySelect = () => {
- this.refs.users.children.forEach(el => {
+ Array.from(this.refs.users.children).forEach(el => {
el.removeAttribute('data-selected');
});
diff --git a/src/web/app/desktop/tags/big-follow-button.tag b/src/web/app/desktop/tags/big-follow-button.tag
index 86df2d4924..8897748ae1 100644
--- a/src/web/app/desktop/tags/big-follow-button.tag
+++ b/src/web/app/desktop/tags/big-follow-button.tag
@@ -74,7 +74,10 @@
this.mixin('i');
this.mixin('api');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.user = null;
this.userPromise = isPromise(this.opts.user)
@@ -89,14 +92,15 @@
init: false,
user: user
});
- this.stream.on('follow', this.onStreamFollow);
- this.stream.on('unfollow', this.onStreamUnfollow);
+ this.connection.on('follow', this.onStreamFollow);
+ this.connection.on('unfollow', this.onStreamUnfollow);
});
});
this.on('unmount', () => {
- this.stream.off('follow', this.onStreamFollow);
- this.stream.off('unfollow', this.onStreamUnfollow);
+ this.connection.off('follow', this.onStreamFollow);
+ this.connection.off('unfollow', this.onStreamUnfollow);
+ this.stream.dispose(this.connectionId);
});
this.onStreamFollow = user => {
diff --git a/src/web/app/desktop/tags/donation.tag b/src/web/app/desktop/tags/donation.tag
index 33f377a192..1c19fac1f5 100644
--- a/src/web/app/desktop/tags/donation.tag
+++ b/src/web/app/desktop/tags/donation.tag
@@ -54,11 +54,10 @@
e.preventDefault();
e.stopPropagation();
- this.I.data.no_donation = 'true';
+ this.I.client_settings.show_donation = false;
this.I.update();
- this.api('i/appdata/set', {
- key: 'no_donation',
- value: 'true'
+ this.api('i/update', {
+ show_donation: false
});
this.unmount();
diff --git a/src/web/app/desktop/tags/drive/browser-window.tag b/src/web/app/desktop/tags/drive/browser-window.tag
index dc55371da6..7cd24fc4ad 100644
--- a/src/web/app/desktop/tags/drive/browser-window.tag
+++ b/src/web/app/desktop/tags/drive/browser-window.tag
@@ -1,11 +1,11 @@
<mk-drive-browser-window>
- <mk-window ref="window" is-modal={ false } width={ '800px' } height={ '500px' }>
+ <mk-window ref="window" is-modal={ false } width={ '800px' } height={ '500px' } popout={ popout }>
<yield to="header">
<p class="info" if={ parent.usage }><b>{ parent.usage.toFixed(1) }%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p>
<i class="fa fa-cloud"></i>%i18n:desktop.tags.mk-drive-browser-window.drive%
</yield>
<yield to="content">
- <mk-drive-browser multiple={ true } folder={ parent.folder }/>
+ <mk-drive-browser multiple={ true } folder={ parent.folder } ref="browser"/>
</yield>
</mk-window>
<style>
@@ -32,6 +32,15 @@
this.folder = this.opts.folder ? this.opts.folder : null;
+ this.popout = () => {
+ const folder = this.refs.window.refs.browser.folder;
+ if (folder) {
+ return `${_URL_}/i/drive/folder/${folder.id}`;
+ } else {
+ return `${_URL_}/i/drive`;
+ }
+ };
+
this.on('mount', () => {
this.refs.window.on('closed', () => {
this.unmount();
diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index 93db0a04d7..6b756b9952 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -2,7 +2,8 @@
<nav>
<div class="path" oncontextmenu={ pathOncontextmenu }>
<mk-drive-browser-nav-folder class={ current: folder == null } folder={ null }/>
- <virtual each={ folder in hierarchyFolders }><span class="separator"><i class="fa fa-angle-right"></i></span>
+ <virtual each={ folder in hierarchyFolders }>
+ <span class="separator"><i class="fa fa-angle-right"></i></span>
<mk-drive-browser-nav-folder folder={ folder }/>
</virtual>
<span class="separator" if={ folder != null }><i class="fa fa-angle-right"></i></span>
@@ -17,12 +18,14 @@
<virtual each={ folder in folders }>
<mk-drive-browser-folder class="folder" folder={ folder }/>
</virtual>
+ <div class="padding" each={ folders }></div>
<button if={ moreFolders }>%i18n:desktop.tags.mk-drive-browser.load-more%</button>
</div>
<div class="files" ref="filesContainer" if={ files.length > 0 }>
<virtual each={ file in files }>
<mk-drive-browser-file class="file" file={ file }/>
</virtual>
+ <div class="padding" each={ files }></div>
<button if={ moreFiles } onclick={ fetchMoreFiles }>%i18n:desktop.tags.mk-drive-browser.load-more%</button>
</div>
<div class="empty" if={ files.length == 0 && folders.length == 0 && !fetching }>
@@ -160,22 +163,20 @@
> .contents
> .folders
- &:after
- content ""
- display block
- clear both
-
- > .folder
- float left
-
> .files
- &:after
- content ""
- display block
- clear both
+ display flex
+ flex-wrap wrap
+ > .folder
> .file
- float left
+ flex-grow 1
+ width 144px
+ margin 4px
+
+ > .padding
+ flex-grow 1
+ pointer-events none
+ width 144px + 8px // 8px is margin
> .empty
padding 16px
@@ -246,7 +247,10 @@
this.mixin('i');
this.mixin('api');
- this.mixin('stream');
+
+ this.mixin('drive-stream');
+ this.connection = this.driveStream.getConnection();
+ this.connectionId = this.driveStream.use();
this.files = [];
this.folders = [];
@@ -279,10 +283,10 @@
});
});
- this.stream.on('drive_file_created', this.onStreamDriveFileCreated);
- this.stream.on('drive_file_updated', this.onStreamDriveFileUpdated);
- this.stream.on('drive_folder_created', this.onStreamDriveFolderCreated);
- this.stream.on('drive_folder_updated', this.onStreamDriveFolderUpdated);
+ this.connection.on('file_created', this.onStreamDriveFileCreated);
+ this.connection.on('file_updated', this.onStreamDriveFileUpdated);
+ this.connection.on('folder_created', this.onStreamDriveFolderCreated);
+ this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
if (this.opts.folder) {
this.move(this.opts.folder);
@@ -292,10 +296,11 @@
});
this.on('unmount', () => {
- this.stream.off('drive_file_created', this.onStreamDriveFileCreated);
- this.stream.off('drive_file_updated', this.onStreamDriveFileUpdated);
- this.stream.off('drive_folder_created', this.onStreamDriveFolderCreated);
- this.stream.off('drive_folder_updated', this.onStreamDriveFolderUpdated);
+ this.connection.off('file_created', this.onStreamDriveFileCreated);
+ this.connection.off('file_updated', this.onStreamDriveFileUpdated);
+ this.connection.off('folder_created', this.onStreamDriveFolderCreated);
+ this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
+ this.driveStream.dispose(this.connectionId);
});
this.onStreamDriveFileCreated = file => {
@@ -407,7 +412,7 @@
// ドロップされてきたものがファイルだったら
if (e.dataTransfer.files.length > 0) {
- e.dataTransfer.files.forEach(file => {
+ Array.from(e.dataTransfer.files).forEach(file => {
this.upload(file, this.folder);
});
return false;
@@ -509,7 +514,7 @@
};
this.changeFileInput = () => {
- this.refs.fileInput.files.forEach(file => {
+ Array.from(this.refs.fileInput.files).forEach(file => {
this.upload(file, this.folder);
});
};
@@ -571,6 +576,7 @@
if (folder.parent) dive(folder.parent);
this.update();
+ this.trigger('open-folder', folder);
this.fetch();
});
};
@@ -640,6 +646,7 @@
folder: null,
hierarchyFolders: []
});
+ this.trigger('move-root');
this.fetch();
};
diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag
index 64838d6814..0f019d95bf 100644
--- a/src/web/app/desktop/tags/drive/file.tag
+++ b/src/web/app/desktop/tags/drive/file.tag
@@ -5,17 +5,12 @@
<div class="label" if={ I.banner_id == file.id }><img src="/assets/label.svg"/>
<p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p>
</div>
- <div class="label" if={ I.data.wallpaper == file.id }><img src="/assets/label.svg"/>
- <p>%i18n:desktop.tags.mk-drive-browser-file.wallpaper%</p>
- </div>
<div class="thumbnail"><img src={ file.url + '?thumbnail&size=128' } alt=""/></div>
<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
<style>
:scope
display block
- margin 4px
padding 8px 0 0 0
- width 144px
height 180px
border-radius 4px
@@ -116,7 +111,7 @@
> .thumbnail
width 128px
height 128px
- left 8px
+ margin auto
> img
display block
diff --git a/src/web/app/desktop/tags/drive/folder.tag b/src/web/app/desktop/tags/drive/folder.tag
index e03c4e3534..6d2d196258 100644
--- a/src/web/app/desktop/tags/drive/folder.tag
+++ b/src/web/app/desktop/tags/drive/folder.tag
@@ -3,9 +3,7 @@
<style>
:scope
display block
- margin 4px
padding 8px
- width 144px
height 64px
background lighten($theme-color, 95%)
border-radius 4px
@@ -109,7 +107,7 @@
// ファイルだったら
if (e.dataTransfer.files.length > 0) {
- e.dataTransfer.files.forEach(file => {
+ Array.from(e.dataTransfer.files).forEach(file => {
this.browser.upload(file, this.folder);
});
return false;
diff --git a/src/web/app/desktop/tags/drive/nav-folder.tag b/src/web/app/desktop/tags/drive/nav-folder.tag
index c89d9edc1c..0a9421353c 100644
--- a/src/web/app/desktop/tags/drive/nav-folder.tag
+++ b/src/web/app/desktop/tags/drive/nav-folder.tag
@@ -55,7 +55,7 @@
// ファイルだったら
if (e.dataTransfer.files.length > 0) {
- e.dataTransfer.files.forEach(file => {
+ Array.from(e.dataTransfer.files).forEach(file => {
this.browser.upload(file, this.folder);
});
return false;
diff --git a/src/web/app/desktop/tags/follow-button.tag b/src/web/app/desktop/tags/follow-button.tag
index 00ff686f69..a1cbc191d8 100644
--- a/src/web/app/desktop/tags/follow-button.tag
+++ b/src/web/app/desktop/tags/follow-button.tag
@@ -71,7 +71,10 @@
this.mixin('i');
this.mixin('api');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.user = null;
this.userPromise = isPromise(this.opts.user)
@@ -86,14 +89,15 @@
init: false,
user: user
});
- this.stream.on('follow', this.onStreamFollow);
- this.stream.on('unfollow', this.onStreamUnfollow);
+ this.connection.on('follow', this.onStreamFollow);
+ this.connection.on('unfollow', this.onStreamUnfollow);
});
});
this.on('unmount', () => {
- this.stream.off('follow', this.onStreamFollow);
- this.stream.off('unfollow', this.onStreamUnfollow);
+ this.connection.off('follow', this.onStreamFollow);
+ this.connection.off('unfollow', this.onStreamUnfollow);
+ this.stream.dispose(this.connectionId);
});
this.onStreamFollow = user => {
diff --git a/src/web/app/desktop/tags/home-widgets/access-log.tag b/src/web/app/desktop/tags/home-widgets/access-log.tag
new file mode 100644
index 0000000000..44f1cadf4b
--- /dev/null
+++ b/src/web/app/desktop/tags/home-widgets/access-log.tag
@@ -0,0 +1,95 @@
+<mk-access-log-home-widget>
+ <virtual if={ data.design == 0 }>
+ <p class="title"><i class="fa fa-server"></i>%i18n:desktop.tags.mk-access-log-home-widget.title%</p>
+ </virtual>
+ <div ref="log">
+ <p each={ requests }>
+ <span class="ip" style="color:{ fg }; background:{ bg }">{ ip }</span>
+ <span>{ method }</span>
+ <span>{ path }</span>
+ </p>
+ </div>
+ <style>
+ :scope
+ display block
+ overflow hidden
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ > .title
+ z-index 1
+ margin 0
+ padding 0 16px
+ line-height 42px
+ font-size 0.9em
+ font-weight bold
+ color #888
+ box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+ > i
+ margin-right 4px
+
+ > div
+ max-height 250px
+ overflow auto
+
+ > p
+ margin 0
+ padding 8px
+ font-size 0.8em
+ color #555
+
+ &:nth-child(odd)
+ background rgba(0, 0, 0, 0.025)
+
+ > .ip
+ margin-right 4px
+
+ </style>
+ <script>
+ import seedrandom from 'seedrandom';
+
+ this.data = {
+ design: 0
+ };
+
+ this.mixin('widget');
+
+ this.mixin('requests-stream');
+ this.connection = this.requestsStream.getConnection();
+ this.connectionId = this.requestsStream.use();
+
+ this.requests = [];
+
+ this.on('mount', () => {
+ this.connection.on('request', this.onRequest);
+ });
+
+ this.on('unmount', () => {
+ this.connection.off('request', this.onRequest);
+ this.requestsStream.dispose(this.connectionId);
+ });
+
+ this.onRequest = request => {
+ const random = seedrandom(request.ip);
+ const r = Math.floor(random() * 255);
+ const g = Math.floor(random() * 255);
+ const b = Math.floor(random() * 255);
+ const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b); // SMPTE C, Rec. 709 weightings
+ request.bg = `rgb(${r}, ${g}, ${b})`;
+ request.fg = luma >= 165 ? '#000' : '#fff';
+
+ this.requests.push(request);
+ if (this.requests.length > 30) this.requests.shift();
+ this.update();
+
+ this.refs.log.scrollTop = this.refs.log.scrollHeight;
+ };
+
+ this.func = () => {
+ if (++this.data.design == 2) this.data.design = 0;
+ this.save();
+ };
+ </script>
+</mk-access-log-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/activity.tag b/src/web/app/desktop/tags/home-widgets/activity.tag
index 8bd8bfb2aa..2274e84162 100644
--- a/src/web/app/desktop/tags/home-widgets/activity.tag
+++ b/src/web/app/desktop/tags/home-widgets/activity.tag
@@ -1,234 +1,32 @@
<mk-activity-home-widget>
- <p class="title"><i class="fa fa-bar-chart"></i>%i18n:desktop.tags.mk-activity-home-widget.title%</p>
- <button onclick={ toggle } title="%i18n:desktop.tags.mk-activity-home-widget.toggle%"><i class="fa fa-sort"></i></button>
- <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p>
- <mk-activity-home-widget-calender if={ !initializing && view == 0 } data={ [].concat(data) }/>
- <mk-activity-home-widget-chart if={ !initializing && view == 1 } data={ [].concat(data) }/>
+ <mk-activity-widget design={ data.design } view={ data.view } user={ I } ref="activity"/>
<style>
:scope
display block
- background #fff
-
- > .title
- z-index 1
- margin 0
- padding 0 16px
- line-height 42px
- font-size 0.9em
- font-weight bold
- color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
- > i
- margin-right 4px
-
- > button
- position absolute
- z-index 2
- top 0
- right 0
- padding 0
- width 42px
- font-size 0.9em
- line-height 42px
- color #ccc
-
- &:hover
- color #aaa
-
- &:active
- color #999
-
- > .initializing
- margin 0
- padding 16px
- text-align center
- color #aaa
-
- > i
- margin-right 4px
-
</style>
<script>
- this.mixin('i');
- this.mixin('api');
-
- this.initializing = true;
- this.view = 0;
-
- this.on('mount', () => {
- this.api('aggregation/users/activity', {
- user_id: this.I.id,
- limit: 20 * 7
- }).then(data => {
- this.update({
- initializing: false,
- data
- });
- });
- });
-
- this.toggle = () => {
- this.view++;
- if (this.view == 2) this.view = 0;
+ this.data = {
+ view: 0,
+ design: 0
};
- </script>
-</mk-activity-home-widget>
-<mk-activity-home-widget-calender>
- <svg viewBox="0 0 21 7" preserveAspectRatio="none">
- <rect each={ data } class="day"
- width="1" height="1"
- riot-x={ x } riot-y={ date.weekday }
- rx="1" ry="1"
- fill="transparent">
- <title>{ date.year }/{ date.month }/{ date.day }<br/>Post: { posts }, Reply: { replies }, Repost: { reposts }</title>
- </rect>
- <rect each={ data }
- width="1" height="1"
- riot-x={ x } riot-y={ date.weekday }
- rx="1" ry="1"
- fill={ color }
- style="pointer-events: none; transform: scale({ v });"/>
- <rect class="today"
- width="1" height="1"
- riot-x={ data[data.length - 1].x } riot-y={ data[data.length - 1].date.weekday }
- rx="1" ry="1"
- fill="none"
- stroke-width="0.1"
- stroke="#f73520"/>
- </svg>
- <style>
- :scope
- display block
-
- > svg
- display block
- padding 10px
- width 100%
-
- > rect
- transform-origin center
-
- &.day
- &:hover
- fill rgba(0, 0, 0, 0.05)
-
- </style>
- <script>
- this.data = this.opts.data;
- this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
- const peak = Math.max.apply(null, this.data.map(d => d.total));
+ this.mixin('widget');
- let x = 0;
- this.data.reverse().forEach(d => {
- d.x = x;
- d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();
-
- d.v = d.total / (peak / 2);
- if (d.v > 1) d.v = 1;
- const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
- const cs = d.v * 100;
- const cl = 15 + ((1 - d.v) * 80);
- d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
-
- if (d.date.weekday == 6) x++;
- });
- </script>
-</mk-activity-home-widget-calender>
-
-<mk-activity-home-widget-chart>
- <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none" onmousedown={ onMousedown }>
- <title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title>
- <polyline
- riot-points={ pointsPost }
- fill="none"
- stroke-width="1"
- stroke="#41ddde"/>
- <polyline
- riot-points={ pointsReply }
- fill="none"
- stroke-width="1"
- stroke="#f7796c"/>
- <polyline
- riot-points={ pointsRepost }
- fill="none"
- stroke-width="1"
- stroke="#a1de41"/>
- <polyline
- riot-points={ pointsTotal }
- fill="none"
- stroke-width="1"
- stroke="#555"
- stroke-dasharray="2 2"/>
- </svg>
- <style>
- :scope
- display block
-
- > svg
- display block
- padding 10px
- width 100%
- cursor all-scroll
- </style>
- <script>
- this.viewBoxX = 140;
- this.viewBoxY = 60;
- this.zoom = 1;
- this.pos = 0;
-
- this.data = this.opts.data.reverse();
- this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
- const peak = Math.max.apply(null, this.data.map(d => d.total));
+ this.initializing = true;
this.on('mount', () => {
- this.render();
- });
-
- this.render = () => {
- this.update({
- pointsPost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' '),
- pointsReply: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '),
- pointsRepost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' '),
- pointsTotal: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ')
+ this.refs.activity.on('view-changed', view => {
+ this.data.view = view;
+ this.save();
});
- };
-
- this.onMousedown = e => {
- e.preventDefault();
-
- const clickX = e.clientX;
- const clickY = e.clientY;
- const baseZoom = this.zoom;
- const basePos = this.pos;
-
- // 動かした時
- dragListen(me => {
- let moveLeft = me.clientX - clickX;
- let moveTop = me.clientY - clickY;
-
- this.zoom = baseZoom + (-moveTop / 20);
- this.pos = basePos + moveLeft;
- if (this.zoom < 1) this.zoom = 1;
- if (this.pos > 0) this.pos = 0;
- if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
+ });
- this.render();
+ this.func = () => {
+ if (++this.data.design == 3) this.data.design = 0;
+ this.refs.activity.update({
+ design: this.data.design
});
+ this.save();
};
-
- function dragListen(fn) {
- window.addEventListener('mousemove', fn);
- window.addEventListener('mouseleave', dragClear.bind(null, fn));
- window.addEventListener('mouseup', dragClear.bind(null, fn));
- }
-
- function dragClear(fn) {
- window.removeEventListener('mousemove', fn);
- window.removeEventListener('mouseleave', dragClear);
- window.removeEventListener('mouseup', dragClear);
- }
</script>
-</mk-activity-home-widget-chart>
-
+</mk-activity-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/broadcast.tag b/src/web/app/desktop/tags/home-widgets/broadcast.tag
index 1102e22c7f..6f4bb0756d 100644
--- a/src/web/app/desktop/tags/home-widgets/broadcast.tag
+++ b/src/web/app/desktop/tags/home-widgets/broadcast.tag
@@ -1,4 +1,4 @@
-<mk-broadcast-home-widget>
+<mk-broadcast-home-widget data-found={ broadcasts.length != 0 } data-melt={ data.design == 1 }>
<div class="icon">
<svg height="32" version="1.1" viewBox="0 0 32 32" width="32">
<path class="tower" d="M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z"></path>
@@ -8,14 +8,27 @@
<path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path>
</svg>
</div>
- <h1>開発者募集中!</h1>
- <p><a href="https://github.com/syuilo/misskey" target="_blank">Misskeyはオープンソースで開発されています。リポジトリはこちら。</a></p>
+ <p class="fetching" if={ fetching }>%i18n:desktop.tags.mk-broadcast-home-widget.fetching%<mk-ellipsis/></p>
+ <h1 if={ !fetching }>{
+ broadcasts.length == 0 ? '%i18n:desktop.tags.mk-broadcast-home-widget.no-broadcasts%' : broadcasts[i].title
+ }</h1>
+ <p if={ !fetching }><mk-raw if={ broadcasts.length != 0 } content={ broadcasts[i].text }/><virtual if={ broadcasts.length == 0 }>%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</virtual></p>
+ <a if={ broadcasts.length > 1 } onclick={ next }>%i18n:desktop.tags.mk-broadcast-home-widget.next% &gt;&gt;</a>
<style>
:scope
display block
- padding 10px 10px 10px 50px
- background transparent
- border-color #4078c0 !important
+ padding 10px
+ border solid 1px #4078c0
+ border-radius 6px
+
+ &[data-melt]
+ border none
+
+ &[data-found]
+ padding-left 50px
+
+ > .icon
+ display block
&:after
content ""
@@ -23,7 +36,7 @@
clear both
> .icon
- display block
+ display none
float left
margin-left -40px
@@ -72,12 +85,59 @@
font-size 0.7em
color #555
+ &.fetching
+ text-align center
+
a
color #555
+ text-decoration underline
-
-
-
+ > a
+ display block
+ font-size 0.7em
</style>
+ <script>
+ this.data = {
+ design: 0
+ };
+
+ this.mixin('widget');
+ this.mixin('os');
+
+ this.i = 0;
+ this.fetching = true;
+ this.broadcasts = [];
+
+ this.on('mount', () => {
+ this.mios.getMeta().then(meta => {
+ let broadcasts = [];
+ if (meta.broadcasts) {
+ meta.broadcasts.forEach(broadcast => {
+ if (broadcast[_LANG_]) {
+ broadcasts.push(broadcast[_LANG_]);
+ }
+ });
+ }
+ this.update({
+ fetching: false,
+ broadcasts: broadcasts
+ });
+ });
+ });
+
+ this.next = () => {
+ if (this.i == this.broadcasts.length - 1) {
+ this.i = 0;
+ } else {
+ this.i++;
+ }
+ this.update();
+ };
+
+ this.func = () => {
+ if (++this.data.design == 2) this.data.design = 0;
+ this.save();
+ };
+ </script>
</mk-broadcast-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/calendar.tag b/src/web/app/desktop/tags/home-widgets/calendar.tag
index 9aa4ac6326..fded57e07a 100644
--- a/src/web/app/desktop/tags/home-widgets/calendar.tag
+++ b/src/web/app/desktop/tags/home-widgets/calendar.tag
@@ -1,4 +1,4 @@
-<mk-calendar-home-widget data-special={ special }>
+<mk-calendar-home-widget data-melt={ data.design == 1 } data-special={ special }>
<div class="calendar" data-is-holiday={ isHoliday }>
<p class="month-and-year"><span class="year">{ year }年</span><span class="month">{ month }月</span></p>
<p class="day">{ day }日</p>
@@ -30,9 +30,15 @@
padding 16px 0
color #777
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
&[data-special='on-new-years-day']
- border-color #ef95a0 !important
+ border-color #ef95a0
+
+ &[data-melt]
+ background transparent
+ border none
&:after
content ""
@@ -106,6 +112,12 @@
</style>
<script>
+ this.data = {
+ design: 0
+ };
+
+ this.mixin('widget');
+
this.draw = () => {
const now = new Date();
const nd = now.getDate();
@@ -130,7 +142,7 @@
this.isHoliday = now.getDay() == 0 || now.getDay() == 6;
- this.special =
+ this.special =
nm == 0 && nd == 1 ? 'on-new-years-day' :
false;
@@ -146,5 +158,10 @@
this.on('unmount', () => {
clearInterval(this.clock);
});
+
+ this.func = () => {
+ if (++this.data.design == 2) this.data.design = 0;
+ this.save();
+ };
</script>
</mk-calendar-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/channel.tag b/src/web/app/desktop/tags/home-widgets/channel.tag
new file mode 100644
index 0000000000..f22a5f76ef
--- /dev/null
+++ b/src/web/app/desktop/tags/home-widgets/channel.tag
@@ -0,0 +1,318 @@
+<mk-channel-home-widget>
+ <virtual if={ !data.compact }>
+ <p class="title"><i class="fa fa-television"></i>{
+ channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%'
+ }</p>
+ <button onclick={ settings } title="%i18n:desktop.tags.mk-channel-home-widget.settings%"><i class="fa fa-cog"></i></button>
+ </virtual>
+ <p class="get-started" if={ this.data.channel == null }>%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
+ <mk-channel ref="channel" show={ this.data.channel }/>
+ <style>
+ :scope
+ display block
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+ overflow hidden
+
+ > .title
+ z-index 2
+ margin 0
+ padding 0 16px
+ line-height 42px
+ font-size 0.9em
+ font-weight bold
+ color #888
+ box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+ > i
+ margin-right 4px
+
+ > button
+ position absolute
+ z-index 2
+ top 0
+ right 0
+ padding 0
+ width 42px
+ font-size 0.9em
+ line-height 42px
+ color #ccc
+
+ &:hover
+ color #aaa
+
+ &:active
+ color #999
+
+ > .get-started
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > mk-channel
+ height 200px
+
+ </style>
+ <script>
+ this.data = {
+ channel: null,
+ compact: false
+ };
+
+ this.mixin('widget');
+
+ this.on('mount', () => {
+ if (this.data.channel) {
+ this.zap();
+ }
+ });
+
+ this.zap = () => {
+ this.update({
+ fetching: true
+ });
+
+ this.api('channels/show', {
+ channel_id: this.data.channel
+ }).then(channel => {
+ this.update({
+ fetching: false,
+ channel: channel
+ });
+
+ this.refs.channel.zap(channel);
+ });
+ };
+
+ this.settings = () => {
+ const id = window.prompt('チャンネルID');
+ if (!id) return;
+ this.data.channel = id;
+ this.zap();
+
+ // Save state
+ this.save();
+ };
+
+ this.func = () => {
+ this.data.compact = !this.data.compact;
+ this.save();
+ };
+ </script>
+</mk-channel-home-widget>
+
+<mk-channel>
+ <p if={ fetching }>読み込み中<mk-ellipsis/></p>
+ <div if={ !fetching } ref="posts">
+ <p if={ posts.length == 0 }>まだ投稿がありません</p>
+ <mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
+ </div>
+ <mk-channel-form ref="form"/>
+ <style>
+ :scope
+ display block
+
+ > p
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > div
+ height calc(100% - 38px)
+ overflow auto
+ font-size 0.9em
+
+ > mk-channel-post
+ border-bottom solid 1px #eee
+
+ &:last-child
+ border-bottom none
+
+ > mk-channel-form
+ position absolute
+ left 0
+ bottom 0
+
+ </style>
+ <script>
+ import ChannelStream from '../../../common/scripts/streaming/channel-stream';
+
+ this.mixin('api');
+
+ this.fetching = true;
+ this.channel = null;
+ this.posts = [];
+
+ this.on('unmount', () => {
+ if (this.connection) {
+ this.connection.off('post', this.onPost);
+ this.connection.close();
+ }
+ });
+
+ this.zap = channel => {
+ this.update({
+ fetching: true,
+ channel: channel
+ });
+
+ this.api('channels/posts', {
+ channel_id: channel.id
+ }).then(posts => {
+ this.update({
+ fetching: false,
+ posts: posts
+ });
+
+ this.scrollToBottom();
+
+ if (this.connection) {
+ this.connection.off('post', this.onPost);
+ this.connection.close();
+ }
+ this.connection = new ChannelStream(this.channel.id);
+ this.connection.on('post', this.onPost);
+ });
+ };
+
+ this.onPost = post => {
+ this.posts.unshift(post);
+ this.update();
+ this.scrollToBottom();
+ };
+
+ this.scrollToBottom = () => {
+ this.refs.posts.scrollTop = this.refs.posts.scrollHeight;
+ };
+ </script>
+</mk-channel>
+
+<mk-channel-post>
+ <header>
+ <a class="index" onclick={ reply }>{ post.index }:</a>
+ <a class="name" href={ _URL_ + '/' + post.user.username }><b>{ post.user.name }</b></a>
+ <span>ID:<i>{ post.user.username }</i></span>
+ </header>
+ <div>
+ <a if={ post.reply }>&gt;&gt;{ post.reply.index }</a>
+ { post.text }
+ <div class="media" if={ post.media }>
+ <virtual 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>
+ </virtual>
+ </div>
+ </div>
+ <style>
+ :scope
+ display block
+ margin 0
+ padding 0
+ color #444
+
+ > header
+ position -webkit-sticky
+ position sticky
+ z-index 1
+ top 0
+ padding 8px 4px 4px 16px
+ background rgba(255, 255, 255, 0.9)
+
+ > .index
+ margin-right 0.25em
+
+ > .name
+ margin-right 0.5em
+ color #008000
+
+ > div
+ padding 0 16px 16px 16px
+
+ > .media
+ > a
+ display inline-block
+
+ > img
+ max-width 100%
+ vertical-align bottom
+
+ </style>
+ <script>
+ this.post = this.opts.post;
+ this.form = this.opts.form;
+
+ this.reply = () => {
+ this.form.refs.text.value = `>>${ this.post.index } `;
+ };
+ </script>
+</mk-channel-post>
+
+<mk-channel-form>
+ <input ref="text" disabled={ wait } onkeydown={ onkeydown } placeholder="書いて">
+ <style>
+ :scope
+ display block
+ width 100%
+ height 38px
+ padding 4px
+ border-top solid 1px #ddd
+
+ > input
+ padding 0 8px
+ width 100%
+ height 100%
+ font-size 14px
+ color #55595c
+ border solid 1px #dadada
+ border-radius 4px
+
+ &:hover
+ &:focus
+ border-color #aeaeae
+
+ </style>
+ <script>
+ this.mixin('api');
+
+ this.clear = () => {
+ this.refs.text.value = '';
+ };
+
+ this.onkeydown = e => {
+ if (e.which == 10 || e.which == 13) this.post();
+ };
+
+ this.post = () => {
+ this.update({
+ wait: true
+ });
+
+ let text = this.refs.text.value;
+ let reply = null;
+
+ if (/^>>([0-9]+) /.test(text)) {
+ const index = text.match(/^>>([0-9]+) /)[1];
+ reply = this.parent.posts.find(p => p.index.toString() == index);
+ text = text.replace(/^>>([0-9]+) /, '');
+ }
+
+ this.api('posts/create', {
+ text: text,
+ reply_id: reply ? reply.id : undefined,
+ channel_id: this.parent.channel.id
+ }).then(data => {
+ this.clear();
+ }).catch(err => {
+ alert('失敗した');
+ }).then(() => {
+ this.update({
+ wait: false
+ });
+ });
+ };
+ </script>
+</mk-channel-form>
diff --git a/src/web/app/desktop/tags/home-widgets/donation.tag b/src/web/app/desktop/tags/home-widgets/donation.tag
index d533e82831..99ded1b5d4 100644
--- a/src/web/app/desktop/tags/home-widgets/donation.tag
+++ b/src/web/app/desktop/tags/home-widgets/donation.tag
@@ -7,7 +7,8 @@
:scope
display block
background #fff
- border-color #ead8bb !important
+ border solid 1px #ead8bb
+ border-radius 6px
> article
padding 20px
@@ -28,5 +29,8 @@
color #999
</style>
- <script>this.mixin('user-preview');</script>
+ <script>
+ this.mixin('widget');
+ this.mixin('user-preview');
+ </script>
</mk-donation-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag
index b94e9b04c5..257afc4a8c 100644
--- a/src/web/app/desktop/tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/tags/home-widgets/mentions.tag
@@ -9,6 +9,8 @@
:scope
display block
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
> header
padding 8px 16px
diff --git a/src/web/app/desktop/tags/home-widgets/messaging.tag b/src/web/app/desktop/tags/home-widgets/messaging.tag
new file mode 100644
index 0000000000..52251aa539
--- /dev/null
+++ b/src/web/app/desktop/tags/home-widgets/messaging.tag
@@ -0,0 +1,52 @@
+<mk-messaging-home-widget>
+ <virtual if={ data.design == 0 }>
+ <p class="title"><i class="fa fa-comments"></i>%i18n:desktop.tags.mk-messaging-home-widget.title%</p>
+ </virtual>
+ <mk-messaging ref="index" compact={ true }/>
+ <style>
+ :scope
+ display block
+ overflow hidden
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ > .title
+ z-index 2
+ margin 0
+ padding 0 16px
+ line-height 42px
+ font-size 0.9em
+ font-weight bold
+ color #888
+ box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+ > i
+ margin-right 4px
+
+ > mk-messaging
+ max-height 250px
+ overflow auto
+
+ </style>
+ <script>
+ this.data = {
+ design: 0
+ };
+
+ this.mixin('widget');
+
+ this.on('mount', () => {
+ this.refs.index.on('navigate-user', user => {
+ riot.mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
+ user: user
+ });
+ });
+ });
+
+ this.func = () => {
+ if (++this.data.design == 2) this.data.design = 0;
+ this.save();
+ };
+ </script>
+</mk-messaging-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/nav.tag b/src/web/app/desktop/tags/home-widgets/nav.tag
index 54bfb87a11..61c0b4cb55 100644
--- a/src/web/app/desktop/tags/home-widgets/nav.tag
+++ b/src/web/app/desktop/tags/home-widgets/nav.tag
@@ -1,4 +1,5 @@
-<mk-nav-home-widget><a href={ CONFIG.aboutUrl }>%i18n:desktop.tags.mk-nav-home-widget.about%</a><i>・</i><a href={ CONFIG.statsUrl }>%i18n:desktop.tags.mk-nav-home-widget.stats%</a><i>・</i><a href={ CONFIG.statusUrl }>%i18n:desktop.tags.mk-nav-home-widget.status%</a><i>・</i><a href="http://zawazawa.jp/misskey/">%i18n:desktop.tags.mk-nav-home-widget.wiki%</a><i>・</i><a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:desktop.tags.mk-nav-home-widget.donors%</a><i>・</i><a href="https://github.com/syuilo/misskey">%i18n:desktop.tags.mk-nav-home-widget.repository%</a><i>・</i><a href={ CONFIG.devUrl }>%i18n:desktop.tags.mk-nav-home-widget.develop%</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on <i class="fa fa-twitter"></i></a>
+<mk-nav-home-widget>
+ <mk-nav-links/>
<style>
:scope
display block
@@ -6,6 +7,8 @@
font-size 12px
color #aaa
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
a
color #999
@@ -14,4 +17,7 @@
color #ccc
</style>
+ <script>
+ this.mixin('widget');
+ </script>
</mk-nav-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/notifications.tag b/src/web/app/desktop/tags/home-widgets/notifications.tag
index b1170855ac..dadafa660a 100644
--- a/src/web/app/desktop/tags/home-widgets/notifications.tag
+++ b/src/web/app/desktop/tags/home-widgets/notifications.tag
@@ -1,11 +1,15 @@
<mk-notifications-home-widget>
- <p class="title"><i class="fa fa-bell-o"></i>%i18n:desktop.tags.mk-notifications-home-widget.title%</p>
- <button onclick={ settings } title="%i18n:desktop.tags.mk-notifications-home-widget.settings%"><i class="fa fa-cog"></i></button>
+ <virtual if={ !data.compact }>
+ <p class="title"><i class="fa fa-bell-o"></i>%i18n:desktop.tags.mk-notifications-home-widget.title%</p>
+ <button onclick={ settings } title="%i18n:desktop.tags.mk-notifications-home-widget.settings%"><i class="fa fa-cog"></i></button>
+ </virtual>
<mk-notifications/>
<style>
:scope
display block
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
> .title
z-index 1
@@ -43,9 +47,20 @@
</style>
<script>
+ this.data = {
+ compact: false
+ };
+
+ this.mixin('widget');
+
this.settings = () => {
const w = riot.mount(document.body.appendChild(document.createElement('mk-settings-window')))[0];
w.switch('notification');
};
+
+ this.func = () => {
+ this.data.compact = !this.data.compact;
+ this.save();
+ };
</script>
</mk-notifications-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/photo-stream.tag b/src/web/app/desktop/tags/home-widgets/photo-stream.tag
index d1f29589f3..05658c9025 100644
--- a/src/web/app/desktop/tags/home-widgets/photo-stream.tag
+++ b/src/web/app/desktop/tags/home-widgets/photo-stream.tag
@@ -1,5 +1,7 @@
-<mk-photo-stream-home-widget>
- <p class="title"><i class="fa fa-camera"></i>%i18n:desktop.tags.mk-photo-stream-home-widget.title%</p>
+<mk-photo-stream-home-widget data-melt={ data.design == 2 }>
+ <virtual if={ data.design == 0 }>
+ <p class="title"><i class="fa fa-camera"></i>%i18n:desktop.tags.mk-photo-stream-home-widget.title%</p>
+ </virtual>
<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p>
<div class="stream" if={ !initializing && images.length > 0 }>
<virtual each={ image in images }>
@@ -11,6 +13,19 @@
:scope
display block
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ &[data-melt]
+ background transparent !important
+ border none !important
+
+ > .stream
+ padding 0
+
+ > .img
+ border solid 4px transparent
+ border-radius 8px
> .title
z-index 1
@@ -55,15 +70,21 @@
</style>
<script>
- this.mixin('i');
- this.mixin('api');
+ this.data = {
+ design: 0
+ };
+
+ this.mixin('widget');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.images = [];
this.initializing = true;
this.on('mount', () => {
- this.stream.on('drive_file_created', this.onStreamDriveFileCreated);
+ this.connection.on('drive_file_created', this.onStreamDriveFileCreated);
this.api('drive/stream', {
type: 'image/*',
@@ -77,7 +98,8 @@
});
this.on('unmount', () => {
- this.stream.off('drive_file_created', this.onStreamDriveFileCreated);
+ this.connection.off('drive_file_created', this.onStreamDriveFileCreated);
+ this.stream.dispose(this.connectionId);
});
this.onStreamDriveFileCreated = file => {
@@ -87,5 +109,10 @@
this.update();
}
};
+
+ this.func = () => {
+ if (++this.data.design == 3) this.data.design = 0;
+ this.save();
+ };
</script>
</mk-photo-stream-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/post-form.tag b/src/web/app/desktop/tags/home-widgets/post-form.tag
new file mode 100644
index 0000000000..9ca7fecfe7
--- /dev/null
+++ b/src/web/app/desktop/tags/home-widgets/post-form.tag
@@ -0,0 +1,103 @@
+<mk-post-form-home-widget>
+ <mk-post-form if={ place == 'main' }/>
+ <virtual if={ place != 'main' }>
+ <virtual if={ data.design == 0 }>
+ <p class="title"><i class="fa fa-pencil"></i>%i18n:desktop.tags.mk-post-form-home-widget.title%</p>
+ </virtual>
+ <textarea disabled={ posting } ref="text" onkeydown={ onkeydown } placeholder="%i18n:desktop.tags.mk-post-form-home-widget.placeholder%"></textarea>
+ <button onclick={ post } disabled={ posting }>%i18n:desktop.tags.mk-post-form-home-widget.post%</button>
+ </virtual>
+ <style>
+ :scope
+ display block
+ background #fff
+ overflow hidden
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ > .title
+ z-index 1
+ margin 0
+ padding 0 16px
+ line-height 42px
+ font-size 0.9em
+ font-weight bold
+ color #888
+ box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+ > i
+ margin-right 4px
+
+ > textarea
+ display block
+ width 100%
+ max-width 100%
+ min-width 100%
+ padding 16px
+ margin-bottom 28px + 16px
+ border none
+ border-bottom solid 1px #eee
+
+ > button
+ display block
+ position absolute
+ bottom 8px
+ right 8px
+ margin 0
+ padding 0 10px
+ height 28px
+ color $theme-color-foreground
+ background $theme-color !important
+ outline none
+ border none
+ border-radius 4px
+ transition background 0.1s ease
+ cursor pointer
+
+ &:hover
+ background lighten($theme-color, 10%) !important
+
+ &:active
+ background darken($theme-color, 10%) !important
+ transition background 0s ease
+
+ </style>
+ <script>
+ this.data = {
+ design: 0
+ };
+
+ this.mixin('widget');
+
+ this.func = () => {
+ if (++this.data.design == 2) this.data.design = 0;
+ this.save();
+ };
+
+ this.onkeydown = e => {
+ if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
+ };
+
+ this.post = () => {
+ this.update({
+ posting: true
+ });
+
+ this.api('posts/create', {
+ text: this.refs.text.value
+ }).then(data => {
+ this.clear();
+ }).catch(err => {
+ alert('失敗した');
+ }).then(() => {
+ this.update({
+ posting: false
+ });
+ });
+ };
+
+ this.clear = () => {
+ this.refs.text.value = '';
+ };
+ </script>
+</mk-post-form-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/profile.tag b/src/web/app/desktop/tags/home-widgets/profile.tag
index e6a8752113..eb8ba52e84 100644
--- a/src/web/app/desktop/tags/home-widgets/profile.tag
+++ b/src/web/app/desktop/tags/home-widgets/profile.tag
@@ -1,11 +1,56 @@
-<mk-profile-home-widget>
- <div class="banner" style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' } title="クリックでバナー編集" onclick={ setBanner }></div><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } onclick={ setAvatar } alt="avatar" title="クリックでアバター編集" data-user-preview={ I.id }/><a class="name" href={ '/' + I.username }>{ I.name }</a>
+<mk-profile-home-widget data-compact={ data.design == 1 || data.design == 2 } data-melt={ data.design == 2 }>
+ <div class="banner" style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' } title="クリックでバナー編集" onclick={ setBanner }></div>
+ <img class="avatar" src={ I.avatar_url + '?thumbnail&size=96' } onclick={ setAvatar } alt="avatar" title="クリックでアバター編集" data-user-preview={ I.id }/>
+ <a class="name" href={ '/' + I.username }>{ I.name }</a>
<p class="username">@{ I.username }</p>
<style>
:scope
display block
overflow hidden
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ &[data-compact]
+ > .banner:before
+ content ""
+ display block
+ width 100%
+ height 100%
+ background rgba(0, 0, 0, 0.5)
+
+ > .avatar
+ top ((100px - 58px) / 2)
+ left ((100px - 58px) / 2)
+ border none
+ border-radius 100%
+ box-shadow 0 0 16px rgba(0, 0, 0, 0.5)
+
+ > .name
+ position absolute
+ top 0
+ left 92px
+ margin 0
+ line-height 100px
+ color #fff
+ text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
+
+ > .username
+ display none
+
+ &[data-melt]
+ background transparent !important
+ border none !important
+
+ > .banner
+ visibility hidden
+
+ > .avatar
+ box-shadow none
+
+ > .name
+ color #666
+ text-shadow none
> .banner
height 100px
@@ -47,7 +92,12 @@
import updateAvatar from '../../scripts/update-avatar';
import updateBanner from '../../scripts/update-banner';
- this.mixin('i');
+ this.data = {
+ design: 0
+ };
+
+ this.mixin('widget');
+
this.mixin('user-preview');
this.setAvatar = () => {
@@ -57,5 +107,10 @@
this.setBanner = () => {
updateBanner(this.I);
};
+
+ this.func = () => {
+ if (++this.data.design == 3) this.data.design = 0;
+ this.save();
+ };
</script>
</mk-profile-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
index b724718af7..5bfa839820 100644
--- a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
+++ b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
@@ -1,6 +1,8 @@
<mk-recommended-polls-home-widget>
- <p class="title"><i class="fa fa-pie-chart"></i>%i18n:desktop.tags.mk-recommended-polls-home-widget.title%</p>
- <button onclick={ fetch } title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%"><i class="fa fa-refresh"></i></button>
+ <virtual if={ !data.compact }>
+ <p class="title"><i class="fa fa-pie-chart"></i>%i18n:desktop.tags.mk-recommended-polls-home-widget.title%</p>
+ <button onclick={ fetch } title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%"><i class="fa fa-refresh"></i></button>
+ </virtual>
<div class="poll" if={ !loading && poll != null }>
<p if={ poll.text }><a href="/{ poll.user.username }/{ poll.id }">{ poll.text }</a></p>
<p if={ !poll.text }><a href="/{ poll.user.username }/{ poll.id }"><i class="fa fa-link"></i></a></p>
@@ -12,6 +14,8 @@
:scope
display block
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
> .title
margin 0
@@ -70,7 +74,11 @@
</style>
<script>
- this.mixin('api');
+ this.data = {
+ compact: false
+ };
+
+ this.mixin('widget');
this.poll = null;
this.loading = true;
@@ -102,5 +110,10 @@
});
});
};
+
+ this.func = () => {
+ this.data.compact = !this.data.compact;
+ this.save();
+ };
</script>
</mk-recommended-polls-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/rss-reader.tag b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
index e9b740762e..fe04ee0e20 100644
--- a/src/web/app/desktop/tags/home-widgets/rss-reader.tag
+++ b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
@@ -1,6 +1,8 @@
<mk-rss-reader-home-widget>
- <p class="title"><i class="fa fa-rss-square"></i>RSS</p>
- <button onclick={ settings } title="設定"><i class="fa fa-cog"></i></button>
+ <virtual if={ !data.compact }>
+ <p class="title"><i class="fa fa-rss-square"></i>RSS</p>
+ <button onclick={ settings } title="設定"><i class="fa fa-cog"></i></button>
+ </virtual>
<div class="feed" if={ !initializing }>
<virtual each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></virtual>
</div>
@@ -9,6 +11,8 @@
:scope
display block
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
> .title
margin 0
@@ -62,6 +66,12 @@
</style>
<script>
+ this.data = {
+ compact: false
+ };
+
+ this.mixin('widget');
+
this.url = 'http://news.yahoo.co.jp/pickup/rss.xml';
this.items = [];
this.initializing = true;
@@ -88,5 +98,10 @@
this.settings = () => {
};
+
+ this.func = () => {
+ this.data.compact = !this.data.compact;
+ this.save();
+ };
</script>
</mk-rss-reader-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag
index bc8f313d53..b37d347361 100644
--- a/src/web/app/desktop/tags/home-widgets/server.tag
+++ b/src/web/app/desktop/tags/home-widgets/server.tag
@@ -1,17 +1,25 @@
-<mk-server-home-widget>
- <p class="title"><i class="fa fa-server"></i>%i18n:desktop.tags.mk-server-home-widget.title%</p>
- <button onclick={ toggle } title="%i18n:desktop.tags.mk-server-home-widget.toggle%"><i class="fa fa-sort"></i></button>
+<mk-server-home-widget data-melt={ data.design == 2 }>
+ <virtual if={ data.design == 0 }>
+ <p class="title"><i class="fa fa-server"></i>%i18n:desktop.tags.mk-server-home-widget.title%</p>
+ <button onclick={ toggle } title="%i18n:desktop.tags.mk-server-home-widget.toggle%"><i class="fa fa-sort"></i></button>
+ </virtual>
<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p>
- <mk-server-home-widget-cpu-and-memory-usage if={ !initializing } show={ view == 0 } connection={ connection }/>
- <mk-server-home-widget-cpu if={ !initializing } show={ view == 1 } connection={ connection } meta={ meta }/>
- <mk-server-home-widget-memory if={ !initializing } show={ view == 2 } connection={ connection }/>
- <mk-server-home-widget-disk if={ !initializing } show={ view == 3 } connection={ connection }/>
- <mk-server-home-widget-uptimes if={ !initializing } show={ view == 4 } connection={ connection }/>
- <mk-server-home-widget-info if={ !initializing } show={ view == 5 } connection={ connection } meta={ meta }/>
+ <mk-server-home-widget-cpu-and-memory-usage if={ !initializing } show={ data.view == 0 } connection={ connection }/>
+ <mk-server-home-widget-cpu if={ !initializing } show={ data.view == 1 } connection={ connection } meta={ meta }/>
+ <mk-server-home-widget-memory if={ !initializing } show={ data.view == 2 } connection={ connection }/>
+ <mk-server-home-widget-disk if={ !initializing } show={ data.view == 3 } connection={ connection }/>
+ <mk-server-home-widget-uptimes if={ !initializing } show={ data.view == 4 } connection={ connection }/>
+ <mk-server-home-widget-info if={ !initializing } show={ data.view == 5 } connection={ connection } meta={ meta }/>
<style>
:scope
display block
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ &[data-melt]
+ background transparent !important
+ border none !important
> .title
z-index 1
@@ -54,16 +62,23 @@
</style>
<script>
- import Connection from '../../../common/scripts/server-stream';
+ this.mixin('os');
+
+ this.data = {
+ view: 0,
+ design: 0
+ };
+
+ this.mixin('widget');
- this.mixin('api');
+ this.mixin('server-stream');
+ this.connection = this.serverStream.getConnection();
+ this.connectionId = this.serverStream.use();
this.initializing = true;
- this.view = 0;
- this.connection = new Connection();
this.on('mount', () => {
- this.api('meta').then(meta => {
+ this.mios.getMeta().then(meta => {
this.update({
initializing: false,
meta
@@ -72,12 +87,20 @@
});
this.on('unmount', () => {
- this.connection.close();
+ this.serverStream.dispose(this.connectionId);
});
this.toggle = () => {
- this.view++;
- if (this.view == 6) this.view = 0;
+ this.data.view++;
+ if (this.data.view == 6) this.data.view = 0;
+
+ // Save widget state
+ this.save();
+ };
+
+ this.func = () => {
+ if (++this.data.design == 3) this.data.design = 0;
+ this.save();
};
</script>
</mk-server-home-widget>
@@ -164,7 +187,7 @@
clear both
</style>
<script>
- import uuid from '../../../common/scripts/uuid';
+ import uuid from 'uuid';
this.viewBoxX = 50;
this.viewBoxY = 30;
diff --git a/src/web/app/desktop/tags/home-widgets/slideshow.tag b/src/web/app/desktop/tags/home-widgets/slideshow.tag
new file mode 100644
index 0000000000..4acb680e42
--- /dev/null
+++ b/src/web/app/desktop/tags/home-widgets/slideshow.tag
@@ -0,0 +1,151 @@
+<mk-slideshow-home-widget>
+ <div onclick={ choose }>
+ <p if={ data.folder === undefined }>クリックしてフォルダを指定してください</p>
+ <p if={ data.folder !== undefined && images.length == 0 && !fetching }>このフォルダには画像がありません</p>
+ <div ref="slideA" class="slide a"></div>
+ <div ref="slideB" class="slide b"></div>
+ </div>
+ <button onclick={ resize }><i class="fa fa-expand"></i></button>
+ <style>
+ :scope
+ display block
+ overflow hidden
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ &:hover > button
+ display block
+
+ > button
+ position absolute
+ left 0
+ bottom 0
+ display none
+ padding 4px
+ font-size 24px
+ color #fff
+ text-shadow 0 0 8px #000
+
+ > div
+ width 100%
+ height 100%
+ cursor pointer
+
+ > *
+ pointer-events none
+
+ > .slide
+ position absolute
+ top 0
+ left 0
+ width 100%
+ height 100%
+ background-size cover
+ background-position center
+
+ &.b
+ opacity 0
+
+ </style>
+ <script>
+ import anime from 'animejs';
+
+ this.data = {
+ folder: undefined,
+ size: 0
+ };
+
+ this.mixin('widget');
+
+ this.images = [];
+ this.fetching = true;
+
+ this.on('mount', () => {
+ this.applySize();
+
+ if (this.data.folder !== undefined) {
+ this.fetch();
+ }
+
+ this.clock = setInterval(this.change, 10000);
+ });
+
+ this.on('unmount', () => {
+ clearInterval(this.clock);
+ });
+
+ this.applySize = () => {
+ let h;
+
+ if (this.data.size == 1) {
+ h = 250;
+ } else {
+ h = 170;
+ }
+
+ this.root.style.height = `${h}px`;
+ };
+
+ this.resize = () => {
+ this.data.size++;
+ if (this.data.size == 2) this.data.size = 0;
+
+ this.applySize();
+ this.save();
+ };
+
+ this.change = () => {
+ if (this.images.length == 0) return;
+
+ const index = Math.floor(Math.random() * this.images.length);
+ const img = `url(${ this.images[index].url }?thumbnail&size=1024)`;
+
+ this.refs.slideB.style.backgroundImage = img;
+
+ anime({
+ targets: this.refs.slideB,
+ opacity: 1,
+ duration: 1000,
+ easing: 'linear',
+ complete: () => {
+ this.refs.slideA.style.backgroundImage = img;
+ anime({
+ targets: this.refs.slideB,
+ opacity: 0,
+ duration: 0
+ });
+ }
+ });
+ };
+
+ this.fetch = () => {
+ this.update({
+ fetching: true
+ });
+
+ this.api('drive/files', {
+ folder_id: this.data.folder,
+ type: 'image/*',
+ limit: 100
+ }).then(images => {
+ this.update({
+ fetching: false,
+ images: images
+ });
+ this.refs.slideA.style.backgroundImage = '';
+ this.refs.slideB.style.backgroundImage = '';
+ this.change();
+ });
+ };
+
+ this.choose = () => {
+ const i = riot.mount(document.body.appendChild(document.createElement('mk-select-folder-from-drive-window')))[0];
+ i.one('selected', folder => {
+ this.data.folder = folder ? folder.id : null;
+ this.fetch();
+ this.save();
+ });
+ };
+ </script>
+</mk-slideshow-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag
index 08d96ad715..c751069f74 100644
--- a/src/web/app/desktop/tags/home-widgets/timeline.tag
+++ b/src/web/app/desktop/tags/home-widgets/timeline.tag
@@ -3,12 +3,18 @@
<div class="loading" if={ isLoading }>
<mk-ellipsis-icon/>
</div>
- <p class="empty" if={ isEmpty }><i class="fa fa-comments-o"></i>自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p>
- <mk-timeline ref="timeline"><yield to="footer"><i class="fa fa-moon-o" if={ !parent.moreLoading }></i><i class="fa fa-spinner fa-pulse fa-fw" if={ parent.moreLoading }></i></yield/>
+ <p class="empty" if={ isEmpty && !isLoading }><i class="fa fa-comments-o"></i>自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p>
+ <mk-timeline ref="timeline" hide={ isLoading }>
+ <yield to="footer">
+ <i class="fa fa-moon-o" if={ !parent.moreLoading }></i><i class="fa fa-spinner fa-pulse fa-fw" if={ parent.moreLoading }></i>
+ </yield/>
+ </mk-timeline>
<style>
:scope
display block
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
> mk-following-setuper
border-bottom solid 1px #eee
@@ -34,7 +40,10 @@
<script>
this.mixin('i');
this.mixin('api');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.isLoading = true;
this.isEmpty = false;
@@ -42,9 +51,9 @@
this.noFollowing = this.I.following_count == 0;
this.on('mount', () => {
- this.stream.on('post', this.onStreamPost);
- this.stream.on('follow', this.onStreamFollow);
- this.stream.on('unfollow', this.onStreamUnfollow);
+ this.connection.on('post', this.onStreamPost);
+ this.connection.on('follow', this.onStreamFollow);
+ this.connection.on('unfollow', this.onStreamUnfollow);
document.addEventListener('keydown', this.onDocumentKeydown);
window.addEventListener('scroll', this.onScroll);
@@ -53,9 +62,10 @@
});
this.on('unmount', () => {
- this.stream.off('post', this.onStreamPost);
- this.stream.off('follow', this.onStreamFollow);
- this.stream.off('unfollow', this.onStreamUnfollow);
+ this.connection.off('post', this.onStreamPost);
+ this.connection.off('follow', this.onStreamFollow);
+ this.connection.off('unfollow', this.onStreamUnfollow);
+ this.stream.dispose(this.connectionId);
document.removeEventListener('keydown', this.onDocumentKeydown);
window.removeEventListener('scroll', this.onScroll);
@@ -70,7 +80,13 @@
};
this.load = (cb) => {
- this.api('posts/timeline').then(posts => {
+ this.update({
+ isLoading: true
+ });
+
+ this.api('posts/timeline', {
+ max_date: this.date ? this.date.getTime() : undefined
+ }).then(posts => {
this.update({
isLoading: false,
isEmpty: posts.length == 0
@@ -114,5 +130,13 @@
const current = window.scrollY + window.innerHeight;
if (current > document.body.offsetHeight - 8) this.more();
};
+
+ this.warp = date => {
+ this.update({
+ date: date
+ });
+
+ this.load();
+ };
</script>
</mk-timeline-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/timemachine.tag b/src/web/app/desktop/tags/home-widgets/timemachine.tag
new file mode 100644
index 0000000000..3cddf53551
--- /dev/null
+++ b/src/web/app/desktop/tags/home-widgets/timemachine.tag
@@ -0,0 +1,23 @@
+<mk-timemachine-home-widget>
+ <mk-calendar-widget design={ data.design } warp={ warp }/>
+ <style>
+ :scope
+ display block
+ </style>
+ <script>
+ this.data = {
+ design: 0
+ };
+
+ this.mixin('widget');
+
+ this.warp = date => {
+ this.opts.tl.warp(date);
+ };
+
+ this.func = () => {
+ if (++this.data.design == 6) this.data.design = 0;
+ this.save();
+ };
+ </script>
+</mk-timemachine-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/tips.tag b/src/web/app/desktop/tags/home-widgets/tips.tag
index 5a535099ab..81cea64643 100644
--- a/src/web/app/desktop/tags/home-widgets/tips.tag
+++ b/src/web/app/desktop/tags/home-widgets/tips.tag
@@ -3,8 +3,6 @@
<style>
:scope
display block
- background transparent !important
- border none !important
overflow visible !important
> p
@@ -31,6 +29,8 @@
<script>
import anime from 'animejs';
+ this.mixin('widget');
+
this.tips = [
'<kbd>t</kbd>でタイムラインにフォーカスできます',
'<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開きます',
@@ -39,8 +39,24 @@
'ドライブにファイルをドラッグ&ドロップしてアップロードできます',
'ドライブでファイルをドラッグしてフォルダ移動できます',
'ドライブでフォルダをドラッグしてフォルダ移動できます',
- 'ホームをカスタマイズできます(準備中)',
- 'MisskeyはMIT Licenseです'
+ 'ホームは設定からカスタマイズできます',
+ 'MisskeyはMIT Licenseです',
+ 'タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れます',
+ '投稿の ... をクリックして、投稿をユーザーページにピン留めできます',
+ 'ドライブの容量は(デフォルトで)1GBです',
+ '投稿に添付したファイルは全てドライブに保存されます',
+ 'ホームのカスタマイズ中、ウィジェットを右クリックしてデザインを変更できます',
+ 'タイムライン上部にもウィジェットを設置できます',
+ '投稿をダブルクリックすると詳細が見れます',
+ '「**」でテキストを囲むと**強調表示**されます',
+ 'チャンネルウィジェットを利用すると、よく利用するチャンネルを素早く確認できます',
+ 'いくつかのウィンドウはブラウザの外に切り離すことができます',
+ 'カレンダーウィジェットのパーセンテージは、経過の割合を示しています',
+ 'APIを利用してbotの開発なども行えます',
+ 'MisskeyはLINEを通じてでも利用できます',
+ 'まゆかわいいよまゆ',
+ 'Misskeyは2014年にサービスを開始しました',
+ '対応ブラウザではMisskeyを開いていなくても通知を受け取れます'
]
this.on('mount', () => {
diff --git a/src/web/app/desktop/tags/home-widgets/trends.tag b/src/web/app/desktop/tags/home-widgets/trends.tag
index 021df3f728..8713c68746 100644
--- a/src/web/app/desktop/tags/home-widgets/trends.tag
+++ b/src/web/app/desktop/tags/home-widgets/trends.tag
@@ -1,6 +1,8 @@
<mk-trends-home-widget>
- <p class="title"><i class="fa fa-fire"></i>%i18n:desktop.tags.mk-trends-home-widget.title%</p>
- <button onclick={ fetch } title="%i18n:desktop.tags.mk-trends-home-widget.refresh%"><i class="fa fa-refresh"></i></button>
+ <virtual if={ !data.compact }>
+ <p class="title"><i class="fa fa-fire"></i>%i18n:desktop.tags.mk-trends-home-widget.title%</p>
+ <button onclick={ fetch } title="%i18n:desktop.tags.mk-trends-home-widget.refresh%"><i class="fa fa-refresh"></i></button>
+ </virtual>
<div class="post" if={ !loading && post != null }>
<p class="text"><a href="/{ post.user.username }/{ post.id }">{ post.text }</a></p>
<p class="author">―<a href="/{ post.user.username }">@{ post.user.username }</a></p>
@@ -11,6 +13,8 @@
:scope
display block
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
> .title
margin 0
@@ -72,7 +76,11 @@
</style>
<script>
- this.mixin('api');
+ this.data = {
+ compact: false
+ };
+
+ this.mixin('widget');
this.post = null;
this.loading = true;
@@ -108,5 +116,10 @@
});
});
};
+
+ this.func = () => {
+ this.data.compact = !this.data.compact;
+ this.save();
+ };
</script>
</mk-trends-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
index f78d7944f1..cf563db535 100644
--- a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
+++ b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
@@ -1,6 +1,8 @@
<mk-user-recommendation-home-widget>
- <p class="title"><i class="fa fa-users"></i>%i18n:desktop.tags.mk-user-recommendation-home-widget.title%</p>
- <button onclick={ refresh } title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%"><i class="fa fa-refresh"></i></button>
+ <virtual if={ !data.compact }>
+ <p class="title"><i class="fa fa-users"></i>%i18n:desktop.tags.mk-user-recommendation-home-widget.title%</p>
+ <button onclick={ refresh } title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%"><i class="fa fa-refresh"></i></button>
+ </virtual>
<div class="user" if={ !loading && users.length != 0 } each={ _user in users }>
<a class="avatar-anchor" href={ '/' + _user.username }>
<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/>
@@ -17,6 +19,8 @@
:scope
display block
background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
> .title
margin 0
@@ -111,7 +115,11 @@
</style>
<script>
- this.mixin('api');
+ this.data = {
+ compact: false
+ };
+
+ this.mixin('widget');
this.mixin('user-preview');
this.users = null;
@@ -148,5 +156,10 @@
}
this.fetch();
};
+
+ this.func = () => {
+ this.data.compact = !this.data.compact;
+ this.save();
+ };
</script>
</mk-user-recommendation-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/version.tag b/src/web/app/desktop/tags/home-widgets/version.tag
index ea5307061c..2b66b0490e 100644
--- a/src/web/app/desktop/tags/home-widgets/version.tag
+++ b/src/web/app/desktop/tags/home-widgets/version.tag
@@ -1,10 +1,8 @@
<mk-version-home-widget>
- <p>ver { version } (葵 aoi)</p>
+ <p>ver { _VERSION_ } (葵 aoi)</p>
<style>
:scope
display block
- background transparent !important
- border none !important
overflow visible !important
> p
@@ -17,6 +15,6 @@
</style>
<script>
- this.version = VERSION;
+ this.mixin('widget');
</script>
</mk-version-home-widget>
diff --git a/src/web/app/desktop/tags/home.tag b/src/web/app/desktop/tags/home.tag
index 37b2d3cf7e..55f36e0977 100644
--- a/src/web/app/desktop/tags/home.tag
+++ b/src/web/app/desktop/tags/home.tag
@@ -1,50 +1,173 @@
-<mk-home>
+<mk-home data-customize={ opts.customize }>
+ <div class="customize" if={ opts.customize }>
+ <a href="/"><i class="fa fa-check"></i>完了</a>
+ <div>
+ <div class="adder">
+ <p>ウィジェットを追加:</p>
+ <select ref="widgetSelector">
+ <option value="profile">プロフィール</option>
+ <option value="calendar">カレンダー</option>
+ <option value="timemachine">カレンダー(タイムマシン)</option>
+ <option value="activity">アクティビティ</option>
+ <option value="rss-reader">RSSリーダー</option>
+ <option value="trends">トレンド</option>
+ <option value="photo-stream">フォトストリーム</option>
+ <option value="slideshow">スライドショー</option>
+ <option value="version">バージョン</option>
+ <option value="broadcast">ブロードキャスト</option>
+ <option value="notifications">通知</option>
+ <option value="user-recommendation">おすすめユーザー</option>
+ <option value="recommended-polls">投票</option>
+ <option value="post-form">投稿フォーム</option>
+ <option value="messaging">メッセージ</option>
+ <option value="channel">チャンネル</option>
+ <option value="access-log">アクセスログ</option>
+ <option value="server">サーバー情報</option>
+ <option value="donation">寄付のお願い</option>
+ <option value="nav">ナビゲーション</option>
+ <option value="tips">ヒント</option>
+ </select>
+ <button onclick={ addWidget }>追加</button>
+ </div>
+ <div class="trash">
+ <div ref="trash"></div>
+ <p>ゴミ箱</p>
+ </div>
+ </div>
+ </div>
<div class="main">
- <div class="left" ref="left"></div>
- <main>
+ <div class="left">
+ <div ref="left" data-place="left"></div>
+ </div>
+ <main ref="main">
+ <div class="maintop" ref="maintop" data-place="main" if={ opts.customize }></div>
<mk-timeline-home-widget ref="tl" if={ mode == 'timeline' }/>
<mk-mentions-home-widget ref="tl" if={ mode == 'mentions' }/>
</main>
- <div class="right" ref="right"></div>
+ <div class="right">
+ <div ref="right" data-place="right"></div>
+ </div>
</div>
<style>
:scope
display block
+ &[data-customize]
+ padding-top 48px
+ background-image url('/assets/desktop/grid.svg')
+
+ > .main > main > *:not(.maintop)
+ cursor not-allowed
+
+ > *
+ pointer-events none
+
+ &:not([data-customize])
+ > .main > *:empty
+ display none
+
+ > .customize
+ position fixed
+ z-index 1000
+ top 0
+ left 0
+ width 100%
+ height 48px
+ background #f7f7f7
+ box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+
+ > a
+ display block
+ position absolute
+ z-index 1001
+ top 0
+ right 0
+ padding 0 16px
+ line-height 48px
+ text-decoration none
+ color $theme-color-foreground
+ background $theme-color
+ transition background 0.1s ease
+
+ &:hover
+ background lighten($theme-color, 10%)
+
+ &:active
+ background darken($theme-color, 10%)
+ transition background 0s ease
+
+ > i
+ margin-right 8px
+
+ > div
+ display flex
+ margin 0 auto
+ max-width 1200px - 32px
+
+ > div
+ width 50%
+
+ &.adder
+ > p
+ display inline
+ line-height 48px
+
+ &.trash
+ border-left solid 1px #ddd
+
+ > div
+ width 100%
+ height 100%
+
+ > p
+ position absolute
+ top 0
+ left 0
+ width 100%
+ line-height 48px
+ margin 0
+ text-align center
+ pointer-events none
+
> .main
+ display flex
+ justify-content center
margin 0 auto
max-width 1200px
- &:after
- content ""
- display block
- clear both
-
> *
- float left
+ .customize-container
+ cursor move
- > *
- display block
- //border solid 1px #eaeaea
- border solid 1px rgba(0, 0, 0, 0.075)
- border-radius 6px
- //box-shadow 0px 2px 16px rgba(0, 0, 0, 0.2)
-
- &:not(:last-child)
- margin-bottom 16px
+ > *
+ pointer-events none
> main
padding 16px
width calc(100% - 275px * 2)
+ > *:not(.maintop):not(:last-child)
+ > .maintop > *:not(:last-child)
+ margin-bottom 16px
+
+ > .maintop
+ min-height 64px
+ margin-bottom 16px
+
> *:not(main)
width 275px
+ > *
+ padding 16px 0 16px 0
+
+ > *:not(:last-child)
+ margin-bottom 16px
+
> .left
- padding 16px 0 16px 16px
+ padding-left 16px
> .right
- padding 16px 16px 16px 0
+ padding-right 16px
@media (max-width 1100px)
> *:not(main)
@@ -58,72 +181,208 @@
</style>
<script>
+ import uuid from 'uuid';
+ import Sortable from 'sortablejs';
+ import dialog from '../scripts/dialog';
+ import ScrollFollower from '../scripts/scroll-follower';
+
this.mixin('i');
+ this.mixin('api');
this.mode = this.opts.mode || 'timeline';
- const _home = {
- left: [
- 'profile',
- 'calendar',
- 'activity',
- 'rss-reader',
- 'trends',
- 'photo-stream',
- 'version'
- ],
- right: [
- 'broadcast',
- 'notifications',
- 'user-recommendation',
- 'recommended-polls',
- 'server',
- 'donation',
- 'nav',
- 'tips'
- ]
- };
-
this.home = [];
+ this.bakeHomeData = () => JSON.stringify(this.I.client_settings.home);
+ this.bakedHomeData = this.bakeHomeData();
+
this.on('mount', () => {
this.refs.tl.on('loaded', () => {
this.trigger('loaded');
});
-/*
- this.I.data.home.forEach(widget => {
+
+ this.I.on('refreshed', this.onMeRefreshed);
+
+ this.I.client_settings.home.forEach(widget => {
try {
- const el = document.createElement(`mk-${widget.name}-home-widget`);
- switch (widget.place) {
- case 'left': this.refs.left.appendChild(el); break;
- case 'right': this.refs.right.appendChild(el); break;
- }
- this.home.push(riot.mount(el, {
- id: widget.id,
- data: widget.data
- })[0]);
+ this.setWidget(widget);
} catch (e) {
// noop
}
});
-*/
- _home.left.forEach(widget => {
- const el = document.createElement(`mk-${widget}-home-widget`);
- this.refs.left.appendChild(el);
- this.home.push(riot.mount(el)[0]);
- });
- _home.right.forEach(widget => {
- const el = document.createElement(`mk-${widget}-home-widget`);
- this.refs.right.appendChild(el);
- this.home.push(riot.mount(el)[0]);
- });
+ if (!this.opts.customize) {
+ if (this.refs.left.children.length == 0) {
+ this.refs.left.parentNode.removeChild(this.refs.left);
+ }
+ if (this.refs.right.children.length == 0) {
+ this.refs.right.parentNode.removeChild(this.refs.right);
+ }
+ }
+
+ if (this.opts.customize) {
+ dialog('<i class="fa fa-info-circle"></i>カスタマイズのヒント',
+ '<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
+ '<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
+ '<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
+ '<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
+ [{
+ text: 'Got it!'
+ }]);
+
+ const sortableOption = {
+ group: 'kyoppie',
+ animation: 150,
+ onMove: evt => {
+ const id = evt.dragged.getAttribute('data-widget-id');
+ this.home.find(tag => tag.id == id).update({ place: evt.to.getAttribute('data-place') });
+ },
+ onSort: () => {
+ this.saveHome();
+ }
+ };
+
+ new Sortable(this.refs.left, sortableOption);
+ new Sortable(this.refs.right, sortableOption);
+ new Sortable(this.refs.maintop, sortableOption);
+ new Sortable(this.refs.trash, Object.assign({}, sortableOption, {
+ onAdd: evt => {
+ const el = evt.item;
+ const id = el.getAttribute('data-widget-id');
+ el.parentNode.removeChild(el);
+ this.I.client_settings.home = this.I.client_settings.home.filter(w => w.id != id);
+ this.saveHome();
+ }
+ }));
+ }
+
+ if (!this.opts.customize) {
+ this.scrollFollowerLeft = this.refs.left.parentNode ? new ScrollFollower(this.refs.left, this.root.getBoundingClientRect().top) : null;
+ this.scrollFollowerRight = this.refs.right.parentNode ? new ScrollFollower(this.refs.right, this.root.getBoundingClientRect().top) : null;
+ }
});
this.on('unmount', () => {
+ this.I.off('refreshed', this.onMeRefreshed);
+
this.home.forEach(widget => {
widget.unmount();
});
+
+ if (!this.opts.customize) {
+ if (this.scrollFollowerLeft) this.scrollFollowerLeft.dispose();
+ if (this.scrollFollowerRight) this.scrollFollowerRight.dispose();
+ }
});
+
+ this.onMeRefreshed = () => {
+ if (this.bakedHomeData != this.bakeHomeData()) {
+ alert('別の場所でホームが編集されました。ページを再度読み込みすると編集が反映されます。');
+ }
+ };
+
+ this.setWidget = (widget, prepend = false) => {
+ const el = document.createElement(`mk-${widget.name}-home-widget`);
+
+ let actualEl;
+
+ if (this.opts.customize) {
+ const container = document.createElement('div');
+ container.classList.add('customize-container');
+ container.setAttribute('data-widget-id', widget.id);
+ container.appendChild(el);
+ actualEl = container;
+ } else {
+ actualEl = el;
+ }
+
+ switch (widget.place) {
+ case 'left':
+ if (prepend) {
+ this.refs.left.insertBefore(actualEl, this.refs.left.firstChild);
+ } else {
+ this.refs.left.appendChild(actualEl);
+ }
+ break;
+ case 'right':
+ if (prepend) {
+ this.refs.right.insertBefore(actualEl, this.refs.right.firstChild);
+ } else {
+ this.refs.right.appendChild(actualEl);
+ }
+ break;
+ case 'main':
+ if (this.opts.customize) {
+ this.refs.maintop.appendChild(actualEl);
+ } else {
+ this.refs.main.insertBefore(actualEl, this.refs.tl.root);
+ }
+ break;
+ }
+
+ const tag = riot.mount(el, {
+ id: widget.id,
+ data: widget.data,
+ place: widget.place,
+ tl: this.refs.tl
+ })[0];
+
+ this.home.push(tag);
+
+ if (this.opts.customize) {
+ actualEl.oncontextmenu = e => {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ if (tag.func) tag.func();
+ return false;
+ };
+ }
+ };
+
+ this.addWidget = () => {
+ const widget = {
+ name: this.refs.widgetSelector.options[this.refs.widgetSelector.selectedIndex].value,
+ id: uuid(),
+ place: 'left',
+ data: {}
+ };
+
+ this.I.client_settings.home.unshift(widget);
+
+ this.setWidget(widget, true);
+
+ this.saveHome();
+ };
+
+ this.saveHome = () => {
+ const data = [];
+
+ Array.from(this.refs.left.children).forEach(el => {
+ const id = el.getAttribute('data-widget-id');
+ const widget = this.I.client_settings.home.find(w => w.id == id);
+ widget.place = 'left';
+ data.push(widget);
+ });
+
+ Array.from(this.refs.right.children).forEach(el => {
+ const id = el.getAttribute('data-widget-id');
+ const widget = this.I.client_settings.home.find(w => w.id == id);
+ widget.place = 'right';
+ data.push(widget);
+ });
+
+ Array.from(this.refs.maintop.children).forEach(el => {
+ const id = el.getAttribute('data-widget-id');
+ const widget = this.I.client_settings.home.find(w => w.id == id);
+ widget.place = 'main';
+ data.push(widget);
+ });
+
+ this.api('i/update_home', {
+ home: data
+ }).then(() => {
+ this.I.update();
+ });
+ };
</script>
</mk-home>
diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.ts
index 37fdfe37e4..3ec1d108aa 100644
--- a/src/web/app/desktop/tags/index.js
+++ b/src/web/app/desktop/tags/index.ts
@@ -12,6 +12,7 @@ require('./drive/nav-folder.tag');
require('./drive/browser-window.tag');
require('./drive/browser.tag');
require('./select-file-from-drive-window.tag');
+require('./select-folder-from-drive-window.tag');
require('./crop-window.tag');
require('./settings.tag');
require('./settings-window.tag');
@@ -38,6 +39,12 @@ require('./home-widgets/recommended-polls.tag');
require('./home-widgets/trends.tag');
require('./home-widgets/activity.tag');
require('./home-widgets/server.tag');
+require('./home-widgets/slideshow.tag');
+require('./home-widgets/channel.tag');
+require('./home-widgets/timemachine.tag');
+require('./home-widgets/post-form.tag');
+require('./home-widgets/access-log.tag');
+require('./home-widgets/messaging.tag');
require('./timeline.tag');
require('./messaging/window.tag');
require('./messaging/room-window.tag');
@@ -45,23 +52,19 @@ require('./following-setuper.tag');
require('./ellipsis-icon.tag');
require('./ui.tag');
require('./home.tag');
-require('./user-header.tag');
-require('./user-profile.tag');
require('./user-timeline.tag');
require('./user.tag');
-require('./user-home.tag');
-require('./user-graphs.tag');
-require('./user-photos.tag');
require('./big-follow-button.tag');
require('./pages/entrance.tag');
-require('./pages/entrance/signin.tag');
-require('./pages/entrance/signup.tag');
require('./pages/home.tag');
+require('./pages/home-customize.tag');
require('./pages/user.tag');
require('./pages/post.tag');
require('./pages/search.tag');
require('./pages/not-found.tag');
require('./pages/selectdrive.tag');
+require('./pages/drive.tag');
+require('./pages/messaging-room.tag');
require('./autocomplete-suggestion.tag');
require('./progress-dialog.tag');
require('./user-preview.tag');
@@ -83,3 +86,5 @@ require('./user-following-window.tag');
require('./user-followers-window.tag');
require('./list-user.tag');
require('./detailed-post-window.tag');
+require('./widgets/calendar.tag');
+require('./widgets/activity.tag');
diff --git a/src/web/app/desktop/tags/messaging/room-window.tag b/src/web/app/desktop/tags/messaging/room-window.tag
index 5d8a4303a5..1c6ff7c4bc 100644
--- a/src/web/app/desktop/tags/messaging/room-window.tag
+++ b/src/web/app/desktop/tags/messaging/room-window.tag
@@ -1,5 +1,5 @@
<mk-messaging-room-window>
- <mk-window ref="window" is-modal={ false } width={ '500px' } height={ '560px' }>
+ <mk-window ref="window" is-modal={ false } width={ '500px' } height={ '560px' } popout={ popout }>
<yield to="header"><i class="fa fa-comments"></i>メッセージ: { parent.user.name }</yield>
<yield to="content">
<mk-messaging-room user={ parent.user }/>
@@ -21,6 +21,8 @@
<script>
this.user = this.opts.user;
+ this.popout = `${_URL_}/i/messaging/${this.user.username}`;
+
this.on('mount', () => {
this.refs.window.on('closed', () => {
this.unmount();
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index a4f66105a8..d7855363ea 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -212,9 +212,12 @@
this.mixin('i');
this.mixin('api');
- this.mixin('stream');
this.mixin('user-preview');
+ this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
+
this.notifications = [];
this.loading = true;
@@ -235,11 +238,12 @@
});
});
- this.stream.on('notification', this.onNotification);
+ this.connection.on('notification', this.onNotification);
});
this.on('unmount', () => {
- this.stream.off('notification', this.onNotification);
+ this.connection.off('notification', this.onNotification);
+ this.stream.dispose(this.connectionId);
});
this.on('update', () => {
@@ -253,7 +257,7 @@
this.onNotification = notification => {
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
- this.stream.send({
+ this.connection.send({
type: 'read_notification',
id: notification.id
});
diff --git a/src/web/app/desktop/tags/pages/drive.tag b/src/web/app/desktop/tags/pages/drive.tag
new file mode 100644
index 0000000000..9f3e75ab21
--- /dev/null
+++ b/src/web/app/desktop/tags/pages/drive.tag
@@ -0,0 +1,37 @@
+<mk-drive-page>
+ <mk-drive-browser ref="browser" folder={ opts.folder }/>
+ <style>
+ :scope
+ display block
+ position fixed
+ width 100%
+ height 100%
+ background #fff
+
+ > mk-drive-browser
+ height 100%
+ </style>
+ <script>
+ this.on('mount', () => {
+ document.title = 'Misskey Drive';
+
+ this.refs.browser.on('move-root', () => {
+ const title = 'Misskey Drive';
+
+ // Rewrite URL
+ history.pushState(null, title, '/i/drive');
+
+ document.title = title;
+ });
+
+ this.refs.browser.on('open-folder', folder => {
+ const title = folder.name + ' | Misskey Drive';
+
+ // Rewrite URL
+ history.pushState(null, title, '/i/drive/folder/' + folder.id);
+
+ document.title = title;
+ });
+ });
+ </script>
+</mk-drive-page>
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag
index 7ad19c073e..02aeb922fe 100644
--- a/src/web/app/desktop/tags/pages/entrance.tag
+++ b/src/web/app/desktop/tags/pages/entrance.tag
@@ -1,16 +1,25 @@
<mk-entrance>
<main>
- <img src="/assets/title.svg" alt="Misskey"/>
- <mk-entrance-signin if={ mode == 'signin' }/>
- <mk-entrance-signup if={ mode == 'signup' }/>
- <div class="introduction" if={ mode == 'introduction' }>
- <mk-introduction/>
- <button onclick={ signin }>わかった</button>
+ <div>
+ <h1>どこにいても、ここにあります</h1>
+ <p>ようこそ! MisskeyはTwitter風ミニブログSNSです――思ったこと、共有したいことをシンプルに書き残せます。タイムラインを見れば、皆の反応や皆がどう思っているのかもすぐにわかります。</p>
+ <p if={ stats }>これまでに{ stats.posts_count }投稿されました</p>
+ </div>
+ <div>
+ <mk-entrance-signin if={ mode == 'signin' }/>
+ <mk-entrance-signup if={ mode == 'signup' }/>
+ <div class="introduction" if={ mode == 'introduction' }>
+ <mk-introduction/>
+ <button onclick={ signin }>わかった</button>
+ </div>
</div>
</main>
<mk-forkit/>
<footer>
- <mk-copyright/>
+ <div>
+ <mk-nav-links/>
+ <mk-copyright/>
+ </div>
</footer>
<!-- ↓ https://github.com/riot/riot/issues/2134 (将来的)-->
<style data-disable-scope="data-disable-scope">
@@ -21,66 +30,105 @@
</style>
<style>
:scope
+ $width = 1000px
+
display block
- height 100%
+
+ &:before
+ content ""
+ display block
+ position fixed
+ width 100%
+ height 100%
+ background rgba(0, 0, 0, 0.3)
> main
display block
+ max-width $width
+ margin 0 auto
+ padding 64px 0 0 0
padding-bottom 16px
- > img
+ &:after
+ content ""
display block
- width 160px
- height 170px
- margin 0 auto
- pointer-events none
- user-select none
+ clear both
- > .introduction
- max-width 360px
- margin 0 auto
- color #777
+ > div:first-child
+ position absolute
+ top 64px
+ left 0
+ width calc(100% - 500px)
+ color #fff
+ text-shadow 0 0 32px rgba(0, 0, 0, 0.5)
+ font-weight bold
- > mk-introduction
- padding 32px
- background #fff
- box-shadow 0 4px 16px rgba(0, 0, 0, 0.2)
+ > p:last-child
+ padding 1em 0 0 0
+ border-top solid 1px #fff
- > button
- display block
- margin 16px auto 0 auto
- color #666
+ > div:last-child
+ float right
- &:hover
- text-decoration underline
+ > .introduction
+ max-width 360px
+ margin 0 auto
+ color #777
- > .tl
- padding 32px 0
- background #fff
+ > mk-introduction
+ padding 32px
+ background #fff
+ box-shadow 0 4px 16px rgba(0, 0, 0, 0.2)
- > h2
- display block
- margin 0
- padding 0
- text-align center
- font-size 20px
- color #5b6b73
+ > button
+ display block
+ margin 16px auto 0 auto
+ color #666
+
+ &:hover
+ text-decoration underline
- > mk-public-timeline
- max-width 500px
- margin 0 auto
> footer
- > mk-copyright
- margin 0
+ *
+ color #fff !important
+ text-shadow 0 0 8px #000
+ font-weight bold
+
+ > div
+ max-width $width
+ margin 0 auto
+ padding 16px 0
text-align center
- line-height 64px
- font-size 10px
- color rgba(#000, 0.5)
+ border-top solid 1px #fff
+
+ > mk-copyright
+ margin 0
+ line-height 64px
+ font-size 10px
</style>
<script>
+ this.mixin('api');
+
this.mode = 'signin';
+ this.on('mount', () => {
+ document.documentElement.style.backgroundColor = '#444';
+
+ this.api('meta').then(meta => {
+ const img = meta.top_image ? meta.top_image : '/assets/desktop/index.jpg';
+ document.documentElement.style.backgroundImage = `url("${ img }")`;
+ document.documentElement.style.backgroundSize = 'cover';
+ document.documentElement.style.backgroundPosition = 'center';
+ });
+
+ this.api('stats').then(stats => {
+ this.update({
+ stats
+ });
+ });
+ });
+
this.signup = () => {
this.update({
mode: 'signup'
@@ -100,3 +148,195 @@
};
</script>
</mk-entrance>
+
+<mk-entrance-signin>
+ <a class="help" href={ _ABOUT_URL_ + '/help' } title="お困りですか?"><i class="fa fa-question"></i></a>
+ <div class="form">
+ <h1><img if={ user } src={ user.avatar_url + '?thumbnail&size=32' }/>
+ <p>{ user ? user.name : 'アカウント' }</p>
+ </h1>
+ <mk-signin ref="signin"/>
+ </div>
+ <a href={ _API_URL_ + '/signin/twitter' }>Twitterでサインイン</a>
+ <div class="divider"><span>or</span></div>
+ <button class="signup" onclick={ parent.signup }>新規登録</button><a class="introduction" onclick={ introduction }>Misskeyについて</a>
+ <style>
+ :scope
+ display block
+ width 290px
+ margin 0 auto
+ text-align center
+
+ &:hover
+ > .help
+ opacity 1
+
+ > .help
+ cursor pointer
+ display block
+ position absolute
+ top 0
+ right 0
+ z-index 1
+ margin 0
+ padding 0
+ font-size 1.2em
+ color #999
+ border none
+ outline none
+ background transparent
+ opacity 0
+ transition opacity 0.1s ease
+
+ &:hover
+ color #444
+
+ &:active
+ color #222
+
+ > i
+ padding 14px
+
+ > .form
+ padding 10px 28px 16px 28px
+ background #fff
+ box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
+
+ > h1
+ display block
+ margin 0
+ padding 0
+ height 54px
+ line-height 54px
+ text-align center
+ text-transform uppercase
+ font-size 1em
+ font-weight bold
+ color rgba(0, 0, 0, 0.5)
+ border-bottom solid 1px rgba(0, 0, 0, 0.1)
+
+ > p
+ display inline
+ margin 0
+ padding 0
+
+ > img
+ display inline-block
+ top 10px
+ width 32px
+ height 32px
+ margin-right 8px
+ border-radius 100%
+
+ &[src='']
+ display none
+
+ > .divider
+ padding 16px 0
+ text-align center
+
+ &:before
+ &:after
+ content ""
+ display block
+ position absolute
+ top 50%
+ width 45%
+ height 1px
+ border-top solid 1px rgba(0, 0, 0, 0.1)
+
+ &:before
+ left 0
+
+ &:after
+ right 0
+
+ > *
+ z-index 1
+ padding 0 8px
+ color #fff
+ text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
+
+ > .signup
+ width 100%
+ line-height 56px
+ font-size 1em
+ color #fff
+ background $theme-color
+ border-radius 64px
+
+ &:hover
+ background lighten($theme-color, 5%)
+
+ &:active
+ background darken($theme-color, 5%)
+
+ > .introduction
+ display inline-block
+ margin-top 16px
+ font-size 12px
+ color #666
+
+ </style>
+ <script>
+ this.on('mount', () => {
+ this.refs.signin.on('user', user => {
+ this.update({
+ user: user
+ });
+ });
+ });
+
+ this.introduction = () => {
+ this.parent.introduction();
+ };
+ </script>
+</mk-entrance-signin>
+
+<mk-entrance-signup>
+ <mk-signup/>
+ <button class="cancel" type="button" onclick={ parent.signin } title="キャンセル"><i class="fa fa-times"></i></button>
+ <style>
+ :scope
+ display block
+ width 368px
+ margin 0 auto
+
+ &:hover
+ > .cancel
+ opacity 1
+
+ > mk-signup
+ padding 18px 32px 0 32px
+ background #fff
+ box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
+
+ > .cancel
+ cursor pointer
+ display block
+ position absolute
+ top 0
+ right 0
+ z-index 1
+ margin 0
+ padding 0
+ font-size 1.2em
+ color #999
+ border none
+ outline none
+ box-shadow none
+ background transparent
+ opacity 0
+ transition opacity 0.1s ease
+
+ &:hover
+ color #555
+
+ &:active
+ color #222
+
+ > i
+ padding 14px
+
+ </style>
+</mk-entrance-signup>
diff --git a/src/web/app/desktop/tags/pages/entrance/signin.tag b/src/web/app/desktop/tags/pages/entrance/signin.tag
deleted file mode 100644
index 6caa747c1c..0000000000
--- a/src/web/app/desktop/tags/pages/entrance/signin.tag
+++ /dev/null
@@ -1,134 +0,0 @@
-<mk-entrance-signin><a class="help" href={ CONFIG.aboutUrl + '/help' } title="お困りですか?"><i class="fa fa-question"></i></a>
- <div class="form">
- <h1><img if={ user } src={ user.avatar_url + '?thumbnail&size=32' }/>
- <p>{ user ? user.name : 'アカウント' }</p>
- </h1>
- <mk-signin ref="signin"/>
- </div>
- <div class="divider"><span>or</span></div>
- <button class="signup" onclick={ parent.signup }>新規登録</button><a class="introduction" onclick={ introduction }>Misskeyについて</a>
- <style>
- :scope
- display block
- width 290px
- margin 0 auto
- text-align center
-
- &:hover
- > .help
- opacity 1
-
- > .help
- cursor pointer
- display block
- position absolute
- top 0
- right 0
- z-index 1
- margin 0
- padding 0
- font-size 1.2em
- color #999
- border none
- outline none
- background transparent
- opacity 0
- transition opacity 0.1s ease
-
- &:hover
- color #444
-
- &:active
- color #222
-
- > i
- padding 14px
-
- > .form
- padding 10px 28px 16px 28px
- background #fff
- box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
-
- > h1
- display block
- margin 0
- padding 0
- height 54px
- line-height 54px
- text-align center
- text-transform uppercase
- font-size 1em
- font-weight bold
- color rgba(0, 0, 0, 0.5)
- border-bottom solid 1px rgba(0, 0, 0, 0.1)
-
- > p
- display inline
- margin 0
- padding 0
-
- > img
- display inline-block
- top 10px
- width 32px
- height 32px
- margin-right 8px
- border-radius 100%
-
- &[src='']
- display none
-
- > .divider
- padding 16px 0
- text-align center
-
- &:after
- content ""
- display block
- position absolute
- top 50%
- width 100%
- height 1px
- border-top solid 1px rgba(0, 0, 0, 0.1)
-
- > *
- z-index 1
- padding 0 8px
- color rgba(0, 0, 0, 0.5)
- background #fdfdfd
-
- > .signup
- width 100%
- line-height 56px
- font-size 1em
- color #fff
- background $theme-color
- border-radius 64px
-
- &:hover
- background lighten($theme-color, 5%)
-
- &:active
- background darken($theme-color, 5%)
-
- > .introduction
- display inline-block
- margin-top 16px
- font-size 12px
- color #666
-
- </style>
- <script>
- this.on('mount', () => {
- this.refs.signin.on('user', user => {
- this.update({
- user: user
- });
- });
- });
-
- this.introduction = () => {
- this.parent.introduction();
- };
- </script>
-</mk-entrance-signin>
diff --git a/src/web/app/desktop/tags/pages/entrance/signup.tag b/src/web/app/desktop/tags/pages/entrance/signup.tag
deleted file mode 100644
index 0722d82a65..0000000000
--- a/src/web/app/desktop/tags/pages/entrance/signup.tag
+++ /dev/null
@@ -1,47 +0,0 @@
-<mk-entrance-signup>
- <mk-signup/>
- <button class="cancel" type="button" onclick={ parent.signin } title="キャンセル"><i class="fa fa-times"></i></button>
- <style>
- :scope
- display block
- width 368px
- margin 0 auto
-
- &:hover
- > .cancel
- opacity 1
-
- > mk-signup
- padding 18px 32px 0 32px
- background #fff
- box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
-
- > .cancel
- cursor pointer
- display block
- position absolute
- top 0
- right 0
- z-index 1
- margin 0
- padding 0
- font-size 1.2em
- color #999
- border none
- outline none
- box-shadow none
- background transparent
- opacity 0
- transition opacity 0.1s ease
-
- &:hover
- color #555
-
- &:active
- color #222
-
- > i
- padding 14px
-
- </style>
-</mk-entrance-signup>
diff --git a/src/web/app/desktop/tags/pages/home-customize.tag b/src/web/app/desktop/tags/pages/home-customize.tag
new file mode 100644
index 0000000000..457b8390e7
--- /dev/null
+++ b/src/web/app/desktop/tags/pages/home-customize.tag
@@ -0,0 +1,12 @@
+<mk-home-customize-page>
+ <mk-home ref="home" mode="timeline" customize={ true }/>
+ <style>
+ :scope
+ display block
+ </style>
+ <script>
+ this.on('mount', () => {
+ document.title = 'Misskey - ホームのカスタマイズ';
+ });
+ </script>
+</mk-home-customize-page>
diff --git a/src/web/app/desktop/tags/pages/home.tag b/src/web/app/desktop/tags/pages/home.tag
index e8ba4023de..3c8f4ec570 100644
--- a/src/web/app/desktop/tags/pages/home.tag
+++ b/src/web/app/desktop/tags/pages/home.tag
@@ -12,10 +12,12 @@
this.mixin('i');
this.mixin('api');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.unreadCount = 0;
-
this.page = this.opts.mode || 'timeline';
this.on('mount', () => {
@@ -24,12 +26,14 @@
});
document.title = 'Misskey';
Progress.start();
- this.stream.on('post', this.onStreamPost);
+
+ this.connection.on('post', this.onStreamPost);
document.addEventListener('visibilitychange', this.windowOnVisibilitychange, false);
});
this.on('unmount', () => {
- this.stream.off('post', this.onStreamPost);
+ this.connection.off('post', this.onStreamPost);
+ this.stream.dispose(this.connectionId);
document.removeEventListener('visibilitychange', this.windowOnVisibilitychange);
});
diff --git a/src/web/app/desktop/tags/pages/messaging-room.tag b/src/web/app/desktop/tags/pages/messaging-room.tag
new file mode 100644
index 0000000000..3c21b97501
--- /dev/null
+++ b/src/web/app/desktop/tags/pages/messaging-room.tag
@@ -0,0 +1,37 @@
+<mk-messaging-room-page>
+ <mk-messaging-room if={ user } user={ user } is-naked={ true }/>
+
+ <style>
+ :scope
+ display block
+ background #fff
+
+ </style>
+ <script>
+ import Progress from '../../../common/scripts/loading';
+
+ this.mixin('api');
+
+ this.fetching = true;
+ this.user = null;
+
+ this.on('mount', () => {
+ Progress.start();
+
+ document.documentElement.style.background = '#fff';
+
+ this.api('users/show', {
+ username: this.opts.user
+ }).then(user => {
+ this.update({
+ fetching: false,
+ user: user
+ });
+
+ document.title = 'メッセージ: ' + this.user.name;
+
+ Progress.done();
+ });
+ });
+ </script>
+</mk-messaging-room-page>
diff --git a/src/web/app/desktop/tags/pages/post.tag b/src/web/app/desktop/tags/pages/post.tag
index f270b43ac2..4a9672c1ef 100644
--- a/src/web/app/desktop/tags/pages/post.tag
+++ b/src/web/app/desktop/tags/pages/post.tag
@@ -28,6 +28,7 @@
> mk-post-detail
margin 0 auto
+ width 640px
</style>
<script>
diff --git a/src/web/app/desktop/tags/pages/selectdrive.tag b/src/web/app/desktop/tags/pages/selectdrive.tag
index 63fc588fac..9c3ac16eb1 100644
--- a/src/web/app/desktop/tags/pages/selectdrive.tag
+++ b/src/web/app/desktop/tags/pages/selectdrive.tag
@@ -1,15 +1,16 @@
<mk-selectdrive-page>
<mk-drive-browser ref="browser" multiple={ multiple }/>
<div>
- <button class="upload" title="PCからドライブにファイルをアップロード" onclick={ upload }><i class="fa fa-upload"></i></button>
- <button class="cancel" onclick={ close }>キャンセル</button>
- <button class="ok" onclick={ ok }>決定</button>
+ <button class="upload" title="%i18n:desktop.tags.mk-selectdrive-page.upload%" onclick={ upload }><i class="fa fa-upload"></i></button>
+ <button class="cancel" onclick={ close }>%i18n:desktop.tags.mk-selectdrive-page.cancel%</button>
+ <button class="ok" onclick={ ok }>%i18n:desktop.tags.mk-selectdrive-page.ok%</button>
</div>
<style>
:scope
display block
position fixed
+ width 100%
height 100%
background #fff
@@ -130,7 +131,7 @@
this.multiple = q.get('multiple') == 'true' ? true : false;
this.on('mount', () => {
- document.documentElement.style.background = '#fff';
+ document.title = '%i18n:desktop.tags.mk-selectdrive-page.title%';
this.refs.browser.on('selected', file => {
this.files = [file];
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index 8a0ada5f2a..e22386df91 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -129,7 +129,7 @@
this.refs.text.innerHTML = compile(tokens);
- this.refs.text.children.forEach(e => {
+ Array.from(this.refs.text.children).forEach(e => {
if (e.tagName == 'MK-URL') riot.mount(e);
});
}
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index ce7f81e32c..585b1b8280 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -57,7 +57,7 @@
</button>
</footer>
</article>
- <div class="replies">
+ <div class="replies" if={ !compact }>
<virtual each={ post in replies }>
<mk-post-detail-sub post={ post }/>
</virtual>
@@ -68,7 +68,6 @@
display block
margin 0
padding 0
- width 640px
overflow hidden
text-align left
background #fff
@@ -259,6 +258,7 @@
this.mixin('api');
this.mixin('user-preview');
+ this.compact = this.opts.compact;
this.contextFetching = false;
this.context = null;
this.post = this.opts.post;
@@ -273,7 +273,7 @@
this.refs.text.innerHTML = compile(tokens);
- this.refs.text.children.forEach(e => {
+ Array.from(this.refs.text.children).forEach(e => {
if (e.tagName == 'MK-URL') riot.mount(e);
});
@@ -288,14 +288,16 @@
}
// Get replies
- this.api('posts/replies', {
- post_id: this.p.id,
- limit: 8
- }).then(replies => {
- this.update({
- replies: replies
+ if (!this.compact) {
+ this.api('posts/replies', {
+ post_id: this.p.id,
+ limit: 8
+ }).then(replies => {
+ this.update({
+ replies: replies
+ });
});
- });
+ }
});
this.reply = () => {
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index 5041078bee..f6d9ee3779 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -405,7 +405,22 @@
// ファイルだったら
if (e.dataTransfer.files.length > 0) {
- e.dataTransfer.files.forEach(this.upload);
+ Array.from(e.dataTransfer.files).forEach(this.upload);
+ return;
+ }
+
+ // データ取得
+ const data = e.dataTransfer.getData('text');
+ if (data == null) return false;
+
+ // パース
+ // TODO: Validate JSON
+ const obj = JSON.parse(data);
+
+ // (ドライブの)ファイルだったら
+ if (obj.type == 'file') {
+ this.files.push(obj.file);
+ this.update();
}
};
@@ -414,7 +429,7 @@
};
this.onpaste = e => {
- e.clipboardData.items.forEach(item => {
+ Array.from(e.clipboardData.items).forEach(item => {
if (item.kind == 'file') {
this.upload(item.getAsFile());
}
@@ -435,7 +450,7 @@
};
this.changeFile = () => {
- this.refs.file.files.forEach(this.upload);
+ Array.from(this.refs.file.files).forEach(this.upload);
};
this.upload = file => {
diff --git a/src/web/app/desktop/tags/select-folder-from-drive-window.tag b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
new file mode 100644
index 0000000000..375f428bfc
--- /dev/null
+++ b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
@@ -0,0 +1,112 @@
+<mk-select-folder-from-drive-window>
+ <mk-window ref="window" is-modal={ true } width={ '800px' } height={ '500px' }>
+ <yield to="header">
+ <mk-raw content={ parent.title }/>
+ </yield>
+ <yield to="content">
+ <mk-drive-browser ref="browser"/>
+ <div>
+ <button class="cancel" onclick={ parent.close }>キャンセル</button>
+ <button class="ok" onclick={ parent.ok }>決定</button>
+ </div>
+ </yield>
+ </mk-window>
+ <style>
+ :scope
+ > mk-window
+ [data-yield='header']
+ > mk-raw
+ > i
+ margin-right 4px
+
+ [data-yield='content']
+ > mk-drive-browser
+ height calc(100% - 72px)
+
+ > div
+ height 72px
+ background lighten($theme-color, 95%)
+
+ .ok
+ .cancel
+ display block
+ position absolute
+ bottom 16px
+ cursor pointer
+ padding 0
+ margin 0
+ width 120px
+ height 40px
+ font-size 1em
+ outline none
+ border-radius 4px
+
+ &:focus
+ &:after
+ content ""
+ pointer-events none
+ position absolute
+ top -5px
+ right -5px
+ bottom -5px
+ left -5px
+ border 2px solid rgba($theme-color, 0.3)
+ border-radius 8px
+
+ &:disabled
+ opacity 0.7
+ cursor default
+
+ .ok
+ right 16px
+ color $theme-color-foreground
+ background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+ border solid 1px lighten($theme-color, 15%)
+
+ &:not(:disabled)
+ font-weight bold
+
+ &:hover:not(:disabled)
+ background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+ border-color $theme-color
+
+ &:active:not(:disabled)
+ background $theme-color
+ border-color $theme-color
+
+ .cancel
+ right 148px
+ color #888
+ background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+ border solid 1px #e2e2e2
+
+ &:hover
+ background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+ border-color #dcdcdc
+
+ &:active
+ background #ececec
+ border-color #dcdcdc
+
+ </style>
+ <script>
+ this.files = [];
+
+ this.title = this.opts.title || '<i class="fa fa-folder-o"></i>フォルダを選択';
+
+ this.on('mount', () => {
+ this.refs.window.on('closed', () => {
+ this.unmount();
+ });
+ });
+
+ this.close = () => {
+ this.refs.window.close();
+ };
+
+ this.ok = () => {
+ this.trigger('selected', this.refs.window.refs.browser.folder);
+ this.refs.window.close();
+ };
+ </script>
+</mk-select-folder-from-drive-window>
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index eabddfb432..4c16f9eaa8 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -38,6 +38,7 @@
<section class="web" show={ page == 'web' }>
<h1>デザイン</h1>
+ <a href="/i/customize-home">ホームをカスタマイズ</a>
</section>
<section class="web" show={ page == 'web' }>
diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag
index c75ae2911c..86269fdbe9 100644
--- a/src/web/app/desktop/tags/sub-post-content.tag
+++ b/src/web/app/desktop/tags/sub-post-content.tag
@@ -45,7 +45,7 @@
const tokens = this.post.ast;
this.refs.text.innerHTML = compile(tokens, false);
- this.refs.text.children.forEach(e => {
+ Array.from(this.refs.text.children).forEach(e => {
if (e.tagName == 'MK-URL') riot.mount(e);
});
}
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 44f3d5d8ec..13651dfa5f 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -112,7 +112,7 @@
</header>
<div class="body">
<div class="text" ref="text">
- <p class="channel" if={ p.channel != null }><a href={ CONFIG.chUrl + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
+ <p class="channel" if={ p.channel != null }><a href={ _CH_URL_ + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
<a class="reply" if={ p.reply }>
<i class="fa fa-reply"></i>
</a>
@@ -430,9 +430,12 @@
this.mixin('i');
this.mixin('api');
- this.mixin('stream');
this.mixin('user-preview');
+ this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
+
this.isDetailOpened = false;
this.set = post => {
@@ -468,21 +471,21 @@
this.capture = withHandler => {
if (this.SIGNIN) {
- this.stream.send({
+ this.connection.send({
type: 'capture',
id: this.post.id
});
- if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated);
+ if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
}
};
this.decapture = withHandler => {
if (this.SIGNIN) {
- this.stream.send({
+ this.connection.send({
type: 'decapture',
id: this.post.id
});
- if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated);
+ if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
}
};
@@ -490,7 +493,7 @@
this.capture(true);
if (this.SIGNIN) {
- this.stream.on('_connected_', this.onStreamConnected);
+ this.connection.on('_connected_', this.onStreamConnected);
}
if (this.p.text) {
@@ -498,7 +501,7 @@
this.refs.text.innerHTML = this.refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
- this.refs.text.children.forEach(e => {
+ Array.from(this.refs.text.children).forEach(e => {
if (e.tagName == 'MK-URL') riot.mount(e);
});
@@ -515,7 +518,8 @@
this.on('unmount', () => {
this.decapture(true);
- this.stream.off('_connected_', this.onStreamConnected);
+ this.connection.off('_connected_', this.onStreamConnected);
+ this.stream.dispose(this.connectionId);
});
this.reply = () => {
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 3123c34f4f..047964fab4 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -37,7 +37,7 @@
</mk-ui>
<mk-ui-header>
- <mk-donation if={ SIGNIN && I.data.no_donation != 'true' }/>
+ <mk-donation if={ SIGNIN && I.client_settings.show_donation }/>
<mk-special-message/>
<div class="main">
<div class="backdrop"></div>
@@ -75,8 +75,7 @@
width 100%
height 48px
backdrop-filter blur(12px)
- //background-color rgba(255, 255, 255, 0.75)
- background #1d2429
+ background #f7f7f7
&:after
content ""
@@ -138,23 +137,28 @@
> input
user-select text
cursor auto
- margin 0
+ margin 8px 0 0 0
padding 6px 18px
width 14em
- height 48px
+ height 32px
font-size 1em
- line-height calc(48px - 12px)
- background transparent
+ background rgba(0, 0, 0, 0.05)
outline none
//border solid 1px #ddd
border none
- border-radius 0
+ border-radius 16px
transition color 0.5s ease, border 0.5s ease
font-family FontAwesome, sans-serif
&::-webkit-input-placeholder
color #9eaba8
+ &:hover
+ background rgba(0, 0, 0, 0.08)
+
+ &:focus
+ box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important
+
</style>
<script>
this.mixin('page');
@@ -167,7 +171,7 @@
</mk-ui-header-search>
<mk-ui-header-post-button>
- <button onclick={ post } title="新規投稿"><i class="fa fa-pencil-square-o"></i></button>
+ <button onclick={ post } title="%i18n:desktop.tags.mk-ui-header-post-button.post%"><i class="fa fa-pencil"></i></button>
<style>
:scope
display inline-block
@@ -187,7 +191,7 @@
background $theme-color !important
outline none
border none
- border-radius 2px
+ border-radius 4px
transition background 0.1s ease
cursor pointer
@@ -210,7 +214,9 @@
</mk-ui-header-post-button>
<mk-ui-header-notifications>
- <button class="header" data-active={ isOpen } onclick={ toggle }><i class="fa fa-bell-o"></i></button>
+ <button data-active={ isOpen } onclick={ toggle } title="%i18n:desktop.tags.mk-ui-header-notifications.title%">
+ <i class="fa fa-bell-o icon"></i><i class="fa fa-circle badge" if={ hasUnreadNotifications }></i>
+ </button>
<div class="notifications" if={ isOpen }>
<mk-notifications/>
</div>
@@ -219,7 +225,7 @@
display block
float left
- > .header
+ > button
display block
margin 0
padding 0
@@ -239,10 +245,16 @@
&:active
color darken(#9eaba8, 30%)
- > i
+ > .icon
font-size 1.2em
line-height 48px
+ > .badge
+ margin-left -5px
+ vertical-align super
+ font-size 10px
+ color $theme-color
+
> .notifications
display block
position absolute
@@ -286,8 +298,53 @@
<script>
import contains from '../../common/scripts/contains';
+ this.mixin('i');
+ this.mixin('api');
+
+ if (this.SIGNIN) {
+ this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
+ }
+
this.isOpen = false;
+ this.on('mount', () => {
+ if (this.SIGNIN) {
+ this.connection.on('read_all_notifications', this.onReadAllNotifications);
+ this.connection.on('unread_notification', this.onUnreadNotification);
+
+ // Fetch count of unread notifications
+ this.api('notifications/get_unread_count').then(res => {
+ if (res.count > 0) {
+ this.update({
+ hasUnreadNotifications: true
+ });
+ }
+ });
+ }
+ });
+
+ this.on('unmount', () => {
+ if (this.SIGNIN) {
+ this.connection.off('read_all_notifications', this.onReadAllNotifications);
+ this.connection.off('unread_notification', this.onUnreadNotification);
+ this.stream.dispose(this.connectionId);
+ }
+ });
+
+ this.onReadAllNotifications = () => {
+ this.update({
+ hasUnreadNotifications: false
+ });
+ };
+
+ this.onUnreadNotification = () => {
+ this.update({
+ hasUnreadNotifications: true
+ });
+ };
+
this.toggle = () => {
this.isOpen ? this.close() : this.open();
};
@@ -322,7 +379,7 @@
<ul>
<virtual if={ SIGNIN }>
<li class="home { active: page == 'home' }">
- <a href={ CONFIG.url }>
+ <a href={ _URL_ }>
<i class="fa fa-home"></i>
<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
</a>
@@ -336,7 +393,7 @@
</li>
</virtual>
<li class="ch">
- <a href={ CONFIG.chUrl } target="_blank">
+ <a href={ _CH_URL_ } target="_blank">
<i class="fa fa-television"></i>
<p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p>
</a>
@@ -419,14 +476,19 @@
<script>
this.mixin('i');
this.mixin('api');
- this.mixin('stream');
+
+ if (this.SIGNIN) {
+ this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
+ }
this.page = this.opts.page;
this.on('mount', () => {
if (this.SIGNIN) {
- this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
+ this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+ this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
// Fetch count of unread messaging messages
this.api('messaging/unread').then(res => {
@@ -441,8 +503,9 @@
this.on('unmount', () => {
if (this.SIGNIN) {
- this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
+ this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+ this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
+ this.stream.dispose(this.connectionId);
}
});
@@ -565,7 +628,7 @@
<p><i class="fa fa-cloud"></i>%i18n:desktop.tags.mk-ui-header-account.drive%<i class="fa fa-angle-right"></i></p>
</li>
<li>
- <a href="/i>mentions"><i class="fa fa-at"></i>%i18n:desktop.tags.mk-ui-header-account.mentions%<i class="fa fa-angle-right"></i></a>
+ <a href="/i/mentions"><i class="fa fa-at"></i>%i18n:desktop.tags.mk-ui-header-account.mentions%<i class="fa fa-angle-right"></i></a>
</li>
</ul>
<ul>
diff --git a/src/web/app/desktop/tags/user-graphs.tag b/src/web/app/desktop/tags/user-graphs.tag
deleted file mode 100644
index 0677d8c187..0000000000
--- a/src/web/app/desktop/tags/user-graphs.tag
+++ /dev/null
@@ -1,41 +0,0 @@
-<mk-user-graphs>
- <section>
- <h1>投稿</h1>
- <mk-user-posts-graph user={ opts.user }/>
- </section>
- <section>
- <h1>フォロー/フォロワー</h1>
- <mk-user-friends-graph user={ opts.user }/>
- </section>
- <section>
- <h1>いいね</h1>
- <mk-user-likes-graph user={ opts.user }/>
- </section>
- <style>
- :scope
- display block
-
- > section
- margin 16px 0
- background #fff
- border solid 1px rgba(0, 0, 0, 0.1)
- border-radius 4px
-
- > h1
- margin 0 0 8px 0
- padding 0 16px
- line-height 40px
- font-size 1em
- color #666
- border-bottom solid 1px #eee
-
- > *:not(h1)
- margin 0 auto 16px auto
-
- </style>
- <script>
- this.on('mount', () => {
- this.trigger('loaded');
- });
- </script>
-</mk-user-graphs>
diff --git a/src/web/app/desktop/tags/user-header.tag b/src/web/app/desktop/tags/user-header.tag
deleted file mode 100644
index ea7ea6bb37..0000000000
--- a/src/web/app/desktop/tags/user-header.tag
+++ /dev/null
@@ -1,147 +0,0 @@
-<mk-user-header data-is-dark-background={ user.banner_url != null }>
- <div class="banner" ref="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=1024)' : '' } onclick={ onUpdateBanner }></div><img class="avatar" src={ user.avatar_url + '?thumbnail&size=150' } alt="avatar"/>
- <div class="title">
- <p class="name" href={ '/' + user.username }>{ user.name }</p>
- <p class="username">@{ user.username }</p>
- <p class="location" if={ user.profile.location }><i class="fa fa-map-marker"></i>{ user.profile.location }</p>
- </div>
- <footer>
- <a href={ '/' + user.username }>投稿</a>
- <a href={ '/' + user.username + '/media' }>メディア</a>
- <a href={ '/' + user.username + '/graphs' }>グラフ</a>
- </footer>
- <style>
- :scope
- $footer-height = 58px
-
- display block
- background #fff
-
- &[data-is-dark-background]
- > .banner
- background-color #383838
-
- > .title
- color #fff
- background linear-gradient(transparent, rgba(0, 0, 0, 0.7))
-
- > .name
- text-shadow 0 0 8px #000
-
- > .banner
- height 280px
- background-color #f5f5f5
- background-size cover
- background-position center
-
- > .avatar
- display block
- position absolute
- bottom 16px
- left 16px
- z-index 2
- width 150px
- height 150px
- margin 0
- border solid 3px #fff
- border-radius 8px
- box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2)
-
- > .title
- position absolute
- bottom $footer-height
- left 0
- width 100%
- padding 0 0 8px 195px
- color #656565
- font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif
-
- > .name
- display block
- margin 0
- line-height 40px
- font-weight bold
- font-size 2em
-
- > .username
- > .location
- display inline-block
- margin 0 16px 0 0
- line-height 20px
- opacity 0.8
-
- > i
- margin-right 4px
-
- > footer
- z-index 1
- height $footer-height
- padding-left 195px
- background #fff
-
- > a
- display inline-block
- margin 0
- width 100px
- line-height $footer-height
- color #555
-
- > button
- display block
- position absolute
- top 0
- right 0
- margin 8px
- padding 0
- width $footer-height - 16px
- line-height $footer-height - 16px - 2px
- font-size 1.2em
- color #777
- border solid 1px #eee
- border-radius 4px
-
- &:hover
- color #555
- border solid 1px #ddd
-
- </style>
- <script>
- import updateBanner from '../scripts/update-banner';
-
- this.mixin('i');
-
- this.user = this.opts.user;
-
- this.on('mount', () => {
- window.addEventListener('load', this.scroll);
- window.addEventListener('scroll', this.scroll);
- window.addEventListener('resize', this.scroll);
- });
-
- this.on('unmount', () => {
- window.removeEventListener('load', this.scroll);
- window.removeEventListener('scroll', this.scroll);
- window.removeEventListener('resize', this.scroll);
- });
-
- this.scroll = () => {
- const top = window.scrollY;
- const height = 280/*px*/;
-
- const pos = 50 - ((top / height) * 50);
- this.refs.banner.style.backgroundPosition = `center ${pos}%`;
-
- const blur = top / 32
- if (blur <= 10) this.refs.banner.style.filter = `blur(${blur}px)`;
- };
-
- this.onUpdateBanner = () => {
- if (!this.SIGNIN || this.I.id != this.user.id) return;
-
- updateBanner(this.I, i => {
- this.user.banner_url = i.banner_url;
- this.update();
- });
- };
- </script>
-</mk-user-header>
diff --git a/src/web/app/desktop/tags/user-home.tag b/src/web/app/desktop/tags/user-home.tag
deleted file mode 100644
index a879db5bb6..0000000000
--- a/src/web/app/desktop/tags/user-home.tag
+++ /dev/null
@@ -1,46 +0,0 @@
-<mk-user-home>
- <div class="side">
- <mk-user-profile user={ user }/>
- <mk-user-photos user={ user }/>
- </div>
- <main>
- <mk-user-timeline ref="tl" user={ user }/>
- </main>
- <style>
- :scope
- display flex
- justify-content center
-
- > *
- > *
- display block
- //border solid 1px #eaeaea
- border solid 1px rgba(0, 0, 0, 0.075)
- border-radius 6px
-
- &:not(:last-child)
- margin-bottom 16px
-
- > main
- flex 1 1 560px
- max-width 560px
- margin 0
- padding 16px 0 16px 16px
-
- > .side
- flex 1 1 270px
- max-width 270px
- margin 0
- padding 16px 0 16px 0
-
- </style>
- <script>
- this.user = this.opts.user;
-
- this.on('mount', () => {
- this.refs.tl.on('loaded', () => {
- this.trigger('loaded');
- });
- });
- </script>
-</mk-user-home>
diff --git a/src/web/app/desktop/tags/user-photos.tag b/src/web/app/desktop/tags/user-photos.tag
deleted file mode 100644
index dce1e50add..0000000000
--- a/src/web/app/desktop/tags/user-photos.tag
+++ /dev/null
@@ -1,91 +0,0 @@
-<mk-user-photos>
- <p class="title"><i class="fa fa-camera"></i>フォト</p>
- <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>読み込んでいます<mk-ellipsis/></p>
- <div class="stream" if={ !initializing && images.length > 0 }>
- <virtual each={ image in images }>
- <div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div>
- </virtual>
- </div>
- <p class="empty" if={ !initializing && images.length == 0 }>写真はありません</p>
- <style>
- :scope
- display block
- background #fff
-
- > .title
- z-index 1
- margin 0
- padding 0 16px
- line-height 42px
- font-size 0.9em
- font-weight bold
- color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
- > i
- margin-right 4px
-
- > .stream
- display -webkit-flex
- display -moz-flex
- display -ms-flex
- display flex
- justify-content center
- flex-wrap wrap
- padding 8px
-
- > .img
- flex 1 1 33%
- width 33%
- height 80px
- background-position center center
- background-size cover
- background-clip content-box
- border solid 2px transparent
-
- > .initializing
- > .empty
- margin 0
- padding 16px
- text-align center
- color #aaa
-
- > i
- margin-right 4px
-
- </style>
- <script>
- import isPromise from '../../common/scripts/is-promise';
-
- this.mixin('api');
-
- this.images = [];
- this.initializing = true;
- this.user = null;
- this.userPromise = isPromise(this.opts.user)
- ? this.opts.user
- : Promise.resolve(this.opts.user);
-
- this.on('mount', () => {
- this.userPromise.then(user => {
- this.update({
- user: user
- });
-
- this.api('users/posts', {
- user_id: this.user.id,
- with_media: true,
- limit: 9
- }).then(posts => {
- this.initializing = false;
- posts.forEach(post => {
- post.media.forEach(media => {
- if (this.images.length < 9) this.images.push(media);
- });
- });
- this.update();
- });
- });
- });
- </script>
-</mk-user-photos>
diff --git a/src/web/app/desktop/tags/user-profile.tag b/src/web/app/desktop/tags/user-profile.tag
deleted file mode 100644
index 7472a47801..0000000000
--- a/src/web/app/desktop/tags/user-profile.tag
+++ /dev/null
@@ -1,102 +0,0 @@
-<mk-user-profile>
- <div class="friend-form" if={ SIGNIN && I.id != user.id }>
- <mk-big-follow-button user={ user }/>
- <p class="followed" if={ user.is_followed }>フォローされています</p>
- </div>
- <div class="description" if={ user.description }>{ user.description }</div>
- <div class="birthday" if={ user.profile.birthday }>
- <p><i class="fa fa-birthday-cake"></i>{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' } ({ age(user.profile.birthday) }歳)</p>
- </div>
- <div class="twitter" if={ user.twitter }>
- <p><i class="fa fa-twitter"></i><a href={ 'https://twitter.com/' + user.twitter.screen_name } target="_blank">@{ user.twitter.screen_name }</a></p>
- </div>
- <div class="status">
- <p class="posts-count"><i class="fa fa-angle-right"></i><a>{ user.posts_count }</a><b>ポスト</b></p>
- <p class="following"><i class="fa fa-angle-right"></i><a onclick={ showFollowing }>{ user.following_count }</a>人を<b>フォロー</b></p>
- <p class="followers"><i class="fa fa-angle-right"></i><a onclick={ showFollowers }>{ user.followers_count }</a>人の<b>フォロワー</b></p>
- </div>
- <style>
- :scope
- display block
- background #fff
-
- > *:first-child
- border-top none !important
-
- > .friend-form
- padding 16px
- border-top solid 1px #eee
-
- > mk-big-follow-button
- width 100%
-
- > .followed
- margin 12px 0 0 0
- padding 0
- text-align center
- line-height 24px
- font-size 0.8em
- color #71afc7
- background #eefaff
- border-radius 4px
-
- > .description
- padding 16px
- color #555
- border-top solid 1px #eee
-
- > .birthday
- padding 16px
- color #555
- border-top solid 1px #eee
-
- > p
- margin 0
-
- > i
- margin-right 8px
-
- > .twitter
- padding 16px
- color #555
- border-top solid 1px #eee
-
- > p
- margin 0
-
- > i
- margin-right 8px
-
- > .status
- padding 16px
- color #555
- border-top solid 1px #eee
-
- > p
- margin 8px 0
-
- > i
- margin-left 8px
- margin-right 8px
-
- </style>
- <script>
- this.age = require('s-age');
-
- this.mixin('i');
-
- this.user = this.opts.user;
-
- this.showFollowing = () => {
- riot.mount(document.body.appendChild(document.createElement('mk-user-following-window')), {
- user: this.user
- });
- };
-
- this.showFollowers = () => {
- riot.mount(document.body.appendChild(document.createElement('mk-user-followers-window')), {
- user: this.user
- });
- };
- </script>
-</mk-user-profile>
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag
index 08ab47b160..5df13c436c 100644
--- a/src/web/app/desktop/tags/user-timeline.tag
+++ b/src/web/app/desktop/tags/user-timeline.tag
@@ -91,6 +91,7 @@
this.fetch = cb => {
this.api('users/posts', {
user_id: this.user.id,
+ max_date: this.date ? this.date.getTime() : undefined,
with_replies: this.mode == 'with-replies'
}).then(posts => {
this.update({
@@ -132,5 +133,13 @@
});
this.fetch();
};
+
+ this.warp = date => {
+ this.update({
+ date: date
+ });
+
+ this.fetch();
+ };
</script>
</mk-user-timeline>
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag
index db4fd7cc73..5ec6ac7624 100644
--- a/src/web/app/desktop/tags/user.tag
+++ b/src/web/app/desktop/tags/user.tag
@@ -3,33 +3,18 @@
<header>
<mk-user-header user={ user }/>
</header>
- <div class="body">
- <mk-user-home if={ page == 'home' } user={ user }/>
- <mk-user-graphs if={ page == 'graphs' } user={ user }/>
- </div>
+ <mk-user-home if={ page == 'home' } user={ user }/>
+ <mk-user-graphs if={ page == 'graphs' } user={ user }/>
</div>
<style>
:scope
display block
- background #fff
> .user
> header
- max-width 560px + 270px
- margin 0 auto
- padding 0 16px
-
> mk-user-header
- border solid 1px rgba(0, 0, 0, 0.075)
- border-top none
- border-radius 0 0 6px 6px
overflow hidden
- > .body
- max-width 560px + 270px
- margin 0 auto
- padding 0 16px
-
</style>
<script>
this.mixin('api');
@@ -52,3 +37,791 @@
});
</script>
</mk-user>
+
+<mk-user-header data-is-dark-background={ user.banner_url != null }>
+ <div class="banner-container" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=2048)' : '' }>
+ <div class="banner" ref="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=2048)' : '' } onclick={ onUpdateBanner }></div>
+ </div>
+ <div class="fade"></div>
+ <div class="container">
+ <img class="avatar" src={ user.avatar_url + '?thumbnail&size=150' } alt="avatar"/>
+ <div class="title">
+ <p class="name" href={ '/' + user.username }>{ user.name }</p>
+ <p class="username">@{ user.username }</p>
+ <p class="location" if={ user.profile.location }><i class="fa fa-map-marker"></i>{ user.profile.location }</p>
+ </div>
+ <footer>
+ <a href={ '/' + user.username } data-active={ parent.page == 'home' }><i class="fa fa-home"></i>概要</a>
+ <a href={ '/' + user.username + '/media' } data-active={ parent.page == 'media' }><i class="fa fa-picture-o"></i>メディア</a>
+ <a href={ '/' + user.username + '/graphs' } data-active={ parent.page == 'graphs' }><i class="fa fa-bar-chart"></i>グラフ</a>
+ </footer>
+ </div>
+ <style>
+ :scope
+ $banner-height = 320px
+ $footer-height = 58px
+
+ display block
+ background #f7f7f7
+ box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+
+ &[data-is-dark-background]
+ > .banner-container
+ > .banner
+ background-color #383838
+
+ > .fade
+ background linear-gradient(transparent, rgba(0, 0, 0, 0.7))
+
+ > .container
+ > .title
+ color #fff
+
+ > .name
+ text-shadow 0 0 8px #000
+
+ > .banner-container
+ height $banner-height
+ overflow hidden
+ background-size cover
+ background-position center
+
+ > .banner
+ height 100%
+ background-color #f5f5f5
+ background-size cover
+ background-position center
+
+ > .fade
+ $fade-hight = 78px
+
+ position absolute
+ top ($banner-height - $fade-hight)
+ left 0
+ width 100%
+ height $fade-hight
+
+ > .container
+ max-width 1200px
+ margin 0 auto
+
+ > .avatar
+ display block
+ position absolute
+ bottom 16px
+ left 16px
+ z-index 2
+ width 160px
+ height 160px
+ margin 0
+ border solid 3px #fff
+ border-radius 8px
+ box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2)
+
+ > .title
+ position absolute
+ bottom $footer-height
+ left 0
+ width 100%
+ padding 0 0 8px 195px
+ color #656565
+ font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif
+
+ > .name
+ display block
+ margin 0
+ line-height 40px
+ font-weight bold
+ font-size 2em
+
+ > .username
+ > .location
+ display inline-block
+ margin 0 16px 0 0
+ line-height 20px
+ opacity 0.8
+
+ > i
+ margin-right 4px
+
+ > footer
+ z-index 1
+ height $footer-height
+ padding-left 195px
+
+ > a
+ display inline-block
+ margin 0
+ padding 0 16px
+ height $footer-height
+ line-height $footer-height
+ color #555
+
+ &[data-active]
+ border-bottom solid 4px $theme-color
+
+ > i
+ margin-right 6px
+
+ > button
+ display block
+ position absolute
+ top 0
+ right 0
+ margin 8px
+ padding 0
+ width $footer-height - 16px
+ line-height $footer-height - 16px - 2px
+ font-size 1.2em
+ color #777
+ border solid 1px #eee
+ border-radius 4px
+
+ &:hover
+ color #555
+ border solid 1px #ddd
+
+ </style>
+ <script>
+ import updateBanner from '../scripts/update-banner';
+
+ this.mixin('i');
+
+ this.user = this.opts.user;
+
+ this.on('mount', () => {
+ window.addEventListener('load', this.scroll);
+ window.addEventListener('scroll', this.scroll);
+ window.addEventListener('resize', this.scroll);
+ });
+
+ this.on('unmount', () => {
+ window.removeEventListener('load', this.scroll);
+ window.removeEventListener('scroll', this.scroll);
+ window.removeEventListener('resize', this.scroll);
+ });
+
+ this.scroll = () => {
+ const top = window.scrollY;
+
+ const z = 1.25; // 奥行き(小さいほど奥)
+ const pos = -(top / z);
+ this.refs.banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
+
+ const blur = top / 32
+ if (blur <= 10) this.refs.banner.style.filter = `blur(${blur}px)`;
+ };
+
+ this.onUpdateBanner = () => {
+ if (!this.SIGNIN || this.I.id != this.user.id) return;
+
+ updateBanner(this.I, i => {
+ this.user.banner_url = i.banner_url;
+ this.update();
+ });
+ };
+ </script>
+</mk-user-header>
+
+<mk-user-profile>
+ <div class="friend-form" if={ SIGNIN && I.id != user.id }>
+ <mk-big-follow-button user={ user }/>
+ <p class="followed" if={ user.is_followed }>フォローされています</p>
+ </div>
+ <div class="description" if={ user.description }>{ user.description }</div>
+ <div class="birthday" if={ user.profile.birthday }>
+ <p><i class="fa fa-birthday-cake"></i>{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' } ({ age(user.profile.birthday) }歳)</p>
+ </div>
+ <div class="twitter" if={ user.twitter }>
+ <p><i class="fa fa-twitter"></i><a href={ 'https://twitter.com/' + user.twitter.screen_name } target="_blank">@{ user.twitter.screen_name }</a></p>
+ </div>
+ <div class="status">
+ <p class="posts-count"><i class="fa fa-angle-right"></i><a>{ user.posts_count }</a><b>ポスト</b></p>
+ <p class="following"><i class="fa fa-angle-right"></i><a onclick={ showFollowing }>{ user.following_count }</a>人を<b>フォロー</b></p>
+ <p class="followers"><i class="fa fa-angle-right"></i><a onclick={ showFollowers }>{ user.followers_count }</a>人の<b>フォロワー</b></p>
+ </div>
+ <style>
+ :scope
+ display block
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ > *:first-child
+ border-top none !important
+
+ > .friend-form
+ padding 16px
+ border-top solid 1px #eee
+
+ > mk-big-follow-button
+ width 100%
+
+ > .followed
+ margin 12px 0 0 0
+ padding 0
+ text-align center
+ line-height 24px
+ font-size 0.8em
+ color #71afc7
+ background #eefaff
+ border-radius 4px
+
+ > .description
+ padding 16px
+ color #555
+ border-top solid 1px #eee
+
+ > .birthday
+ padding 16px
+ color #555
+ border-top solid 1px #eee
+
+ > p
+ margin 0
+
+ > i
+ margin-right 8px
+
+ > .twitter
+ padding 16px
+ color #555
+ border-top solid 1px #eee
+
+ > p
+ margin 0
+
+ > i
+ margin-right 8px
+
+ > .status
+ padding 16px
+ color #555
+ border-top solid 1px #eee
+
+ > p
+ margin 8px 0
+
+ > i
+ margin-left 8px
+ margin-right 8px
+
+ </style>
+ <script>
+ this.age = require('s-age');
+
+ this.mixin('i');
+
+ this.user = this.opts.user;
+
+ this.showFollowing = () => {
+ riot.mount(document.body.appendChild(document.createElement('mk-user-following-window')), {
+ user: this.user
+ });
+ };
+
+ this.showFollowers = () => {
+ riot.mount(document.body.appendChild(document.createElement('mk-user-followers-window')), {
+ user: this.user
+ });
+ };
+ </script>
+</mk-user-profile>
+
+<mk-user-photos>
+ <p class="title"><i class="fa fa-camera"></i>%i18n:desktop.tags.mk-user.photos.title%</p>
+ <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p>
+ <div class="stream" if={ !initializing && images.length > 0 }>
+ <virtual each={ image in images }>
+ <div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div>
+ </virtual>
+ </div>
+ <p class="empty" if={ !initializing && images.length == 0 }>%i18n:desktop.tags.mk-user.photos.no-photos%</p>
+ <style>
+ :scope
+ display block
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ > .title
+ z-index 1
+ margin 0
+ padding 0 16px
+ line-height 42px
+ font-size 0.9em
+ font-weight bold
+ color #888
+ box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+ > i
+ margin-right 4px
+
+ > .stream
+ display -webkit-flex
+ display -moz-flex
+ display -ms-flex
+ display flex
+ justify-content center
+ flex-wrap wrap
+ padding 8px
+
+ > .img
+ flex 1 1 33%
+ width 33%
+ height 80px
+ background-position center center
+ background-size cover
+ background-clip content-box
+ border solid 2px transparent
+
+ > .initializing
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > i
+ margin-right 4px
+
+ </style>
+ <script>
+ import isPromise from '../../common/scripts/is-promise';
+
+ this.mixin('api');
+
+ this.images = [];
+ this.initializing = true;
+ this.user = null;
+ this.userPromise = isPromise(this.opts.user)
+ ? this.opts.user
+ : Promise.resolve(this.opts.user);
+
+ this.on('mount', () => {
+ this.userPromise.then(user => {
+ this.update({
+ user: user
+ });
+
+ this.api('users/posts', {
+ user_id: this.user.id,
+ with_media: true,
+ limit: 9
+ }).then(posts => {
+ this.initializing = false;
+ posts.forEach(post => {
+ post.media.forEach(media => {
+ if (this.images.length < 9) this.images.push(media);
+ });
+ });
+ this.update();
+ });
+ });
+ });
+ </script>
+</mk-user-photos>
+
+<mk-user-frequently-replied-users>
+ <p class="title"><i class="fa fa-users"></i>%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p>
+ <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p>
+ <div class="user" if={ !initializing && users.length != 0 } each={ _user in users }>
+ <a class="avatar-anchor" href={ '/' + _user.username }>
+ <img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/>
+ </a>
+ <div class="body">
+ <a class="name" href={ '/' + _user.username } data-user-preview={ _user.id }>{ _user.name }</a>
+ <p class="username">@{ _user.username }</p>
+ </div>
+ <mk-follow-button user={ _user }/>
+ </div>
+ <p class="empty" if={ !initializing && users.length == 0 }>%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p>
+ <style>
+ :scope
+ display block
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ > .title
+ z-index 1
+ margin 0
+ padding 0 16px
+ line-height 42px
+ font-size 0.9em
+ font-weight bold
+ color #888
+ box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+ > i
+ margin-right 4px
+
+ > .initializing
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > i
+ margin-right 4px
+
+ > .user
+ padding 16px
+ border-bottom solid 1px #eee
+
+ &:last-child
+ border-bottom none
+
+ &:after
+ content ""
+ display block
+ clear both
+
+ > .avatar-anchor
+ display block
+ float left
+ margin 0 12px 0 0
+
+ > .avatar
+ display block
+ width 42px
+ height 42px
+ margin 0
+ border-radius 8px
+ vertical-align bottom
+
+ > .body
+ float left
+ width calc(100% - 54px)
+
+ > .name
+ margin 0
+ font-size 16px
+ line-height 24px
+ color #555
+
+ > .username
+ display block
+ margin 0
+ font-size 15px
+ line-height 16px
+ color #ccc
+
+ > mk-follow-button
+ position absolute
+ top 16px
+ right 16px
+
+ </style>
+ <script>
+ this.mixin('api');
+
+ this.user = this.opts.user;
+ this.initializing = true;
+
+ this.on('mount', () => {
+ this.api('users/get_frequently_replied_users', {
+ user_id: this.user.id,
+ limit: 4
+ }).then(docs => {
+ this.update({
+ users: docs.map(doc => doc.user),
+ initializing: false
+ });
+ });
+ });
+ </script>
+</mk-user-frequently-replied-users>
+
+<mk-user-followers-you-know>
+ <p class="title"><i class="fa fa-users"></i>%i18n:desktop.tags.mk-user.followers-you-know.title%</p>
+ <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
+ <div if={ !initializing && users.length > 0 }>
+ <virtual each={ user in users }>
+ <a href={ '/' + user.username }><img src={ user.avatar_url + '?thumbnail&size=64' } alt={ user.name }/></a>
+ </virtual>
+ </div>
+ <p class="empty" if={ !initializing && users.length == 0 }>%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
+ <style>
+ :scope
+ display block
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ > .title
+ z-index 1
+ margin 0
+ padding 0 16px
+ line-height 42px
+ font-size 0.9em
+ font-weight bold
+ color #888
+ box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+ > i
+ margin-right 4px
+
+ > div
+ padding 8px
+
+ > a
+ display inline-block
+ margin 4px
+
+ > img
+ width 48px
+ height 48px
+ vertical-align bottom
+ border-radius 100%
+
+ > .initializing
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > i
+ margin-right 4px
+
+ </style>
+ <script>
+ this.mixin('api');
+
+ this.user = this.opts.user;
+ this.initializing = true;
+
+ this.on('mount', () => {
+ this.api('users/followers', {
+ user_id: this.user.id,
+ iknow: true,
+ limit: 16
+ }).then(x => {
+ this.update({
+ users: x.users,
+ initializing: false
+ });
+ });
+ });
+ </script>
+</mk-user-followers-you-know>
+
+<mk-user-home>
+ <div>
+ <div ref="left">
+ <mk-user-profile user={ user }/>
+ <mk-user-photos user={ user }/>
+ <mk-user-followers-you-know if={ SIGNIN && I.id !== user.id } user={ user }/>
+ <p>%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time time={ user.last_used_at }/></b></p>
+ </div>
+ </div>
+ <main>
+ <mk-post-detail if={ user.pinned_post } post={ user.pinned_post } compact={ true }/>
+ <mk-user-timeline ref="tl" user={ user }/>
+ </main>
+ <div>
+ <div ref="right">
+ <mk-calendar-widget warp={ warp } start={ new Date(user.created_at) }/>
+ <mk-activity-widget user={ user }/>
+ <mk-user-frequently-replied-users user={ user }/>
+ <div class="nav"><mk-nav-links/></div>
+ </div>
+ </div>
+ <style>
+ :scope
+ display flex
+ justify-content center
+ margin 0 auto
+ max-width 1200px
+
+ > main
+ > div > div
+ > *:not(:last-child)
+ margin-bottom 16px
+
+ > main
+ padding 16px
+ width calc(100% - 275px * 2)
+
+ > mk-user-timeline
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ > div
+ width 275px
+ margin 0
+
+ &:first-child > div
+ padding 16px 0 16px 16px
+
+ > p
+ display block
+ margin 0
+ padding 0 12px
+ text-align center
+ font-size 0.8em
+ color #aaa
+
+ &:last-child > div
+ padding 16px 16px 16px 0
+
+ > .nav
+ padding 16px
+ font-size 12px
+ color #aaa
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ a
+ color #999
+
+ i
+ color #ccc
+
+ </style>
+ <script>
+ import ScrollFollower from '../scripts/scroll-follower';
+
+ this.mixin('i');
+
+ this.user = this.opts.user;
+
+ this.on('mount', () => {
+ this.refs.tl.on('loaded', () => {
+ this.trigger('loaded');
+ });
+
+ this.scrollFollowerLeft = new ScrollFollower(this.refs.left, this.parent.root.getBoundingClientRect().top);
+ this.scrollFollowerRight = new ScrollFollower(this.refs.right, this.parent.root.getBoundingClientRect().top);
+ });
+
+ this.on('unmount', () => {
+ this.scrollFollowerLeft.dispose();
+ this.scrollFollowerRight.dispose();
+ });
+
+ this.warp = date => {
+ this.refs.tl.warp(date);
+ };
+ </script>
+</mk-user-home>
+
+<mk-user-graphs>
+ <section>
+ <div>
+ <h1><i class="fa fa-pencil"></i>投稿</h1>
+ <mk-user-graphs-activity-chart user={ opts.user }/>
+ </div>
+ </section>
+ <section>
+ <div>
+ <h1>フォロー/フォロワー</h1>
+ <mk-user-friends-graph user={ opts.user }/>
+ </div>
+ </section>
+ <section>
+ <div>
+ <h1>いいね</h1>
+ <mk-user-likes-graph user={ opts.user }/>
+ </div>
+ </section>
+ <style>
+ :scope
+ display block
+
+ > section
+ margin 16px 0
+ color #666
+ border-bottom solid 1px rgba(0, 0, 0, 0.1)
+
+ > div
+ max-width 1200px
+ margin 0 auto
+ padding 0 16px
+
+ > h1
+ margin 0 0 16px 0
+ padding 0
+ font-size 1.3em
+
+ > i
+ margin-right 8px
+
+ </style>
+ <script>
+ this.on('mount', () => {
+ this.trigger('loaded');
+ });
+ </script>
+</mk-user-graphs>
+
+<mk-user-graphs-activity-chart>
+ <svg if={ data } ref="canvas" viewBox="0 0 365 1" preserveAspectRatio="none">
+ <g each={ d, i in data.reverse() }>
+ <rect width="0.8" riot-height={ d.postsH }
+ riot-x={ i + 0.1 } riot-y={ 1 - d.postsH - d.repliesH - d.repostsH }
+ fill="#41ddde"/>
+ <rect width="0.8" riot-height={ d.repliesH }
+ riot-x={ i + 0.1 } riot-y={ 1 - d.repliesH - d.repostsH }
+ fill="#f7796c"/>
+ <rect width="0.8" riot-height={ d.repostsH }
+ riot-x={ i + 0.1 } riot-y={ 1 - d.repostsH }
+ fill="#a1de41"/>
+ </g>
+ </svg>
+ <p>直近1年間分の統計です。一番右が現在で、一番左が1年前です。青は通常の投稿、赤は返信、緑はRepostをそれぞれ表しています。</p>
+ <p>
+ <span>だいたい*1日に<b>{ averageOfAllTypePostsEachDays }回</b>投稿(返信、Repost含む)しています。</span><br>
+ <span>だいたい*1日に<b>{ averageOfPostsEachDays }回</b>投稿(通常の)しています。</span><br>
+ <span>だいたい*1日に<b>{ averageOfRepliesEachDays }回</b>返信しています。</span><br>
+ <span>だいたい*1日に<b>{ averageOfRepostsEachDays }回</b>Repostしています。</span><br>
+ </p>
+ <p>* 中央値</p>
+
+ <style>
+ :scope
+ display block
+
+ > svg
+ display block
+ width 100%
+ height 180px
+
+ > rect
+ transform-origin center
+
+ </style>
+ <script>
+ import getMedian from '../../common/scripts/get-median';
+
+ this.mixin('api');
+
+ this.user = this.opts.user;
+
+ this.on('mount', () => {
+ this.api('aggregation/users/activity', {
+ user_id: this.user.id,
+ limit: 365
+ }).then(data => {
+ data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+ this.peak = Math.max.apply(null, data.map(d => d.total));
+ data.forEach(d => {
+ d.postsH = d.posts / this.peak;
+ d.repliesH = d.replies / this.peak;
+ d.repostsH = d.reposts / this.peak;
+ });
+
+ this.update({
+ data,
+ averageOfAllTypePostsEachDays: getMedian(data.map(d => d.total)),
+ averageOfPostsEachDays: getMedian(data.map(d => d.posts)),
+ averageOfRepliesEachDays: getMedian(data.map(d => d.replies)),
+ averageOfRepostsEachDays: getMedian(data.map(d => d.reposts))
+ });
+ });
+ });
+ </script>
+</mk-user-graphs-activity-chart>
diff --git a/src/web/app/desktop/tags/widgets/activity.tag b/src/web/app/desktop/tags/widgets/activity.tag
new file mode 100644
index 0000000000..baf385fe92
--- /dev/null
+++ b/src/web/app/desktop/tags/widgets/activity.tag
@@ -0,0 +1,246 @@
+<mk-activity-widget data-melt={ design == 2 }>
+ <virtual if={ design == 0 }>
+ <p class="title"><i class="fa fa-bar-chart"></i>%i18n:desktop.tags.mk-activity-widget.title%</p>
+ <button onclick={ toggle } title="%i18n:desktop.tags.mk-activity-widget.toggle%"><i class="fa fa-sort"></i></button>
+ </virtual>
+ <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p>
+ <mk-activity-widget-calender if={ !initializing && view == 0 } data={ [].concat(activity) }/>
+ <mk-activity-widget-chart if={ !initializing && view == 1 } data={ [].concat(activity) }/>
+ <style>
+ :scope
+ display block
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ &[data-melt]
+ background transparent !important
+ border none !important
+
+ > .title
+ z-index 1
+ margin 0
+ padding 0 16px
+ line-height 42px
+ font-size 0.9em
+ font-weight bold
+ color #888
+ box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+ > i
+ margin-right 4px
+
+ > button
+ position absolute
+ z-index 2
+ top 0
+ right 0
+ padding 0
+ width 42px
+ font-size 0.9em
+ line-height 42px
+ color #ccc
+
+ &:hover
+ color #aaa
+
+ &:active
+ color #999
+
+ > .initializing
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > i
+ margin-right 4px
+
+ </style>
+ <script>
+ this.mixin('api');
+
+ this.design = this.opts.design || 0;
+ this.view = this.opts.view || 0;
+
+ this.user = this.opts.user;
+ this.initializing = true;
+
+ this.on('mount', () => {
+ this.api('aggregation/users/activity', {
+ user_id: this.user.id,
+ limit: 20 * 7
+ }).then(activity => {
+ this.update({
+ initializing: false,
+ activity
+ });
+ });
+ });
+
+ this.toggle = () => {
+ this.view++;
+ if (this.view == 2) this.view = 0;
+ this.update();
+ this.trigger('view-changed', this.view);
+ };
+ </script>
+</mk-activity-widget>
+
+<mk-activity-widget-calender>
+ <svg viewBox="0 0 21 7" preserveAspectRatio="none">
+ <rect each={ data } class="day"
+ width="1" height="1"
+ riot-x={ x } riot-y={ date.weekday }
+ rx="1" ry="1"
+ fill="transparent">
+ <title>{ date.year }/{ date.month }/{ date.day }<br/>Post: { posts }, Reply: { replies }, Repost: { reposts }</title>
+ </rect>
+ <rect each={ data }
+ riot-width={ v } riot-height={ v }
+ riot-x={ x + ((1 - v) / 2) } riot-y={ date.weekday + ((1 - v) / 2) }
+ rx="1" ry="1"
+ fill={ color }
+ style="pointer-events: none;"/>
+ <rect class="today"
+ width="1" height="1"
+ riot-x={ data[data.length - 1].x } riot-y={ data[data.length - 1].date.weekday }
+ rx="1" ry="1"
+ fill="none"
+ stroke-width="0.1"
+ stroke="#f73520"/>
+ </svg>
+ <style>
+ :scope
+ display block
+
+ > svg
+ display block
+ padding 10px
+ width 100%
+
+ > rect
+ transform-origin center
+
+ &.day
+ &:hover
+ fill rgba(0, 0, 0, 0.05)
+
+ </style>
+ <script>
+ this.data = this.opts.data;
+ this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+ const peak = Math.max.apply(null, this.data.map(d => d.total));
+
+ let x = 0;
+ this.data.reverse().forEach(d => {
+ d.x = x;
+ d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();
+
+ d.v = d.total / (peak / 2);
+ if (d.v > 1) d.v = 1;
+ const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
+ const cs = d.v * 100;
+ const cl = 15 + ((1 - d.v) * 80);
+ d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
+
+ if (d.date.weekday == 6) x++;
+ });
+ </script>
+</mk-activity-widget-calender>
+
+<mk-activity-widget-chart>
+ <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none" onmousedown={ onMousedown }>
+ <title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title>
+ <polyline
+ riot-points={ pointsPost }
+ fill="none"
+ stroke-width="1"
+ stroke="#41ddde"/>
+ <polyline
+ riot-points={ pointsReply }
+ fill="none"
+ stroke-width="1"
+ stroke="#f7796c"/>
+ <polyline
+ riot-points={ pointsRepost }
+ fill="none"
+ stroke-width="1"
+ stroke="#a1de41"/>
+ <polyline
+ riot-points={ pointsTotal }
+ fill="none"
+ stroke-width="1"
+ stroke="#555"
+ stroke-dasharray="2 2"/>
+ </svg>
+ <style>
+ :scope
+ display block
+
+ > svg
+ display block
+ padding 10px
+ width 100%
+ cursor all-scroll
+ </style>
+ <script>
+ this.viewBoxX = 140;
+ this.viewBoxY = 60;
+ this.zoom = 1;
+ this.pos = 0;
+
+ this.data = this.opts.data.reverse();
+ this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+ const peak = Math.max.apply(null, this.data.map(d => d.total));
+
+ this.on('mount', () => {
+ this.render();
+ });
+
+ this.render = () => {
+ this.update({
+ pointsPost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' '),
+ pointsReply: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '),
+ pointsRepost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' '),
+ pointsTotal: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ')
+ });
+ };
+
+ this.onMousedown = e => {
+ e.preventDefault();
+
+ const clickX = e.clientX;
+ const clickY = e.clientY;
+ const baseZoom = this.zoom;
+ const basePos = this.pos;
+
+ // 動かした時
+ dragListen(me => {
+ let moveLeft = me.clientX - clickX;
+ let moveTop = me.clientY - clickY;
+
+ this.zoom = baseZoom + (-moveTop / 20);
+ this.pos = basePos + moveLeft;
+ if (this.zoom < 1) this.zoom = 1;
+ if (this.pos > 0) this.pos = 0;
+ if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
+
+ this.render();
+ });
+ };
+
+ function dragListen(fn) {
+ window.addEventListener('mousemove', fn);
+ window.addEventListener('mouseleave', dragClear.bind(null, fn));
+ window.addEventListener('mouseup', dragClear.bind(null, fn));
+ }
+
+ function dragClear(fn) {
+ window.removeEventListener('mousemove', fn);
+ window.removeEventListener('mouseleave', dragClear);
+ window.removeEventListener('mouseup', dragClear);
+ }
+ </script>
+</mk-activity-widget-chart>
+
diff --git a/src/web/app/desktop/tags/widgets/calendar.tag b/src/web/app/desktop/tags/widgets/calendar.tag
new file mode 100644
index 0000000000..5f00d5cf29
--- /dev/null
+++ b/src/web/app/desktop/tags/widgets/calendar.tag
@@ -0,0 +1,241 @@
+<mk-calendar-widget data-melt={ opts.design == 4 || opts.design == 5 }>
+ <virtual if={ opts.design == 0 || opts.design == 1 }>
+ <button onclick={ prev } title="%i18n:desktop.tags.mk-calendar-widget.prev%"><i class="fa fa-chevron-circle-left"></i></button>
+ <p class="title">{ '%i18n:desktop.tags.mk-calendar-widget.title%'.replace('{1}', year).replace('{2}', month) }</p>
+ <button onclick={ next } title="%i18n:desktop.tags.mk-calendar-widget.next%"><i class="fa fa-chevron-circle-right"></i></button>
+ </virtual>
+
+ <div class="calendar">
+ <div class="weekday" if={ opts.design == 0 || opts.design == 2 || opts.design == 4} each={ day, i in Array(7).fill(0) }
+ data-today={ year == today.getFullYear() && month == today.getMonth() + 1 && today.getDay() == i }
+ data-is-donichi={ i == 0 || i == 6 }>{ weekdayText[i] }</div>
+ <div each={ day, i in Array(paddingDays).fill(0) }></div>
+ <div class="day" each={ day, i in Array(days).fill(0) }
+ data-today={ isToday(i + 1) }
+ data-selected={ isSelected(i + 1) }
+ data-is-out-of-range={ isOutOfRange(i + 1) }
+ data-is-donichi={ isDonichi(i + 1) }
+ onclick={ go.bind(null, i + 1) }
+ title={ isOutOfRange(i + 1) ? null : '%i18n:desktop.tags.mk-calendar-widget.go%' }><div>{ i + 1 }</div></div>
+ </div>
+ <style>
+ :scope
+ display block
+ color #777
+ background #fff
+ border solid 1px rgba(0, 0, 0, 0.075)
+ border-radius 6px
+
+ &[data-melt]
+ background transparent !important
+ border none !important
+
+ > .title
+ z-index 1
+ margin 0
+ padding 0 16px
+ text-align center
+ line-height 42px
+ font-size 0.9em
+ font-weight bold
+ color #888
+ box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+ > i
+ margin-right 4px
+
+ > button
+ position absolute
+ z-index 2
+ top 0
+ padding 0
+ width 42px
+ font-size 0.9em
+ line-height 42px
+ color #ccc
+
+ &:hover
+ color #aaa
+
+ &:active
+ color #999
+
+ &:first-of-type
+ left 0
+
+ &:last-of-type
+ right 0
+
+ > .calendar
+ display flex
+ flex-wrap wrap
+ padding 16px
+
+ *
+ user-select none
+
+ > div
+ width calc(100% * (1/7))
+ text-align center
+ line-height 32px
+ font-size 14px
+
+ &.weekday
+ color #19a2a9
+
+ &[data-is-donichi]
+ color #ef95a0
+
+ &[data-today]
+ box-shadow 0 0 0 1px #19a2a9 inset
+ border-radius 6px
+
+ &[data-is-donichi]
+ box-shadow 0 0 0 1px #ef95a0 inset
+
+ &.day
+ cursor pointer
+ color #777
+
+ > div
+ border-radius 6px
+
+ &:hover > div
+ background rgba(0, 0, 0, 0.025)
+
+ &:active > div
+ background rgba(0, 0, 0, 0.05)
+
+ &[data-is-donichi]
+ color #ef95a0
+
+ &[data-is-out-of-range]
+ cursor default
+ color rgba(#777, 0.5)
+
+ &[data-is-donichi]
+ color rgba(#ef95a0, 0.5)
+
+ &[data-selected]
+ font-weight bold
+
+ > div
+ background rgba(0, 0, 0, 0.025)
+
+ &:active > div
+ background rgba(0, 0, 0, 0.05)
+
+ &[data-today]
+ > div
+ color $theme-color-foreground
+ background $theme-color
+
+ &:hover > div
+ background lighten($theme-color, 10%)
+
+ &:active > div
+ background darken($theme-color, 10%)
+
+ </style>
+ <script>
+ if (this.opts.design == null) this.opts.design = 0;
+
+ const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+
+ function isLeapYear(year) {
+ return (year % 400 == 0) ? true :
+ (year % 100 == 0) ? false :
+ (year % 4 == 0) ? true :
+ false;
+ }
+
+ this.today = new Date();
+ this.year = this.today.getFullYear();
+ this.month = this.today.getMonth() + 1;
+ this.selected = this.today;
+ this.weekdayText = [
+ '%i18n:common.weekday-short.sunday%',
+ '%i18n:common.weekday-short.monday%',
+ '%i18n:common.weekday-short.tuesday%',
+ '%i18n:common.weekday-short.wednesday%',
+ '%i18n:common.weekday-short.thursday%',
+ '%i18n:common.weekday-short.friday%',
+ '%i18n:common.weekday-short.satruday%'
+ ];
+
+ this.on('mount', () => {
+ this.calc();
+ });
+
+ this.isToday = day => {
+ return this.year == this.today.getFullYear() && this.month == this.today.getMonth() + 1 && day == this.today.getDate();
+ };
+
+ this.isSelected = day => {
+ return this.year == this.selected.getFullYear() && this.month == this.selected.getMonth() + 1 && day == this.selected.getDate();
+ };
+
+ this.isOutOfRange = day => {
+ const test = (new Date(this.year, this.month - 1, day)).getTime();
+ return test > this.today.getTime() ||
+ (this.opts.start ? test < this.opts.start.getTime() : false);
+ };
+
+ this.isDonichi = day => {
+ const weekday = (new Date(this.year, this.month - 1, day)).getDay();
+ return weekday == 0 || weekday == 6;
+ };
+
+ this.calc = () => {
+ let days = eachMonthDays[this.month - 1];
+
+ // うるう年なら+1日
+ if (this.month == 2 && isLeapYear(this.year)) days++;
+
+ const date = new Date(this.year, this.month - 1, 1);
+ const weekday = date.getDay();
+
+ this.update({
+ paddingDays: weekday,
+ days: days
+ });
+ };
+
+ this.prev = () => {
+ if (this.month == 1) {
+ this.update({
+ year: this.year - 1,
+ month: 12
+ });
+ } else {
+ this.update({
+ month: this.month - 1
+ });
+ }
+ this.calc();
+ };
+
+ this.next = () => {
+ if (this.month == 12) {
+ this.update({
+ year: this.year + 1,
+ month: 1
+ });
+ } else {
+ this.update({
+ month: this.month + 1
+ });
+ }
+ this.calc();
+ };
+
+ this.go = day => {
+ if (this.isOutOfRange(day)) return;
+ const date = new Date(this.year, this.month - 1, day, 23, 59, 59, 999);
+ this.update({
+ selected: date
+ });
+ this.opts.warp(date);
+ };
+</script>
+</mk-calendar-widget>
diff --git a/src/web/app/desktop/tags/window.tag b/src/web/app/desktop/tags/window.tag
index aefb6499b7..256cfb7900 100644
--- a/src/web/app/desktop/tags/window.tag
+++ b/src/web/app/desktop/tags/window.tag
@@ -4,7 +4,10 @@
<div class="body">
<header ref="header" onmousedown={ onHeaderMousedown }>
<h1 data-yield="header"><yield from="header"/></h1>
- <button class="close" if={ canClose } onmousedown={ repelMove } onclick={ close } title="閉じる"><i class="fa fa-times"></i></button>
+ <div>
+ <button class="popout" if={ popoutUrl } onmousedown={ repelMove } onclick={ popout } title="ポップアウト"><i class="fa fa-window-restore"></i></button>
+ <button class="close" if={ canClose } onmousedown={ repelMove } onclick={ close } title="閉じる"><i class="fa fa-times"></i></button>
+ </div>
</header>
<div class="content" data-yield="content"><yield from="content"/></div>
</div>
@@ -117,8 +120,12 @@
box-shadow 0 2px 6px 0 rgba(0, 0, 0, 0.2)
> header
+ $header-height = 40px
+
z-index 128
+ height $header-height
overflow hidden
+ white-space nowrap
cursor move
background #fff
border-radius 6px 6px 0 0
@@ -130,39 +137,45 @@
> h1
pointer-events none
display block
- margin 0
- height 40px
+ margin 0 auto
+ overflow hidden
+ height $header-height
+ text-overflow ellipsis
text-align center
font-size 1em
- line-height 40px
+ line-height $header-height
font-weight normal
color #666
- > .close
- cursor pointer
- display block
+ > div:last-child
position absolute
top 0
right 0
+ display block
z-index 1
- margin 0
- padding 0
- font-size 1.2em
- color rgba(#000, 0.4)
- border none
- outline none
- background transparent
- &:hover
- color rgba(#000, 0.6)
+ > *
+ display inline-block
+ margin 0
+ padding 0
+ cursor pointer
+ font-size 1.2em
+ color rgba(#000, 0.4)
+ border none
+ outline none
+ background transparent
+
+ &:hover
+ color rgba(#000, 0.6)
- &:active
- color darken(#000, 30%)
+ &:active
+ color darken(#000, 30%)
- > i
- padding 0
- width 40px
- line-height 40px
+ > i
+ padding 0
+ width $header-height
+ line-height $header-height
+ text-align center
> .content
height 100%
@@ -181,6 +194,7 @@
this.isModal = this.opts.isModal != null ? this.opts.isModal : false;
this.canClose = this.opts.canClose != null ? this.opts.canClose : true;
+ this.popoutUrl = this.opts.popout;
this.isFlexible = this.opts.height == null;
this.canResize = !this.isFlexible;
@@ -247,6 +261,22 @@
}, 300);
};
+ this.popout = () => {
+ const position = this.refs.main.getBoundingClientRect();
+
+ const width = parseInt(getComputedStyle(this.refs.main, '').width, 10);
+ const height = parseInt(getComputedStyle(this.refs.main, '').height, 10);
+ const x = window.screenX + position.left;
+ const y = window.screenY + position.top;
+
+ const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl;
+
+ window.open(url, url,
+ `height=${height},width=${width},left=${x},top=${y}`);
+
+ this.close();
+ };
+
this.close = () => {
this.trigger('closing');
diff --git a/src/web/app/dev/router.js b/src/web/app/dev/router.ts
index 7fde30fa5c..fcd2b1f76b 100644
--- a/src/web/app/dev/router.js
+++ b/src/web/app/dev/router.ts
@@ -1,8 +1,8 @@
import * as riot from 'riot';
-const route = require('page');
+import * as route from 'page';
let page = null;
-export default me => {
+export default () => {
route('/', index);
route('/apps', apps);
route('/app/new', newApp);
@@ -32,7 +32,7 @@ export default me => {
}
// EXEC
- route();
+ (route as any)();
};
function mount(content) {
diff --git a/src/web/app/dev/script.js b/src/web/app/dev/script.ts
index 39d7fc891e..b115c5be48 100644
--- a/src/web/app/dev/script.js
+++ b/src/web/app/dev/script.ts
@@ -12,7 +12,7 @@ import route from './router';
/**
* init
*/
-init(me => {
+init(() => {
// Start routing
- route(me);
+ route();
});
diff --git a/src/web/app/dev/tags/index.js b/src/web/app/dev/tags/index.ts
index 1e0c73697e..1e0c73697e 100644
--- a/src/web/app/dev/tags/index.js
+++ b/src/web/app/dev/tags/index.ts
diff --git a/src/web/app/init.js b/src/web/app/init.js
deleted file mode 100644
index 5a6899ed4f..0000000000
--- a/src/web/app/init.js
+++ /dev/null
@@ -1,206 +0,0 @@
-/**
- * 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 Connection from './common/scripts/home-stream';
-import Progress from './common/scripts/loading';
-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} (葵 aoi)`);
-
-{ // Set lang attr
- const html = document.documentElement;
- html.setAttribute('lang', LANG);
-}
-
-{ // Set description meta tag
- const head = document.getElementsByTagName('head')[0];
- const meta = document.createElement('meta');
- meta.setAttribute('name', 'description');
- meta.setAttribute('content', '%i18n:common.misskey%');
- head.appendChild(meta);
-}
-
-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));
- });
- }
-
- // Init home stream connection
- const stream = me ? new Connection(me) : null;
-
- // ミックスイン初期化
- mixin(me, stream);
-
- // ローディング画面クリア
- 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, stream);
- } 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
- // Render the error screen
- document.body.innerHTML = '<mk-error />';
- riot.mount('*');
- Progress.done();
- });
-
- 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.documentElement.style.background = '#1269e2';
- document.body.innerHTML =
- '<div id="error">'
- + '<h1>:( 致命的な問題が発生しました。</h1>'
- + '<p>お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。</p>'
- + '<hr>'
- + `<p>エラーコード: ${e.toString()}</p>`
- + `<p>ブラウザ バージョン: ${navigator.userAgent}</p>`
- + `<p>クライアント バージョン: ${VERSION}</p>`
- + '<hr>'
- + '<p>問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。</p>'
- + '<p>Thank you for using Misskey.</p>'
- + '</div>';
-
- // TODO: Report the bug
-}
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
new file mode 100644
index 0000000000..79be1d3687
--- /dev/null
+++ b/src/web/app/init.ts
@@ -0,0 +1,105 @@
+/**
+ * App initializer
+ */
+
+declare const _VERSION_: string;
+declare const _LANG_: string;
+declare const _HOST_: string;
+declare const __CONSTS__: any;
+
+import * as riot from 'riot';
+import checkForUpdate from './common/scripts/check-for-update';
+import mixin from './common/mixins';
+import MiOS from './common/mios';
+require('./common/tags');
+
+/**
+ * APP ENTRY POINT!
+ */
+
+console.info(`Misskey v${_VERSION_} (葵 aoi)`);
+
+if (_HOST_ != 'localhost') {
+ document.domain = _HOST_;
+}
+
+{ // Set lang attr
+ const html = document.documentElement;
+ html.setAttribute('lang', _LANG_);
+}
+
+{ // Set description meta tag
+ const head = document.getElementsByTagName('head')[0];
+ const meta = document.createElement('meta');
+ meta.setAttribute('name', 'description');
+ meta.setAttribute('content', '%i18n:common.misskey%');
+ head.appendChild(meta);
+}
+
+// Set global configuration
+(riot as any).mixin(__CONSTS__);
+
+// 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);
+}
+
+// MiOSを初期化してコールバックする
+export default (callback, sw = false) => {
+ const mios = new MiOS(sw);
+
+ mios.init(() => {
+ // ミックスイン初期化
+ mixin(mios);
+
+ // ローディング画面クリア
+ const ini = document.getElementById('ini');
+ ini.parentNode.removeChild(ini);
+
+ // アプリ基底要素マウント
+ const app = document.createElement('div');
+ app.setAttribute('id', 'app');
+ document.body.appendChild(app);
+
+ try {
+ callback(mios);
+ } catch (e) {
+ panic(e);
+ }
+
+ // 更新チェック
+ setTimeout(() => {
+ checkForUpdate(mios);
+ }, 3000);
+ });
+};
+
+// BSoD
+function panic(e) {
+ console.error(e);
+
+ // Display blue screen
+ document.documentElement.style.background = '#1269e2';
+ document.body.innerHTML =
+ '<div id="error">'
+ + '<h1>:( 致命的な問題が発生しました。</h1>'
+ + '<p>お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。</p>'
+ + '<hr>'
+ + `<p>エラーコード: ${e.toString()}</p>`
+ + `<p>ブラウザ バージョン: ${navigator.userAgent}</p>`
+ + `<p>クライアント バージョン: ${_VERSION_}</p>`
+ + '<hr>'
+ + '<p>問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。</p>'
+ + '<p>Thank you for using Misskey.</p>'
+ + '</div>';
+
+ // TODO: Report the bug
+}
diff --git a/src/web/app/mobile/router.js b/src/web/app/mobile/router.ts
index 01eb3c8145..0358d10e9e 100644
--- a/src/web/app/mobile/router.js
+++ b/src/web/app/mobile/router.ts
@@ -3,10 +3,11 @@
*/
import * as riot from 'riot';
-const route = require('page');
+import * as route from 'page';
+import MiOS from '../common/mios';
let page = null;
-export default me => {
+export default (mios: MiOS) => {
route('/', index);
route('/selectdrive', selectDrive);
route('/i/notifications', notifications);
@@ -32,7 +33,7 @@ export default me => {
route('*', notFound);
function index() {
- me ? home() : entrance();
+ mios.isSignedin ? home() : entrance();
}
function home() {
@@ -131,12 +132,12 @@ export default me => {
mount(document.createElement('mk-not-found'));
}
- riot.mixin('page', {
+ (riot as any).mixin('page', {
page: route
});
// EXEC
- route();
+ (route as any)();
};
function mount(content) {
diff --git a/src/web/app/mobile/script.js b/src/web/app/mobile/script.ts
index 503e0fd673..4dfff8f72f 100644
--- a/src/web/app/mobile/script.js
+++ b/src/web/app/mobile/script.ts
@@ -8,14 +8,15 @@ import './style.styl';
require('./tags');
import init from '../init';
import route from './router';
+import MiOS from '../common/mios';
/**
* init
*/
-init(me => {
+init((mios: MiOS) => {
// http://qiita.com/junya/items/3ff380878f26ca447f85
document.body.setAttribute('ontouchstart', '');
// Start routing
- route(me);
-});
+ route(mios);
+}, true);
diff --git a/src/web/app/mobile/scripts/open-post-form.js b/src/web/app/mobile/scripts/open-post-form.ts
index e0fae4d8ca..e0fae4d8ca 100644
--- a/src/web/app/mobile/scripts/open-post-form.js
+++ b/src/web/app/mobile/scripts/open-post-form.ts
diff --git a/src/web/app/mobile/scripts/ui-event.js b/src/web/app/mobile/scripts/ui-event.ts
index 2e406549a4..2e406549a4 100644
--- a/src/web/app/mobile/scripts/ui-event.js
+++ b/src/web/app/mobile/scripts/ui-event.ts
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index 6929c50ab1..2c36c43ac5 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -172,7 +172,10 @@
<script>
this.mixin('i');
this.mixin('api');
- this.mixin('stream');
+
+ this.mixin('drive-stream');
+ this.connection = this.driveStream.getConnection();
+ this.connectionId = this.driveStream.use();
this.files = [];
this.folders = [];
@@ -189,10 +192,10 @@
this.multiple = this.opts.multiple;
this.on('mount', () => {
- this.stream.on('drive_file_created', this.onStreamDriveFileCreated);
- this.stream.on('drive_file_updated', this.onStreamDriveFileUpdated);
- this.stream.on('drive_folder_created', this.onStreamDriveFolderCreated);
- this.stream.on('drive_folder_updated', this.onStreamDriveFolderUpdated);
+ this.connection.on('file_created', this.onStreamDriveFileCreated);
+ this.connection.on('file_updated', this.onStreamDriveFileUpdated);
+ this.connection.on('folder_created', this.onStreamDriveFolderCreated);
+ this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
if (this.opts.folder) {
this.cd(this.opts.folder, true);
@@ -208,10 +211,11 @@
});
this.on('unmount', () => {
- this.stream.off('drive_file_created', this.onStreamDriveFileCreated);
- this.stream.off('drive_file_updated', this.onStreamDriveFileUpdated);
- this.stream.off('drive_folder_created', this.onStreamDriveFolderCreated);
- this.stream.off('drive_folder_updated', this.onStreamDriveFolderUpdated);
+ this.connection.off('file_created', this.onStreamDriveFileCreated);
+ this.connection.off('file_updated', this.onStreamDriveFileUpdated);
+ this.connection.off('folder_created', this.onStreamDriveFolderCreated);
+ this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
+ this.driveStream.dispose(this.connectionId);
});
this.onStreamDriveFileCreated = file => {
@@ -561,7 +565,7 @@
};
this.changeLocalFile = () => {
- this.refs.file.files.forEach(f => this.refs.uploader.upload(f, this.folder));
+ Array.from(this.refs.file.files).forEach(f => this.refs.uploader.upload(f, this.folder));
};
</script>
</mk-drive>
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index e6129652b0..2cec4f329e 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -2,7 +2,7 @@
<div class="preview">
<img if={ kind == 'image' } src={ file.url } alt={ file.name } title={ file.name }>
<i if={ kind != 'image' } class="fa fa-file"></i>
- <footer if={ kind == 'image' }>
+ <footer if={ kind == 'image' && file.properties && file.properties.width && file.properties.height }>
<span class="size">
<span class="width">{ file.properties.width }</span>
<span class="time">×</span>
@@ -44,7 +44,7 @@
<p>
<i class="fa fa-hashtag"></i>%i18n:mobile.tags.mk-drive-file-viewer.hash%
</p>
- <code>{ file.hash }</code>
+ <code>{ file.md5 }</code>
</div>
</div>
<style>
diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
index bf51f79a5d..1499e8d7b7 100644
--- a/src/web/app/mobile/tags/drive/file.tag
+++ b/src/web/app/mobile/tags/drive/file.tag
@@ -2,7 +2,7 @@
<div class="container">
<div class="thumbnail" style={ 'background-image: url(' + file.url + '?thumbnail&size=128)' }></div>
<div class="body">
- <p class="name">{ file.name }</p>
+ <p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
<!--
if file.tags.length > 0
ul.tags
@@ -64,6 +64,9 @@
text-overflow ellipsis
overflow-wrap break-word
+ > .ext
+ opacity 0.5
+
> .tags
display block
margin 4px 0 0 0
diff --git a/src/web/app/mobile/tags/follow-button.tag b/src/web/app/mobile/tags/follow-button.tag
index 67d580eb99..f25b2ed50e 100644
--- a/src/web/app/mobile/tags/follow-button.tag
+++ b/src/web/app/mobile/tags/follow-button.tag
@@ -52,7 +52,10 @@
this.mixin('i');
this.mixin('api');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.user = null;
this.userPromise = isPromise(this.opts.user)
@@ -67,14 +70,15 @@
init: false,
user: user
});
- this.stream.on('follow', this.onStreamFollow);
- this.stream.on('unfollow', this.onStreamUnfollow);
+ this.connection.on('follow', this.onStreamFollow);
+ this.connection.on('unfollow', this.onStreamUnfollow);
});
});
this.on('unmount', () => {
- this.stream.off('follow', this.onStreamFollow);
- this.stream.off('unfollow', this.onStreamUnfollow);
+ this.connection.off('follow', this.onStreamFollow);
+ this.connection.off('unfollow', this.onStreamUnfollow);
+ this.stream.dispose(this.connectionId);
});
this.onStreamFollow = user => {
diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
index 051158597d..e96823fa10 100644
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ b/src/web/app/mobile/tags/home-timeline.tag
@@ -12,7 +12,10 @@
<script>
this.mixin('i');
this.mixin('api');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.noFollowing = this.I.following_count == 0;
@@ -30,15 +33,16 @@
};
this.on('mount', () => {
- this.stream.on('post', this.onStreamPost);
- this.stream.on('follow', this.onStreamFollow);
- this.stream.on('unfollow', this.onStreamUnfollow);
+ this.connection.on('post', this.onStreamPost);
+ this.connection.on('follow', this.onStreamFollow);
+ this.connection.on('unfollow', this.onStreamUnfollow);
});
this.on('unmount', () => {
- this.stream.off('post', this.onStreamPost);
- this.stream.off('follow', this.onStreamFollow);
- this.stream.off('unfollow', this.onStreamUnfollow);
+ this.connection.off('post', this.onStreamPost);
+ this.connection.off('follow', this.onStreamFollow);
+ this.connection.off('unfollow', this.onStreamUnfollow);
+ this.stream.dispose(this.connectionId);
});
this.more = () => {
diff --git a/src/web/app/mobile/tags/index.js b/src/web/app/mobile/tags/index.ts
index 19952c20cd..19952c20cd 100644
--- a/src/web/app/mobile/tags/index.js
+++ b/src/web/app/mobile/tags/index.ts
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index 2e95990314..7406fd95e2 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -82,7 +82,10 @@
this.getPostSummary = getPostSummary;
this.mixin('api');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.notifications = [];
this.loading = true;
@@ -106,11 +109,12 @@
this.trigger('fetched');
});
- this.stream.on('notification', this.onNotification);
+ this.connection.on('notification', this.onNotification);
});
this.on('unmount', () => {
- this.stream.off('notification', this.onNotification);
+ this.connection.off('notification', this.onNotification);
+ this.stream.dispose(this.connectionId);
});
this.on('update', () => {
@@ -124,7 +128,7 @@
this.onNotification = notification => {
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
- this.stream.send({
+ this.connection.send({
type: 'read_notification',
id: notification.id
});
diff --git a/src/web/app/mobile/tags/page/entrance/signin.tag b/src/web/app/mobile/tags/page/entrance/signin.tag
index 827fbccb94..6f473feb9d 100644
--- a/src/web/app/mobile/tags/page/entrance/signin.tag
+++ b/src/web/app/mobile/tags/page/entrance/signin.tag
@@ -1,5 +1,6 @@
<mk-entrance-signin>
<mk-signin/>
+ <a href={ _API_URL_ + '/signin/twitter' }>Twitterでサインイン</a>
<div class="divider"><span>or</span></div>
<button class="signup" onclick={ parent.signup }>%i18n:mobile.tags.mk-entrance-signin.signup%</button><a class="introduction" onclick={ parent.introduction }>%i18n:mobile.tags.mk-entrance-signin.about%</a>
<style>
diff --git a/src/web/app/mobile/tags/page/home.tag b/src/web/app/mobile/tags/page/home.tag
index 3b0255b293..1b2a4b1e13 100644
--- a/src/web/app/mobile/tags/page/home.tag
+++ b/src/web/app/mobile/tags/page/home.tag
@@ -13,7 +13,10 @@
import openPostForm from '../../scripts/open-post-form';
this.mixin('i');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.unreadCount = 0;
@@ -28,7 +31,7 @@
Progress.start();
- this.stream.on('post', this.onStreamPost);
+ this.connection.on('post', this.onStreamPost);
document.addEventListener('visibilitychange', this.onVisibilitychange, false);
this.refs.ui.refs.home.on('loaded', () => {
@@ -37,7 +40,8 @@
});
this.on('unmount', () => {
- this.stream.off('post', this.onStreamPost);
+ this.connection.off('post', this.onStreamPost);
+ this.stream.dispose(this.connectionId);
document.removeEventListener('visibilitychange', this.onVisibilitychange);
});
diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
index b6501142ee..95b3f757d7 100644
--- a/src/web/app/mobile/tags/page/settings.tag
+++ b/src/web/app/mobile/tags/page/settings.tag
@@ -29,7 +29,7 @@
<ul>
<li><a onclick={ signout }><i class="fa fa-power-off"></i>%i18n:mobile.tags.mk-settings-page.signout%</a></li>
</ul>
- <p><small>ver { version } (葵 aoi)</small></p>
+ <p><small>ver { _VERSION_ } (葵 aoi)</small></p>
<style>
:scope
display block
@@ -97,7 +97,5 @@
this.signout = signout;
this.mixin('i');
-
- this.version = VERSION;
</script>
</mk-settings>
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 8a32101036..28071a5cac 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -285,7 +285,7 @@
this.refs.text.innerHTML = compile(tokens);
- this.refs.text.children.forEach(e => {
+ Array.from(this.refs.text.children).forEach(e => {
if (e.tagName == 'MK-URL') riot.mount(e);
});
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index d7d382c9e2..2912bfdfa2 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -207,7 +207,7 @@
};
this.onpaste = e => {
- e.clipboardData.items.forEach(item => {
+ Array.from(e.clipboardData.items).forEach(item => {
if (item.kind == 'file') {
this.upload(item.getAsFile());
}
@@ -228,7 +228,7 @@
};
this.changeFile = () => {
- this.refs.file.files.forEach(this.upload);
+ Array.from(this.refs.file.files).forEach(this.upload);
};
this.upload = file => {
diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag
index e32e245185..c14233d3b7 100644
--- a/src/web/app/mobile/tags/sub-post-content.tag
+++ b/src/web/app/mobile/tags/sub-post-content.tag
@@ -37,7 +37,7 @@
const tokens = this.post.ast;
this.refs.text.innerHTML = compile(tokens, false);
- this.refs.text.children.forEach(e => {
+ Array.from(this.refs.text.children).forEach(e => {
if (e.tagName == 'MK-URL') riot.mount(e);
});
}
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index f9ec2cca60..074422a20e 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -164,7 +164,7 @@
</header>
<div class="body">
<div class="text" ref="text">
- <p class="channel" if={ p.channel != null }><a href={ CONFIG.chUrl + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
+ <p class="channel" if={ p.channel != null }><a href={ _CH_URL_ + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
<a class="reply" if={ p.reply }>
<i class="fa fa-reply"></i>
</a>
@@ -473,7 +473,10 @@
this.mixin('i');
this.mixin('api');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.set = post => {
this.post = post;
@@ -508,21 +511,21 @@
this.capture = withHandler => {
if (this.SIGNIN) {
- this.stream.send({
+ this.connection.send({
type: 'capture',
id: this.post.id
});
- if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated);
+ if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
}
};
this.decapture = withHandler => {
if (this.SIGNIN) {
- this.stream.send({
+ this.connection.send({
type: 'decapture',
id: this.post.id
});
- if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated);
+ if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
}
};
@@ -530,7 +533,7 @@
this.capture(true);
if (this.SIGNIN) {
- this.stream.on('_connected_', this.onStreamConnected);
+ this.connection.on('_connected_', this.onStreamConnected);
}
if (this.p.text) {
@@ -538,7 +541,7 @@
this.refs.text.innerHTML = this.refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
- this.refs.text.children.forEach(e => {
+ Array.from(this.refs.text.children).forEach(e => {
if (e.tagName == 'MK-URL') riot.mount(e);
});
@@ -555,7 +558,8 @@
this.on('unmount', () => {
this.decapture(true);
- this.stream.off('_connected_', this.onStreamConnected);
+ this.connection.off('_connected_', this.onStreamConnected);
+ this.stream.dispose(this.connectionId);
});
this.reply = () => {
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index b2d96f6b8b..bad6bf73fe 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -12,16 +12,20 @@
</style>
<script>
this.mixin('i');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.isDrawerOpening = false;
this.on('mount', () => {
- this.stream.on('notification', this.onStreamNotification);
+ this.connection.on('notification', this.onStreamNotification);
});
this.on('unmount', () => {
- this.stream.off('notification', this.onStreamNotification);
+ this.connection.off('notification', this.onStreamNotification);
+ this.stream.dispose(this.connectionId);
});
this.toggleDrawer = () => {
@@ -31,7 +35,7 @@
this.onStreamNotification = notification => {
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
- this.stream.send({
+ this.connection.send({
type: 'read_notification',
id: notification.id
});
@@ -145,15 +149,18 @@
import ui from '../scripts/ui-event';
this.mixin('api');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.func = null;
this.funcIcon = null;
this.on('mount', () => {
- this.stream.on('read_all_notifications', this.onReadAllNotifications);
- this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
+ this.connection.on('read_all_notifications', this.onReadAllNotifications);
+ this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+ this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
// Fetch count of unread notifications
this.api('notifications/get_unread_count').then(res => {
@@ -175,9 +182,10 @@
});
this.on('unmount', () => {
- this.stream.off('read_all_notifications', this.onReadAllNotifications);
- this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
+ this.connection.off('read_all_notifications', this.onReadAllNotifications);
+ this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+ this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
+ this.stream.dispose(this.connectionId);
ui.off('title', this.setTitle);
ui.off('func', this.setFunc);
@@ -231,7 +239,7 @@
<li><a href="/i/messaging"><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-ui-nav.messaging%<i class="i fa fa-circle" if={ hasUnreadMessagingMessages }></i><i class="fa fa-angle-right"></i></a></li>
</ul>
<ul>
- <li><a href={ CONFIG.chUrl } target="_blank"><i class="fa fa-television"></i>%i18n:mobile.tags.mk-ui-nav.ch%<i class="fa fa-angle-right"></i></a></li>
+ <li><a href={ _CH_URL_ } target="_blank"><i class="fa fa-television"></i>%i18n:mobile.tags.mk-ui-nav.ch%<i class="fa fa-angle-right"></i></a></li>
<li><a href="/i/drive"><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="fa fa-angle-right"></i></a></li>
</ul>
<ul>
@@ -241,7 +249,7 @@
<li><a href="/i/settings"><i class="fa fa-cog"></i>%i18n:mobile.tags.mk-ui-nav.settings%<i class="fa fa-angle-right"></i></a></li>
</ul>
</div>
- <a href={ CONFIG.aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
+ <a href={ _ABOUT_URL_ }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
</div>
<style>
:scope
@@ -348,12 +356,15 @@
this.mixin('i');
this.mixin('page');
this.mixin('api');
+
this.mixin('stream');
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
this.on('mount', () => {
- this.stream.on('read_all_notifications', this.onReadAllNotifications);
- this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
+ this.connection.on('read_all_notifications', this.onReadAllNotifications);
+ this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+ this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
// Fetch count of unread notifications
this.api('notifications/get_unread_count').then(res => {
@@ -375,9 +386,10 @@
});
this.on('unmount', () => {
- this.stream.off('read_all_notifications', this.onReadAllNotifications);
- this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
- this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
+ this.connection.off('read_all_notifications', this.onReadAllNotifications);
+ this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+ this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
+ this.stream.dispose(this.connectionId);
});
this.onReadAllNotifications = () => {
diff --git a/src/web/app/safe.js b/src/web/app/safe.js
index 77293be81d..2fd5361725 100644
--- a/src/web/app/safe.js
+++ b/src/web/app/safe.js
@@ -1,6 +1,5 @@
/**
- * 古いブラウザの検知を行う
- * ブートローダーとは隔離されているため互いに影響を及ぼすことはない
+ * ブラウザの検証
*/
// Detect an old browser
@@ -9,6 +8,24 @@ if (!('fetch' in window)) {
'お使いのブラウザが古いためMisskeyを動作させることができません。' +
'バージョンを最新のものに更新するか、別のブラウザをお試しください。' +
'\n\n' +
- 'Your browser seems outdated.' +
+ 'Your browser seems outdated. ' +
'To run Misskey, please update your browser to latest version or try other browsers.');
}
+
+// Detect Edge
+if (navigator.userAgent.toLowerCase().indexOf('edge') != -1) {
+ alert(
+ '現在、お使いのブラウザ(Microsoft Edge)ではMisskeyは正しく動作しません。' +
+ 'サポートしているブラウザ: Google Chrome, Mozilla Firefox, Apple Safari など' +
+ '\n\n' +
+ 'Currently, Misskey cannot run correctly on your browser (Microsoft Edge). ' +
+ 'Supported browsers: Google Chrome, Mozilla Firefox, Apple Safari, etc');
+}
+
+// Check whether cookie enabled
+if (!navigator.cookieEnabled) {
+ alert(
+ 'Misskeyを利用するにはCookieを有効にしてください。' +
+ '\n\n' +
+ 'To use Misskey, please enable Cookie.');
+}
diff --git a/src/web/app/stats/script.js b/src/web/app/stats/script.ts
index 75063501bb..3bbd80c339 100644
--- a/src/web/app/stats/script.js
+++ b/src/web/app/stats/script.ts
@@ -14,7 +14,7 @@ document.title = 'Misskey Statistics';
/**
* init
*/
-init(me => {
+init(() => {
mount(document.createElement('mk-index'));
});
diff --git a/src/web/app/stats/tags/index.tag b/src/web/app/stats/tags/index.tag
index 134fad3c0c..4b5415b2fd 100644
--- a/src/web/app/stats/tags/index.tag
+++ b/src/web/app/stats/tags/index.tag
@@ -4,7 +4,7 @@
<mk-users stats={ stats }/>
<mk-posts stats={ stats }/>
</main>
- <footer><a href={ CONFIG.url }>{ CONFIG.host }</a></footer>
+ <footer><a href={ _URL_ }>{ _HOST_ }</a></footer>
<style>
:scope
display block
diff --git a/src/web/app/stats/tags/index.js b/src/web/app/stats/tags/index.ts
index f41151949f..f41151949f 100644
--- a/src/web/app/stats/tags/index.js
+++ b/src/web/app/stats/tags/index.ts
diff --git a/src/web/app/status/script.js b/src/web/app/status/script.ts
index 06d4d9a7a4..84483acab7 100644
--- a/src/web/app/status/script.js
+++ b/src/web/app/status/script.ts
@@ -14,7 +14,7 @@ document.title = 'Misskey System Status';
/**
* init
*/
-init(me => {
+init(() => {
mount(document.createElement('mk-index'));
});
diff --git a/src/web/app/status/tags/index.tag b/src/web/app/status/tags/index.tag
index 6fb6041c3c..cb379f66bc 100644
--- a/src/web/app/status/tags/index.tag
+++ b/src/web/app/status/tags/index.tag
@@ -5,7 +5,7 @@
<mk-cpu-usage connection={ connection }/>
<mk-mem-usage connection={ connection }/>
</main>
- <footer><a href={ CONFIG.url }>{ CONFIG.host }</a></footer>
+ <footer><a href={ _URL_ }>{ _HOST_ }</a></footer>
<style>
:scope
display block
@@ -51,7 +51,7 @@
color #546567
</style>
<script>
- import Connection from '../../common/scripts/server-stream';
+ import Connection from '../../common/scripts/streaming/server-stream';
this.mixin('api');
@@ -177,7 +177,7 @@
width 100%
</style>
<script>
- import uuid from '../../common/scripts/uuid';
+ import uuid from 'uuid';
this.viewBoxX = 100;
this.viewBoxY = 30;
diff --git a/src/web/app/status/tags/index.js b/src/web/app/status/tags/index.ts
index f41151949f..f41151949f 100644
--- a/src/web/app/status/tags/index.js
+++ b/src/web/app/status/tags/index.ts
diff --git a/src/web/app/sw.js b/src/web/app/sw.js
new file mode 100644
index 0000000000..a7c84d022a
--- /dev/null
+++ b/src/web/app/sw.js
@@ -0,0 +1,33 @@
+/**
+ * Service Worker
+ */
+
+import composeNotification from './common/scripts/compose-notification';
+
+// インストールされたとき
+self.addEventListener('install', () => {
+ console.info('installed');
+});
+
+// プッシュ通知を受け取ったとき
+self.addEventListener('push', ev => {
+ console.log('pushed');
+
+ // クライアント取得
+ ev.waitUntil(self.clients.matchAll({
+ includeUncontrolled: true
+ }).then(clients => {
+ // クライアントがあったらストリームに接続しているということなので通知しない
+ if (clients.length != 0) return;
+
+ const { type, body } = ev.data.json();
+
+ console.log(type, body);
+
+ const n = composeNotification(type, body);
+ return self.registration.showNotification(n.title, {
+ body: n.body,
+ icon: n.icon,
+ });
+ }));
+});
diff --git a/src/web/assets/manifest.json b/src/web/assets/manifest.json
index 0967ef424b..783d0539ac 100644
--- a/src/web/assets/manifest.json
+++ b/src/web/assets/manifest.json
@@ -1 +1,7 @@
-{}
+{
+ "short_name": "Misskey",
+ "name": "Misskey",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#313a42"
+}
diff --git a/src/web/server.ts b/src/web/server.ts
index dde4eca5ec..d8a4713290 100644
--- a/src/web/server.ts
+++ b/src/web/server.ts
@@ -11,8 +11,6 @@ import * as bodyParser from 'body-parser';
import * as favicon from 'serve-favicon';
import * as compression from 'compression';
-import config from '../conf';
-
/**
* Init app
*/
@@ -37,27 +35,27 @@ app.use((req, res, next) => {
* Static assets
*/
app.use(favicon(`${__dirname}/assets/favicon.ico`));
-app.get('/manifest.json', (req, res) => res.sendFile(`${__dirname}/assets/manifest.json`));
app.get('/apple-touch-icon.png', (req, res) => res.sendFile(`${__dirname}/assets/apple-touch-icon.png`));
app.use('/assets', express.static(`${__dirname}/assets`, {
maxAge: ms('7 days')
}));
/**
- * Common API
+ * ServiceWroker
*/
-app.get(/\/api:url/, require('./service/url-preview'));
+app.get(/^\/sw\.(.+?)\.js$/, (req, res) =>
+ res.sendFile(`${__dirname}/assets/sw.${req.params[0]}.js`));
/**
- * Serve config
+ * Manifest
*/
-app.get('/config.json', (req, res) => {
- res.send({
- recaptcha: {
- siteKey: config.recaptcha.siteKey
- }
- });
-});
+app.get('/manifest.json', (req, res) =>
+ res.sendFile(`${__dirname}/assets/manifest.json`));
+
+/**
+ * Common API
+ */
+app.get(/\/api:url/, require('./service/url-preview'));
/**
* Routing