mirror of
https://github.com/paricafe/misskey.git
synced 2024-11-24 22:16:43 -06:00
commit
d178584828
463 changed files with 25569 additions and 25411 deletions
19
.eslintrc
Normal file
19
.eslintrc
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"parserOptions": {
|
||||
"parser": "typescript-eslint-parser"
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"vue/require-v-for-key": false,
|
||||
"vue/max-attributes-per-line": false,
|
||||
"vue/html-indent": false,
|
||||
"vue/html-self-closing": false,
|
||||
"vue/no-unused-vars": false,
|
||||
"no-console": 0,
|
||||
"no-unused-vars": 0,
|
||||
"no-empty": 0
|
||||
}
|
||||
}
|
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,5 +1,3 @@
|
|||
*.svg -diff -text
|
||||
*.psd -diff -text
|
||||
*.ai -diff -text
|
||||
|
||||
*.tag linguist-language=HTML
|
||||
|
|
|
@ -56,7 +56,7 @@ gulp.task('build:js', () =>
|
|||
);
|
||||
|
||||
gulp.task('build:ts', () => {
|
||||
const tsProject = ts.createProject('./src/tsconfig.json');
|
||||
const tsProject = ts.createProject('./tsconfig.json');
|
||||
|
||||
return tsProject
|
||||
.src()
|
||||
|
|
|
@ -11,7 +11,7 @@ const loadLang = lang => yaml.safeLoad(
|
|||
const native = loadLang('ja');
|
||||
|
||||
const langs = {
|
||||
'en': loadLang('en'),
|
||||
//'en': loadLang('en'),
|
||||
'ja': native
|
||||
};
|
||||
|
||||
|
|
19
package.json
19
package.json
|
@ -81,9 +81,9 @@
|
|||
"accesses": "2.5.0",
|
||||
"animejs": "2.2.0",
|
||||
"autwh": "0.0.1",
|
||||
"awesome-typescript-loader": "3.4.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.18.2",
|
||||
"cache-loader": "^1.2.0",
|
||||
"cafy": "3.2.1",
|
||||
"chai": "4.1.2",
|
||||
"chai-http": "3.0.0",
|
||||
|
@ -99,6 +99,8 @@
|
|||
"diskusage": "0.2.4",
|
||||
"elasticsearch": "14.1.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eslint": "^4.18.0",
|
||||
"eslint-plugin-vue": "^4.2.2",
|
||||
"eventemitter3": "3.0.0",
|
||||
"exif-js": "2.3.0",
|
||||
"express": "4.16.2",
|
||||
|
@ -118,12 +120,15 @@
|
|||
"gulp-typescript": "3.2.4",
|
||||
"gulp-uglify": "3.0.0",
|
||||
"gulp-util": "3.0.8",
|
||||
"hard-source-webpack-plugin": "0.6.0-alpha.8",
|
||||
"highlight.js": "9.12.0",
|
||||
"html-minifier": "^3.5.9",
|
||||
"inquirer": "5.0.1",
|
||||
"is-root": "1.0.0",
|
||||
"is-url": "1.2.2",
|
||||
"js-yaml": "3.10.0",
|
||||
"license-checker": "16.0.0",
|
||||
"loader-utils": "^1.1.0",
|
||||
"mecab-async": "0.1.2",
|
||||
"mkdirp": "0.5.1",
|
||||
"mocha": "5.0.0",
|
||||
|
@ -145,6 +150,7 @@
|
|||
"recaptcha-promise": "0.1.3",
|
||||
"reconnecting-websocket": "3.2.2",
|
||||
"redis": "2.8.0",
|
||||
"replace-string-loader": "0.0.7",
|
||||
"request": "2.83.0",
|
||||
"rimraf": "2.6.2",
|
||||
"riot": "3.8.1",
|
||||
|
@ -155,6 +161,7 @@
|
|||
"serve-favicon": "2.4.5",
|
||||
"sortablejs": "1.7.0",
|
||||
"speakeasy": "2.0.0",
|
||||
"string-replace-loader": "^1.3.0",
|
||||
"string-replace-webpack-plugin": "0.1.3",
|
||||
"style-loader": "0.20.1",
|
||||
"stylus": "0.54.5",
|
||||
|
@ -165,15 +172,25 @@
|
|||
"tcp-port-used": "0.1.2",
|
||||
"textarea-caret": "3.0.2",
|
||||
"tmp": "0.0.33",
|
||||
"ts-loader": "^3.5.0",
|
||||
"ts-node": "4.1.0",
|
||||
"tslint": "5.9.1",
|
||||
"typescript": "2.7.1",
|
||||
"typescript-eslint-parser": "^13.0.0",
|
||||
"uglify-es": "3.3.9",
|
||||
"uglifyjs-webpack-plugin": "1.1.8",
|
||||
"uuid": "3.2.1",
|
||||
"vhost": "3.0.2",
|
||||
"vue": "^2.5.13",
|
||||
"vue-cropperjs": "^2.2.0",
|
||||
"vue-js-modal": "^1.3.9",
|
||||
"vue-loader": "^14.1.1",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.5.13",
|
||||
"vuedraggable": "^2.16.0",
|
||||
"web-push": "3.2.5",
|
||||
"webpack": "3.10.0",
|
||||
"webpack-replace-loader": "^1.3.0",
|
||||
"websocket": "1.0.25",
|
||||
"xev": "2.0.0"
|
||||
}
|
||||
|
|
|
@ -305,7 +305,7 @@ class TlContext extends Context {
|
|||
private async getTl() {
|
||||
const tl = await require('../endpoints/posts/timeline')({
|
||||
limit: 5,
|
||||
max_id: this.next ? this.next : undefined
|
||||
until_id: this.next ? this.next : undefined
|
||||
}, this.bot.user);
|
||||
|
||||
if (tl.length > 0) {
|
||||
|
@ -357,7 +357,7 @@ class NotificationsContext extends Context {
|
|||
private async getNotifications() {
|
||||
const notifications = await require('../endpoints/i/notifications')({
|
||||
limit: 5,
|
||||
max_id: this.next ? this.next : undefined
|
||||
until_id: this.next ? this.next : undefined
|
||||
}, this.bot.user);
|
||||
|
||||
if (notifications.length > 0) {
|
||||
|
|
|
@ -194,6 +194,11 @@ const endpoints: Endpoint[] = [
|
|||
withCredential: true,
|
||||
secure: true
|
||||
},
|
||||
{
|
||||
name: 'i/update_client_setting',
|
||||
withCredential: true,
|
||||
secure: true
|
||||
},
|
||||
{
|
||||
name: 'i/pin',
|
||||
kind: 'account-write'
|
||||
|
|
|
@ -46,19 +46,13 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
|
|||
if (bannerIdErr) return rej('invalid banner_id param');
|
||||
if (bannerId) user.banner_id = bannerId;
|
||||
|
||||
// Get 'show_donation' parameter
|
||||
const [showDonation, showDonationErr] = $(params.show_donation).optional.boolean().$;
|
||||
if (showDonationErr) return rej('invalid show_donation param');
|
||||
if (showDonation) user.client_settings.show_donation = showDonation;
|
||||
|
||||
await User.update(user._id, {
|
||||
$set: {
|
||||
name: user.name,
|
||||
description: user.description,
|
||||
avatar_id: user.avatar_id,
|
||||
banner_id: user.banner_id,
|
||||
profile: user.profile,
|
||||
'client_settings.show_donation': user.client_settings.show_donation
|
||||
profile: user.profile
|
||||
}
|
||||
});
|
||||
|
||||
|
|
43
src/api/endpoints/i/update_client_setting.ts
Normal file
43
src/api/endpoints/i/update_client_setting.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Module dependencies
|
||||
*/
|
||||
import $ from 'cafy';
|
||||
import User, { pack } from '../../models/user';
|
||||
import event from '../../event';
|
||||
|
||||
/**
|
||||
* Update myself
|
||||
*
|
||||
* @param {any} params
|
||||
* @param {any} user
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
module.exports = async (params, user) => new Promise(async (res, rej) => {
|
||||
// Get 'name' parameter
|
||||
const [name, nameErr] = $(params.name).string().$;
|
||||
if (nameErr) return rej('invalid name param');
|
||||
|
||||
// Get 'value' parameter
|
||||
const [value, valueErr] = $(params.value).nullable.any().$;
|
||||
if (valueErr) return rej('invalid value param');
|
||||
|
||||
const x = {};
|
||||
x[`client_settings.${name}`] = value;
|
||||
|
||||
await User.update(user._id, {
|
||||
$set: x
|
||||
});
|
||||
|
||||
// Serialize
|
||||
user.client_settings[name] = value;
|
||||
const iObj = await pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: true
|
||||
});
|
||||
|
||||
// Send response
|
||||
res(iObj);
|
||||
|
||||
// Publish i updated event
|
||||
event(user._id, 'i_updated', iObj);
|
||||
});
|
|
@ -15,7 +15,7 @@ const home = {
|
|||
'profile',
|
||||
'calendar',
|
||||
'activity',
|
||||
'rss-reader',
|
||||
'rss',
|
||||
'trends',
|
||||
'photo-stream',
|
||||
'version'
|
||||
|
@ -23,8 +23,8 @@ const home = {
|
|||
right: [
|
||||
'broadcast',
|
||||
'notifications',
|
||||
'user-recommendation',
|
||||
'recommended-polls',
|
||||
'users',
|
||||
'polls',
|
||||
'server',
|
||||
'donation',
|
||||
'nav',
|
||||
|
|
|
@ -17,7 +17,14 @@ export default class Replacer {
|
|||
}
|
||||
|
||||
private get(key: string) {
|
||||
let text = locale[this.lang];
|
||||
const texts = locale[this.lang];
|
||||
|
||||
if (texts == null) {
|
||||
console.warn(`lang '${this.lang}' is not supported`);
|
||||
return key; // Fallback
|
||||
}
|
||||
|
||||
let text = texts;
|
||||
|
||||
// Check the key existance
|
||||
const error = key.split('.').some(k => {
|
||||
|
|
3
src/web/app/app.vue
Normal file
3
src/web/app/app.vue
Normal file
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<router-view id="app"></router-view>
|
||||
</template>
|
|
@ -1,130 +0,0 @@
|
|||
<mk-form>
|
||||
<header>
|
||||
<h1><i>{ app.name }</i>があなたの<b>アカウント</b>に<b>アクセス</b>することを<b>許可</b>しますか?</h1><img src={ app.icon_url + '?thumbnail&size=64' }/>
|
||||
</header>
|
||||
<div class="app">
|
||||
<section>
|
||||
<h2>{ app.name }</h2>
|
||||
<p class="nid">{ app.name_id }</p>
|
||||
<p class="description">{ app.description }</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>このアプリは次の権限を要求しています:</h2>
|
||||
<ul>
|
||||
<virtual each={ p in app.permission }>
|
||||
<li if={ p == 'account-read' }>アカウントの情報を見る。</li>
|
||||
<li if={ p == 'account-write' }>アカウントの情報を操作する。</li>
|
||||
<li if={ p == 'post-write' }>投稿する。</li>
|
||||
<li if={ p == 'like-write' }>いいねしたりいいね解除する。</li>
|
||||
<li if={ p == 'following-write' }>フォローしたりフォロー解除する。</li>
|
||||
<li if={ p == 'drive-read' }>ドライブを見る。</li>
|
||||
<li if={ p == 'drive-write' }>ドライブを操作する。</li>
|
||||
<li if={ p == 'notification-read' }>通知を見る。</li>
|
||||
<li if={ p == 'notification-write' }>通知を操作する。</li>
|
||||
</virtual>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div class="action">
|
||||
<button onclick={ cancel }>キャンセル</button>
|
||||
<button onclick={ accept }>アクセスを許可</button>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> header
|
||||
> h1
|
||||
margin 0
|
||||
padding 32px 32px 20px 32px
|
||||
font-size 24px
|
||||
font-weight normal
|
||||
color #777
|
||||
|
||||
i
|
||||
color #77aeca
|
||||
|
||||
&:before
|
||||
content '「'
|
||||
|
||||
&:after
|
||||
content '」'
|
||||
|
||||
b
|
||||
color #666
|
||||
|
||||
> img
|
||||
display block
|
||||
z-index 1
|
||||
width 84px
|
||||
height 84px
|
||||
margin 0 auto -38px auto
|
||||
border solid 5px #fff
|
||||
border-radius 100%
|
||||
box-shadow 0 2px 2px rgba(0, 0, 0, 0.1)
|
||||
|
||||
> .app
|
||||
padding 44px 16px 0 16px
|
||||
color #555
|
||||
background #eee
|
||||
box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset
|
||||
|
||||
&:after
|
||||
content ''
|
||||
display block
|
||||
clear both
|
||||
|
||||
> section
|
||||
float left
|
||||
width 50%
|
||||
padding 8px
|
||||
text-align left
|
||||
|
||||
> h2
|
||||
margin 0
|
||||
font-size 16px
|
||||
color #777
|
||||
|
||||
> .action
|
||||
padding 16px
|
||||
|
||||
> button
|
||||
margin 0 8px
|
||||
|
||||
@media (max-width 600px)
|
||||
> header
|
||||
> img
|
||||
box-shadow none
|
||||
|
||||
> .app
|
||||
box-shadow none
|
||||
|
||||
@media (max-width 500px)
|
||||
> header
|
||||
> h1
|
||||
font-size 16px
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('api');
|
||||
|
||||
this.session = this.opts.session;
|
||||
this.app = this.session.app;
|
||||
|
||||
this.cancel = () => {
|
||||
this.api('auth/deny', {
|
||||
token: this.session.token
|
||||
}).then(() => {
|
||||
this.trigger('denied');
|
||||
});
|
||||
};
|
||||
|
||||
this.accept = () => {
|
||||
this.api('auth/accept', {
|
||||
token: this.session.token
|
||||
}).then(() => {
|
||||
this.trigger('accepted');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-form>
|
|
@ -1,143 +0,0 @@
|
|||
<mk-index>
|
||||
<main if={ SIGNIN }>
|
||||
<p class="fetching" if={ fetching }>読み込み中<mk-ellipsis/></p>
|
||||
<mk-form ref="form" if={ state == 'waiting' } session={ session }/>
|
||||
<div class="denied" if={ state == 'denied' }>
|
||||
<h1>アプリケーションの連携をキャンセルしました。</h1>
|
||||
<p>このアプリがあなたのアカウントにアクセスすることはありません。</p>
|
||||
</div>
|
||||
<div class="accepted" if={ state == 'accepted' }>
|
||||
<h1>{ session.app.is_authorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました'}</h1>
|
||||
<p if={ session.app.callback_url }>アプリケーションに戻っています<mk-ellipsis/></p>
|
||||
<p if={ !session.app.callback_url }>アプリケーションに戻って、やっていってください。</p>
|
||||
</div>
|
||||
<div class="error" if={ state == 'fetch-session-error' }>
|
||||
<p>セッションが存在しません。</p>
|
||||
</div>
|
||||
</main>
|
||||
<main class="signin" if={ !SIGNIN }>
|
||||
<h1>サインインしてください</h1>
|
||||
<mk-signin/>
|
||||
</main>
|
||||
<footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> main
|
||||
width 100%
|
||||
max-width 500px
|
||||
margin 0 auto
|
||||
text-align center
|
||||
background #fff
|
||||
box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> .fetching
|
||||
margin 0
|
||||
padding 32px
|
||||
color #555
|
||||
|
||||
> div
|
||||
padding 64px
|
||||
|
||||
> h1
|
||||
margin 0 0 8px 0
|
||||
padding 0
|
||||
font-size 20px
|
||||
font-weight normal
|
||||
|
||||
> p
|
||||
margin 0
|
||||
color #555
|
||||
|
||||
&.denied > h1
|
||||
color #e65050
|
||||
|
||||
&.accepted > h1
|
||||
color #54af7c
|
||||
|
||||
&.signin
|
||||
padding 32px 32px 16px 32px
|
||||
|
||||
> h1
|
||||
margin 0 0 22px 0
|
||||
padding 0
|
||||
font-size 20px
|
||||
font-weight normal
|
||||
color #555
|
||||
|
||||
@media (max-width 600px)
|
||||
max-width none
|
||||
box-shadow none
|
||||
|
||||
@media (max-width 500px)
|
||||
> div
|
||||
> h1
|
||||
font-size 16px
|
||||
|
||||
> footer
|
||||
> img
|
||||
display block
|
||||
width 64px
|
||||
height 64px
|
||||
margin 0 auto
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('i');
|
||||
this.mixin('api');
|
||||
|
||||
this.state = null;
|
||||
this.fetching = true;
|
||||
|
||||
this.token = window.location.href.split('/').pop();
|
||||
|
||||
this.on('mount', () => {
|
||||
if (!this.SIGNIN) return;
|
||||
|
||||
// Fetch session
|
||||
this.api('auth/session/show', {
|
||||
token: this.token
|
||||
}).then(session => {
|
||||
this.session = session;
|
||||
this.fetching = false;
|
||||
|
||||
// 既に連携していた場合
|
||||
if (this.session.app.is_authorized) {
|
||||
this.api('auth/accept', {
|
||||
token: this.session.token
|
||||
}).then(() => {
|
||||
this.accepted();
|
||||
});
|
||||
} else {
|
||||
this.update({
|
||||
state: 'waiting'
|
||||
});
|
||||
|
||||
this.refs.form.on('denied', () => {
|
||||
this.update({
|
||||
state: 'denied'
|
||||
});
|
||||
});
|
||||
|
||||
this.refs.form.on('accepted', this.accepted);
|
||||
}
|
||||
}).catch(error => {
|
||||
this.update({
|
||||
fetching: false,
|
||||
state: 'fetch-session-error'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.accepted = () => {
|
||||
this.update({
|
||||
state: 'accepted'
|
||||
});
|
||||
|
||||
if (this.session.app.callback_url) {
|
||||
location.href = this.session.app.callback_url + '?token=' + this.session.token;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</mk-index>
|
|
@ -1,2 +0,0 @@
|
|||
require('./index.tag');
|
||||
require('./form.tag');
|
140
src/web/app/auth/views/form.vue
Normal file
140
src/web/app/auth/views/form.vue
Normal file
|
@ -0,0 +1,140 @@
|
|||
<template>
|
||||
<div class="form">
|
||||
<header>
|
||||
<h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか?</h1>
|
||||
<img :src="`${app.icon_url}?thumbnail&size=64`"/>
|
||||
</header>
|
||||
<div class="app">
|
||||
<section>
|
||||
<h2>{{ app.name }}</h2>
|
||||
<p class="nid">{{ app.name_id }}</p>
|
||||
<p class="description">{{ app.description }}</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>このアプリは次の権限を要求しています:</h2>
|
||||
<ul>
|
||||
<template v-for="p in app.permission">
|
||||
<li v-if="p == 'account-read'">アカウントの情報を見る。</li>
|
||||
<li v-if="p == 'account-write'">アカウントの情報を操作する。</li>
|
||||
<li v-if="p == 'post-write'">投稿する。</li>
|
||||
<li v-if="p == 'like-write'">いいねしたりいいね解除する。</li>
|
||||
<li v-if="p == 'following-write'">フォローしたりフォロー解除する。</li>
|
||||
<li v-if="p == 'drive-read'">ドライブを見る。</li>
|
||||
<li v-if="p == 'drive-write'">ドライブを操作する。</li>
|
||||
<li v-if="p == 'notification-read'">通知を見る。</li>
|
||||
<li v-if="p == 'notification-write'">通知を操作する。</li>
|
||||
</template>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div class="action">
|
||||
<button @click="cancel">キャンセル</button>
|
||||
<button @click="accept">アクセスを許可</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: ['session'],
|
||||
computed: {
|
||||
app(): any {
|
||||
return this.session.app;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
(this as any).api('auth/deny', {
|
||||
token: this.session.token
|
||||
}).then(() => {
|
||||
this.$emit('denied');
|
||||
});
|
||||
},
|
||||
|
||||
accept() {
|
||||
(this as any).api('auth/accept', {
|
||||
token: this.session.token
|
||||
}).then(() => {
|
||||
this.$emit('accepted');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.form
|
||||
|
||||
> header
|
||||
> h1
|
||||
margin 0
|
||||
padding 32px 32px 20px 32px
|
||||
font-size 24px
|
||||
font-weight normal
|
||||
color #777
|
||||
|
||||
i
|
||||
color #77aeca
|
||||
|
||||
&:before
|
||||
content '「'
|
||||
|
||||
&:after
|
||||
content '」'
|
||||
|
||||
b
|
||||
color #666
|
||||
|
||||
> img
|
||||
display block
|
||||
z-index 1
|
||||
width 84px
|
||||
height 84px
|
||||
margin 0 auto -38px auto
|
||||
border solid 5px #fff
|
||||
border-radius 100%
|
||||
box-shadow 0 2px 2px rgba(0, 0, 0, 0.1)
|
||||
|
||||
> .app
|
||||
padding 44px 16px 0 16px
|
||||
color #555
|
||||
background #eee
|
||||
box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset
|
||||
|
||||
&:after
|
||||
content ''
|
||||
display block
|
||||
clear both
|
||||
|
||||
> section
|
||||
float left
|
||||
width 50%
|
||||
padding 8px
|
||||
text-align left
|
||||
|
||||
> h2
|
||||
margin 0
|
||||
font-size 16px
|
||||
color #777
|
||||
|
||||
> .action
|
||||
padding 16px
|
||||
|
||||
> button
|
||||
margin 0 8px
|
||||
|
||||
@media (max-width 600px)
|
||||
> header
|
||||
> img
|
||||
box-shadow none
|
||||
|
||||
> .app
|
||||
box-shadow none
|
||||
|
||||
@media (max-width 500px)
|
||||
> header
|
||||
> h1
|
||||
font-size 16px
|
||||
|
||||
</style>
|
145
src/web/app/auth/views/index.vue
Normal file
145
src/web/app/auth/views/index.vue
Normal file
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<div class="index">
|
||||
<main v-if="os.isSignedIn">
|
||||
<p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p>
|
||||
<x-form
|
||||
ref="form"
|
||||
v-if="state == 'waiting'"
|
||||
:session="session"
|
||||
@denied="state = 'denied'"
|
||||
@accepted="accepted"
|
||||
/>
|
||||
<div class="denied" v-if="state == 'denied'">
|
||||
<h1>アプリケーションの連携をキャンセルしました。</h1>
|
||||
<p>このアプリがあなたのアカウントにアクセスすることはありません。</p>
|
||||
</div>
|
||||
<div class="accepted" v-if="state == 'accepted'">
|
||||
<h1>{{ session.app.is_authorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました'}}</h1>
|
||||
<p v-if="session.app.callback_url">アプリケーションに戻っています<mk-ellipsis/></p>
|
||||
<p v-if="!session.app.callback_url">アプリケーションに戻って、やっていってください。</p>
|
||||
</div>
|
||||
<div class="error" v-if="state == 'fetch-session-error'">
|
||||
<p>セッションが存在しません。</p>
|
||||
</div>
|
||||
</main>
|
||||
<main class="signin" v-if="!os.isSignedIn">
|
||||
<h1>サインインしてください</h1>
|
||||
<mk-signin/>
|
||||
</main>
|
||||
<footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XForm from './form.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XForm
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
state: null,
|
||||
session: null,
|
||||
fetching: true,
|
||||
token: window.location.href.split('/').pop()
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$root.$data.os.isSignedIn) return;
|
||||
|
||||
// Fetch session
|
||||
(this as any).api('auth/session/show', {
|
||||
token: this.token
|
||||
}).then(session => {
|
||||
this.session = session;
|
||||
this.fetching = false;
|
||||
|
||||
// 既に連携していた場合
|
||||
if (this.session.app.is_authorized) {
|
||||
this.$root.$data.os.api('auth/accept', {
|
||||
token: this.session.token
|
||||
}).then(() => {
|
||||
this.accepted();
|
||||
});
|
||||
} else {
|
||||
this.state = 'waiting';
|
||||
}
|
||||
}).catch(error => {
|
||||
this.state = 'fetch-session-error';
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
accepted() {
|
||||
this.state = 'accepted';
|
||||
if (this.session.app.callback_url) {
|
||||
location.href = this.session.app.callback_url + '?token=' + this.session.token;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.index
|
||||
|
||||
> main
|
||||
width 100%
|
||||
max-width 500px
|
||||
margin 0 auto
|
||||
text-align center
|
||||
background #fff
|
||||
box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> .fetching
|
||||
margin 0
|
||||
padding 32px
|
||||
color #555
|
||||
|
||||
> div
|
||||
padding 64px
|
||||
|
||||
> h1
|
||||
margin 0 0 8px 0
|
||||
padding 0
|
||||
font-size 20px
|
||||
font-weight normal
|
||||
|
||||
> p
|
||||
margin 0
|
||||
color #555
|
||||
|
||||
&.denied > h1
|
||||
color #e65050
|
||||
|
||||
&.accepted > h1
|
||||
color #54af7c
|
||||
|
||||
&.signin
|
||||
padding 32px 32px 16px 32px
|
||||
|
||||
> h1
|
||||
margin 0 0 22px 0
|
||||
padding 0
|
||||
font-size 20px
|
||||
font-weight normal
|
||||
color #555
|
||||
|
||||
@media (max-width 600px)
|
||||
max-width none
|
||||
box-shadow none
|
||||
|
||||
@media (max-width 500px)
|
||||
> div
|
||||
> h1
|
||||
font-size 16px
|
||||
|
||||
> footer
|
||||
> img
|
||||
display block
|
||||
width 64px
|
||||
height 64px
|
||||
margin 0 auto
|
||||
|
||||
</style>
|
|
@ -1,12 +1,12 @@
|
|||
<mk-channel>
|
||||
<mk-header/>
|
||||
<hr>
|
||||
<main if={ !fetching }>
|
||||
<main v-if="!fetching">
|
||||
<h1>{ channel.title }</h1>
|
||||
|
||||
<div if={ SIGNIN }>
|
||||
<p if={ channel.is_watching }>このチャンネルをウォッチしています <a onclick={ unwatch }>ウォッチ解除</a></p>
|
||||
<p if={ !channel.is_watching }><a onclick={ watch }>このチャンネルをウォッチする</a></p>
|
||||
<div v-if="$root.$data.os.isSignedIn">
|
||||
<p v-if="channel.is_watching">このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p>
|
||||
<p v-if="!channel.is_watching"><a @click="watch">このチャンネルをウォッチする</a></p>
|
||||
</div>
|
||||
|
||||
<div class="share">
|
||||
|
@ -15,17 +15,17 @@
|
|||
</div>
|
||||
|
||||
<div class="body">
|
||||
<p if={ postsFetching }>読み込み中<mk-ellipsis/></p>
|
||||
<div if={ !postsFetching }>
|
||||
<p if={ posts == null || posts.length == 0 }>まだ投稿がありません</p>
|
||||
<virtual if={ posts != null }>
|
||||
<p v-if="postsFetching">読み込み中<mk-ellipsis/></p>
|
||||
<div v-if="!postsFetching">
|
||||
<p v-if="posts == null || posts.length == 0">まだ投稿がありません</p>
|
||||
<template v-if="posts != null">
|
||||
<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
|
||||
</virtual>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/>
|
||||
<div if={ !SIGNIN }>
|
||||
<mk-channel-form v-if="$root.$data.os.isSignedIn" channel={ channel } ref="form"/>
|
||||
<div v-if="!$root.$data.os.isSignedIn">
|
||||
<p>参加するには<a href={ _URL_ }>ログインまたは新規登録</a>してください</p>
|
||||
</div>
|
||||
<hr>
|
||||
|
@ -33,7 +33,7 @@
|
|||
<small><a href={ _URL_ }>Misskey</a> ver { _VERSION_ } (葵 aoi)</small>
|
||||
</footer>
|
||||
</main>
|
||||
<style>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
|
||||
|
@ -53,7 +53,7 @@
|
|||
max-width 500px
|
||||
|
||||
</style>
|
||||
<script>
|
||||
<script lang="typescript">
|
||||
import Progress from '../../common/scripts/loading';
|
||||
import ChannelStream from '../../common/scripts/streaming/channel-stream';
|
||||
|
||||
|
@ -76,7 +76,7 @@
|
|||
let fetched = false;
|
||||
|
||||
// チャンネル概要読み込み
|
||||
this.api('channels/show', {
|
||||
this.$root.$data.os.api('channels/show', {
|
||||
channel_id: this.id
|
||||
}).then(channel => {
|
||||
if (fetched) {
|
||||
|
@ -95,7 +95,7 @@
|
|||
});
|
||||
|
||||
// 投稿読み込み
|
||||
this.api('channels/posts', {
|
||||
this.$root.$data.os.api('channels/posts', {
|
||||
channel_id: this.id
|
||||
}).then(posts => {
|
||||
if (fetched) {
|
||||
|
@ -125,7 +125,7 @@
|
|||
this.posts.unshift(post);
|
||||
this.update();
|
||||
|
||||
if (document.hidden && this.SIGNIN && post.user_id !== this.I.id) {
|
||||
if (document.hidden && this.$root.$data.os.isSignedIn && post.user_id !== this.$root.$data.os.i.id) {
|
||||
this.unreadCount++;
|
||||
document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`;
|
||||
}
|
||||
|
@ -139,7 +139,7 @@
|
|||
};
|
||||
|
||||
this.watch = () => {
|
||||
this.api('channels/watch', {
|
||||
this.$root.$data.os.api('channels/watch', {
|
||||
channel_id: this.id
|
||||
}).then(() => {
|
||||
this.channel.is_watching = true;
|
||||
|
@ -150,7 +150,7 @@
|
|||
};
|
||||
|
||||
this.unwatch = () => {
|
||||
this.api('channels/unwatch', {
|
||||
this.$root.$data.os.api('channels/unwatch', {
|
||||
channel_id: this.id
|
||||
}).then(() => {
|
||||
this.channel.is_watching = false;
|
||||
|
@ -164,24 +164,24 @@
|
|||
|
||||
<mk-channel-post>
|
||||
<header>
|
||||
<a class="index" onclick={ reply }>{ post.index }:</a>
|
||||
<a class="index" @click="reply">{ post.index }:</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>
|
||||
</header>
|
||||
<div>
|
||||
<a if={ post.reply }>>>{ post.reply.index }</a>
|
||||
<a v-if="post.reply">>>{ post.reply.index }</a>
|
||||
{ post.text }
|
||||
<div class="media" if={ post.media }>
|
||||
<virtual each={ file in post.media }>
|
||||
<div class="media" v-if="post.media">
|
||||
<template each={ file in post.media }>
|
||||
<a href={ file.url } target="_blank">
|
||||
<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
|
||||
</a>
|
||||
</virtual>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
margin 0
|
||||
|
@ -228,7 +228,7 @@
|
|||
vertical-align bottom
|
||||
|
||||
</style>
|
||||
<script>
|
||||
<script lang="typescript">
|
||||
this.post = this.opts.post;
|
||||
this.form = this.opts.form;
|
||||
|
||||
|
@ -241,21 +241,21 @@
|
|||
</mk-channel-post>
|
||||
|
||||
<mk-channel-form>
|
||||
<p if={ reply }><b>>>{ reply.index }</b> ({ reply.user.name }): <a onclick={ clearReply }>[x]</a></p>
|
||||
<p v-if="reply"><b>>>{ reply.index }</b> ({ reply.user.name }): <a @click="clearReply">[x]</a></p>
|
||||
<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder="%i18n:ch.tags.mk-channel-form.textarea%"></textarea>
|
||||
<div class="actions">
|
||||
<button onclick={ selectFile }>%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button>
|
||||
<button onclick={ drive }>%fa:cloud%%i18n:ch.tags.mk-channel-form.drive%</button>
|
||||
<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }>
|
||||
<virtual if={ !wait }>%fa:paper-plane%</virtual>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis if={ wait }/>
|
||||
<button @click="selectFile">%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button>
|
||||
<button @click="drive">%fa:cloud%%i18n:ch.tags.mk-channel-form.drive%</button>
|
||||
<button :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0) } @click="post">
|
||||
<template v-if="!wait">%fa:paper-plane%</template>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis v-if="wait"/>
|
||||
</button>
|
||||
</div>
|
||||
<mk-uploader ref="uploader"/>
|
||||
<ol if={ files }>
|
||||
<ol v-if="files">
|
||||
<li each={ files }>{ name }</li>
|
||||
</ol>
|
||||
<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
|
||||
<style>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
|
||||
|
@ -282,14 +282,14 @@
|
|||
display none
|
||||
|
||||
</style>
|
||||
<script>
|
||||
<script lang="typescript">
|
||||
this.mixin('api');
|
||||
|
||||
this.channel = this.opts.channel;
|
||||
this.files = null;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.refs.uploader.on('uploaded', file => {
|
||||
this.$refs.uploader.on('uploaded', file => {
|
||||
this.update({
|
||||
files: [file]
|
||||
});
|
||||
|
@ -297,7 +297,7 @@
|
|||
});
|
||||
|
||||
this.upload = file => {
|
||||
this.refs.uploader.upload(file);
|
||||
this.$refs.uploader.upload(file);
|
||||
};
|
||||
|
||||
this.clearReply = () => {
|
||||
|
@ -311,7 +311,7 @@
|
|||
this.update({
|
||||
files: null
|
||||
});
|
||||
this.refs.text.value = '';
|
||||
this.$refs.text.value = '';
|
||||
};
|
||||
|
||||
this.post = () => {
|
||||
|
@ -323,8 +323,8 @@
|
|||
? this.files.map(f => f.id)
|
||||
: undefined;
|
||||
|
||||
this.api('posts/create', {
|
||||
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
|
||||
this.$root.$data.os.api('posts/create', {
|
||||
text: this.$refs.text.value == '' ? undefined : this.$refs.text.value,
|
||||
media_ids: files,
|
||||
reply_id: this.reply ? this.reply.id : undefined,
|
||||
channel_id: this.channel.id
|
||||
|
@ -340,11 +340,11 @@
|
|||
};
|
||||
|
||||
this.changeFile = () => {
|
||||
Array.from(this.refs.file.files).forEach(this.upload);
|
||||
Array.from(this.$refs.file.files).forEach(this.upload);
|
||||
};
|
||||
|
||||
this.selectFile = () => {
|
||||
this.refs.file.click();
|
||||
this.$refs.file.click();
|
||||
};
|
||||
|
||||
this.drive = () => {
|
||||
|
@ -375,7 +375,7 @@
|
|||
|
||||
<mk-twitter-button>
|
||||
<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a>
|
||||
<script>
|
||||
<script lang="typescript">
|
||||
this.on('mount', () => {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
|
@ -388,7 +388,7 @@
|
|||
|
||||
<mk-line-button>
|
||||
<div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ _CH_URL_ } style="display: none;"></div>
|
||||
<script>
|
||||
<script lang="typescript">
|
||||
this.on('mount', () => {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
<a href={ _CH_URL_ }>Index</a> | <a href={ _URL_ }>Misskey</a>
|
||||
</div>
|
||||
<div>
|
||||
<a if={ !SIGNIN } href={ _URL_ }>ログイン(新規登録)</a>
|
||||
<a if={ SIGNIN } href={ _URL_ + '/' + I.username }>{ I.username }</a>
|
||||
<a v-if="!$root.$data.os.isSignedIn" href={ _URL_ }>ログイン(新規登録)</a>
|
||||
<a v-if="$root.$data.os.isSignedIn" href={ _URL_ + '/' + I.username }>{ I.username }</a>
|
||||
</div>
|
||||
<style>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display flex
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
|||
margin-left auto
|
||||
|
||||
</style>
|
||||
<script>
|
||||
<script lang="typescript">
|
||||
this.mixin('i');
|
||||
</script>
|
||||
</mk-header>
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
<mk-index>
|
||||
<mk-header/>
|
||||
<hr>
|
||||
<button onclick={ n }>%i18n:ch.tags.mk-index.new%</button>
|
||||
<button @click="n">%i18n:ch.tags.mk-index.new%</button>
|
||||
<hr>
|
||||
<ul if={ channels }>
|
||||
<ul v-if="channels">
|
||||
<li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li>
|
||||
</ul>
|
||||
<style>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
|
||||
</style>
|
||||
<script>
|
||||
<script lang="typescript">
|
||||
this.mixin('api');
|
||||
|
||||
this.on('mount', () => {
|
||||
this.api('channels', {
|
||||
this.$root.$data.os.api('channels', {
|
||||
limit: 100
|
||||
}).then(channels => {
|
||||
this.update({
|
||||
|
@ -27,7 +27,7 @@
|
|||
this.n = () => {
|
||||
const title = window.prompt('%i18n:ch.tags.mk-index.channel-title%');
|
||||
|
||||
this.api('channels/create', {
|
||||
this.$root.$data.os.api('channels/create', {
|
||||
title: title
|
||||
}).then(channel => {
|
||||
location.href = '/' + channel.id;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<mk-authorized-apps>
|
||||
<div class="none ui info" if={ !fetching && apps.length == 0 }>
|
||||
<div class="none ui info" v-if="!fetching && apps.length == 0">
|
||||
<p>%fa:info-circle%%i18n:common.tags.mk-authorized-apps.no-apps%</p>
|
||||
</div>
|
||||
<div class="apps" if={ apps.length != 0 }>
|
||||
<div class="apps" v-if="apps.length != 0">
|
||||
<div each={ app in apps }>
|
||||
<p><b>{ app.name }</b></p>
|
||||
<p>{ app.description }</p>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
|
||||
|
@ -18,17 +18,16 @@
|
|||
border-bottom solid 1px #eee
|
||||
|
||||
</style>
|
||||
<script>
|
||||
<script lang="typescript">
|
||||
this.mixin('api');
|
||||
|
||||
this.apps = [];
|
||||
this.fetching = true;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.api('i/authorized_apps').then(apps => {
|
||||
this.$root.$data.os.api('i/authorized_apps').then(apps => {
|
||||
this.apps = apps;
|
||||
this.fetching = false;
|
||||
this.update();
|
||||
});
|
||||
});
|
||||
</script>
|
|
@ -1,13 +1,13 @@
|
|||
<mk-signin-history>
|
||||
<div class="records" if={ history.length != 0 }>
|
||||
<div class="records" v-if="history.length != 0">
|
||||
<mk-signin-record each={ rec in history } rec={ rec }/>
|
||||
</div>
|
||||
<style>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
|
||||
</style>
|
||||
<script>
|
||||
<script lang="typescript">
|
||||
this.mixin('i');
|
||||
this.mixin('api');
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
|||
this.fetching = true;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.api('i/signin_history').then(history => {
|
||||
this.$root.$data.os.api('i/signin_history').then(history => {
|
||||
this.update({
|
||||
fetching: false,
|
||||
history: history
|
||||
|
@ -42,15 +42,15 @@
|
|||
</mk-signin-history>
|
||||
|
||||
<mk-signin-record>
|
||||
<header onclick={ toggle }>
|
||||
<virtual if={ rec.success }>%fa:check%</virtual>
|
||||
<virtual if={ !rec.success }>%fa:times%</virtual>
|
||||
<header @click="toggle">
|
||||
<template v-if="rec.success">%fa:check%</template>
|
||||
<template v-if="!rec.success">%fa:times%</template>
|
||||
<span class="ip">{ rec.ip }</span>
|
||||
<mk-time time={ rec.created_at }/>
|
||||
</header>
|
||||
<pre ref="headers" class="json" show={ show }>{ JSON.stringify(rec.headers, null, 2) }</pre>
|
||||
|
||||
<style>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
border-bottom solid 1px #eee
|
||||
|
@ -97,14 +97,14 @@
|
|||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
<script lang="typescript">
|
||||
import hljs from 'highlight.js';
|
||||
|
||||
this.rec = this.opts.rec;
|
||||
this.show = false;
|
||||
|
||||
this.on('mount', () => {
|
||||
hljs.highlightBlock(this.refs.headers);
|
||||
hljs.highlightBlock(this.$refs.headers);
|
||||
});
|
||||
|
||||
this.toggle = () => {
|
44
src/web/app/common/define-widget.ts
Normal file
44
src/web/app/common/define-widget.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export default function<T extends object>(data: {
|
||||
name: string;
|
||||
props?: () => T;
|
||||
}) {
|
||||
return Vue.extend({
|
||||
props: {
|
||||
widget: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
id(): string {
|
||||
return this.widget.id;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
props: data.props ? data.props() : {} as T
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.props) {
|
||||
Object.keys(this.props).forEach(prop => {
|
||||
if (this.widget.data.hasOwnProperty(prop)) {
|
||||
this.props[prop] = this.widget.data[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.$watch('props', newProps => {
|
||||
(this as any).api('i/update_home', {
|
||||
id: this.id,
|
||||
data: newProps
|
||||
}).then(() => {
|
||||
(this as any).os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
|
||||
});
|
||||
}, {
|
||||
deep: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
8
src/web/app/common/filters/bytes.ts
Normal file
8
src/web/app/common/filters/bytes.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
Vue.filter('bytes', (v, digits = 0) => {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (v == 0) return '0Byte';
|
||||
const i = Math.floor(Math.log(v) / Math.log(1024));
|
||||
return (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
|
||||
});
|
1
src/web/app/common/filters/index.ts
Normal file
1
src/web/app/common/filters/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
require('./bytes');
|
|
@ -1,9 +1,15 @@
|
|||
import Vue from 'vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import * as riot from 'riot';
|
||||
import api from './scripts/api';
|
||||
import signout from './scripts/signout';
|
||||
import Progress from './scripts/loading';
|
||||
import HomeStreamManager from './scripts/streaming/home-stream-manager';
|
||||
import api from './scripts/api';
|
||||
import DriveStreamManager from './scripts/streaming/drive-stream-manager';
|
||||
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 Err from '../common/views/components/connect-failed.vue';
|
||||
|
||||
//#region environment variables
|
||||
declare const _VERSION_: string;
|
||||
|
@ -12,6 +18,41 @@ declare const _API_URL_: string;
|
|||
declare const _SW_PUBLICKEY_: string;
|
||||
//#endregion
|
||||
|
||||
export type API = {
|
||||
chooseDriveFile: (opts: {
|
||||
title?: string;
|
||||
currentFolder?: any;
|
||||
multiple?: boolean;
|
||||
}) => Promise<any>;
|
||||
|
||||
chooseDriveFolder: (opts: {
|
||||
title?: string;
|
||||
currentFolder?: any;
|
||||
}) => Promise<any>;
|
||||
|
||||
dialog: (opts: {
|
||||
title: string;
|
||||
text: string;
|
||||
actions: Array<{
|
||||
text: string;
|
||||
id?: string;
|
||||
}>;
|
||||
}) => Promise<string>;
|
||||
|
||||
input: (opts: {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
default?: string;
|
||||
}) => Promise<string>;
|
||||
|
||||
post: (opts?: {
|
||||
reply?: any;
|
||||
repost?: any;
|
||||
}) => void;
|
||||
|
||||
notify: (message: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Misskey Operating System
|
||||
*/
|
||||
|
@ -26,6 +67,16 @@ export default class MiOS extends EventEmitter {
|
|||
|
||||
private isMetaFetching = false;
|
||||
|
||||
public app: Vue;
|
||||
|
||||
public new(vm, props) {
|
||||
const w = new vm({
|
||||
parent: this.app,
|
||||
propsData: props
|
||||
}).$mount();
|
||||
document.body.appendChild(w.$el);
|
||||
}
|
||||
|
||||
/**
|
||||
* A signing user
|
||||
*/
|
||||
|
@ -34,7 +85,7 @@ export default class MiOS extends EventEmitter {
|
|||
/**
|
||||
* Whether signed in
|
||||
*/
|
||||
public get isSignedin() {
|
||||
public get isSignedIn() {
|
||||
return this.i != null;
|
||||
}
|
||||
|
||||
|
@ -45,11 +96,28 @@ export default class MiOS extends EventEmitter {
|
|||
return localStorage.getItem('debug') == 'true';
|
||||
}
|
||||
|
||||
public apis: API;
|
||||
|
||||
/**
|
||||
* A connection manager of home stream
|
||||
*/
|
||||
public stream: HomeStreamManager;
|
||||
|
||||
/**
|
||||
* Connection managers
|
||||
*/
|
||||
public streams: {
|
||||
driveStream: DriveStreamManager;
|
||||
serverStream: ServerStreamManager;
|
||||
requestsStream: RequestsStreamManager;
|
||||
messagingIndexStream: MessagingIndexStreamManager;
|
||||
} = {
|
||||
driveStream: null,
|
||||
serverStream: null,
|
||||
requestsStream: null,
|
||||
messagingIndexStream: null
|
||||
};
|
||||
|
||||
/**
|
||||
* A registration of service worker
|
||||
*/
|
||||
|
@ -60,6 +128,11 @@ export default class MiOS extends EventEmitter {
|
|||
*/
|
||||
private shouldRegisterSw: boolean;
|
||||
|
||||
/**
|
||||
* ウィンドウシステム
|
||||
*/
|
||||
public windows = new WindowSystem();
|
||||
|
||||
/**
|
||||
* MiOSインスタンスを作成します
|
||||
* @param shouldRegisterSw ServiceWorkerを登録するかどうか
|
||||
|
@ -69,6 +142,9 @@ export default class MiOS extends EventEmitter {
|
|||
|
||||
this.shouldRegisterSw = shouldRegisterSw;
|
||||
|
||||
this.streams.serverStream = new ServerStreamManager();
|
||||
this.streams.requestsStream = new RequestsStreamManager();
|
||||
|
||||
//#region BIND
|
||||
this.log = this.log.bind(this);
|
||||
this.logInfo = this.logInfo.bind(this);
|
||||
|
@ -79,6 +155,18 @@ export default class MiOS extends EventEmitter {
|
|||
this.getMeta = this.getMeta.bind(this);
|
||||
this.registerSw = this.registerSw.bind(this);
|
||||
//#endregion
|
||||
|
||||
this.once('signedin', () => {
|
||||
// Init home stream manager
|
||||
this.stream = new HomeStreamManager(this.i);
|
||||
|
||||
// Init other stream manager
|
||||
this.streams.driveStream = new DriveStreamManager(this.i);
|
||||
this.streams.messagingIndexStream = new MessagingIndexStreamManager(this.i);
|
||||
});
|
||||
|
||||
// TODO: this global export is for debugging. so disable this if production build
|
||||
(window as any).os = this;
|
||||
}
|
||||
|
||||
public log(...args) {
|
||||
|
@ -139,8 +227,10 @@ export default class MiOS extends EventEmitter {
|
|||
// When failure
|
||||
.catch(() => {
|
||||
// Render the error screen
|
||||
document.body.innerHTML = '<mk-error />';
|
||||
riot.mount('*');
|
||||
document.body.innerHTML = '<div id="err"></div>';
|
||||
new Vue({
|
||||
render: createEl => createEl(Err)
|
||||
}).$mount('#err');
|
||||
|
||||
Progress.done();
|
||||
});
|
||||
|
@ -153,30 +243,13 @@ export default class MiOS extends EventEmitter {
|
|||
// フェッチが完了したとき
|
||||
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;
|
||||
this.emit('signedin');
|
||||
|
||||
// Finish init
|
||||
callback();
|
||||
|
@ -200,8 +273,6 @@ export default class MiOS extends EventEmitter {
|
|||
// 後から新鮮なデータをフェッチ
|
||||
fetchme(cachedMe.token, freshData => {
|
||||
Object.assign(cachedMe, freshData);
|
||||
cachedMe.trigger('updated');
|
||||
cachedMe.trigger('refreshed');
|
||||
});
|
||||
} else {
|
||||
// Get token from cookie
|
||||
|
@ -223,7 +294,7 @@ export default class MiOS extends EventEmitter {
|
|||
if (!isSwSupported) return;
|
||||
|
||||
// Reject when not signed in to Misskey
|
||||
if (!this.isSignedin) return;
|
||||
if (!this.isSignedIn) return;
|
||||
|
||||
// When service worker activated
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
|
@ -331,6 +402,22 @@ export default class MiOS extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
class WindowSystem {
|
||||
private windows = new Set();
|
||||
|
||||
public add(window) {
|
||||
this.windows.add(window);
|
||||
}
|
||||
|
||||
public remove(window) {
|
||||
this.windows.delete(window);
|
||||
}
|
||||
|
||||
public getAll() {
|
||||
return this.windows;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the URL safe base64 string to a Uint8Array
|
||||
* @param base64String base64 string
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
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) });
|
||||
};
|
|
@ -1,6 +0,0 @@
|
|||
export default (bytes, digits = 0) => {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes == 0) return '0Byte';
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
|
||||
};
|
21
src/web/app/common/scripts/fuck-ad-block.ts
Normal file
21
src/web/app/common/scripts/fuck-ad-block.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
require('fuckadblock');
|
||||
|
||||
declare const fuckAdBlock: any;
|
||||
|
||||
export default (os) => {
|
||||
function adBlockDetected() {
|
||||
os.apis.dialog({
|
||||
title: '%fa:exclamation-triangle%広告ブロッカーを無効にしてください',
|
||||
text: '<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。',
|
||||
actins: [{
|
||||
text: 'OK'
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
if (fuckAdBlock === undefined) {
|
||||
adBlockDetected();
|
||||
} else {
|
||||
fuckAdBlock.onDetected(adBlockDetected);
|
||||
}
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export default x => typeof x.then == 'function';
|
|
@ -16,7 +16,9 @@ export default class Connection extends Stream {
|
|||
}, 1000 * 60);
|
||||
|
||||
// 自分の情報が更新されたとき
|
||||
this.on('i_updated', me.update);
|
||||
this.on('i_updated', i => {
|
||||
Object.assign(me, i);
|
||||
});
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではAPIが利用できないので強制的にサインアウトさせる
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
declare const _URL_: string;
|
||||
|
||||
import * as riot from 'riot';
|
||||
import * as pictograph from 'pictograph';
|
||||
|
||||
const escape = text =>
|
||||
text
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<');
|
||||
|
||||
export default (tokens, shouldBreak) => {
|
||||
if (shouldBreak == null) {
|
||||
shouldBreak = true;
|
||||
}
|
||||
|
||||
const me = (riot as any).mixin('i').me;
|
||||
|
||||
let text = tokens.map(token => {
|
||||
switch (token.type) {
|
||||
case 'text':
|
||||
return escape(token.content)
|
||||
.replace(/(\r\n|\n|\r)/g, shouldBreak ? '<br>' : ' ');
|
||||
case 'bold':
|
||||
return `<strong>${escape(token.bold)}</strong>`;
|
||||
case 'url':
|
||||
return `<mk-url href="${escape(token.content)}" target="_blank"></mk-url>`;
|
||||
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="${_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':
|
||||
return `<pre><code>${token.html}</code></pre>`;
|
||||
case 'inline-code':
|
||||
return `<code>${token.html}</code>`;
|
||||
case 'emoji':
|
||||
return pictograph.dic[token.emoji] || token.content;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
// Remove needless whitespaces
|
||||
text = text
|
||||
.replace(/ <code>/g, '<code>').replace(/<\/code> /g, '</code>')
|
||||
.replace(/<br><code><pre>/g, '<code><pre>').replace(/<\/code><\/pre><br>/g, '</code></pre>');
|
||||
|
||||
return text;
|
||||
};
|
|
@ -1,57 +0,0 @@
|
|||
<mk-activity-table>
|
||||
<svg if={ data } ref="canvas" viewBox="0 0 53 7" preserveAspectRatio="none">
|
||||
<rect each={ data } width="1" height="1"
|
||||
riot-x={ x } riot-y={ date.weekday }
|
||||
rx="1" ry="1"
|
||||
fill={ color }
|
||||
style="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
|
||||
max-width 600px
|
||||
margin 0 auto
|
||||
|
||||
> svg
|
||||
display block
|
||||
|
||||
> rect
|
||||
transform-origin center
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('api');
|
||||
|
||||
this.user = this.opts.user;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.api('aggregation/users/activity', {
|
||||
user_id: this.user.id
|
||||
}).then(data => {
|
||||
data.forEach(d => d.total = d.posts + d.replies + d.reposts);
|
||||
this.peak = Math.max.apply(null, data.map(d => d.total)) / 2;
|
||||
let x = 0;
|
||||
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 / this.peak;
|
||||
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++;
|
||||
});
|
||||
this.update({ data });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</mk-activity-table>
|
|
@ -1,24 +0,0 @@
|
|||
<mk-ellipsis><span>.</span><span>.</span><span>.</span>
|
||||
<style>
|
||||
:scope
|
||||
display inline
|
||||
|
||||
> span
|
||||
animation ellipsis 1.4s infinite ease-in-out both
|
||||
|
||||
&:nth-child(1)
|
||||
animation-delay 0s
|
||||
|
||||
&:nth-child(2)
|
||||
animation-delay 0.16s
|
||||
|
||||
&:nth-child(3)
|
||||
animation-delay 0.32s
|
||||
|
||||
@keyframes ellipsis
|
||||
0%, 80%, 100%
|
||||
opacity 1
|
||||
40%
|
||||
opacity 0
|
||||
</style>
|
||||
</mk-ellipsis>
|
|
@ -1,215 +0,0 @@
|
|||
<mk-error>
|
||||
<img src="data:image/jpeg;base64,%base64:/assets/error.jpg%" alt=""/>
|
||||
<h1>%i18n:common.tags.mk-error.title%</h1>
|
||||
<p class="text">{
|
||||
'%i18n:common.tags.mk-error.description%'.substr(0, '%i18n:common.tags.mk-error.description%'.indexOf('{'))
|
||||
}<a onclick={ reload }>{
|
||||
'%i18n:common.tags.mk-error.description%'.match(/\{(.+?)\}/)[1]
|
||||
}</a>{
|
||||
'%i18n:common.tags.mk-error.description%'.substr('%i18n:common.tags.mk-error.description%'.indexOf('}') + 1)
|
||||
}</p>
|
||||
<button if={ !troubleshooting } onclick={ troubleshoot }>%i18n:common.tags.mk-error.troubleshoot%</button>
|
||||
<mk-troubleshooter if={ troubleshooting }/>
|
||||
<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
width 100%
|
||||
padding 32px 18px
|
||||
text-align center
|
||||
|
||||
> img
|
||||
display block
|
||||
height 200px
|
||||
margin 0 auto
|
||||
pointer-events none
|
||||
user-select none
|
||||
|
||||
> h1
|
||||
display block
|
||||
margin 1.25em auto 0.65em auto
|
||||
font-size 1.5em
|
||||
color #555
|
||||
|
||||
> .text
|
||||
display block
|
||||
margin 0 auto
|
||||
max-width 600px
|
||||
font-size 1em
|
||||
color #666
|
||||
|
||||
> button
|
||||
display block
|
||||
margin 1em auto 0 auto
|
||||
padding 8px 10px
|
||||
color $theme-color-foreground
|
||||
background $theme-color
|
||||
|
||||
&:focus
|
||||
outline solid 3px rgba($theme-color, 0.3)
|
||||
|
||||
&:hover
|
||||
background lighten($theme-color, 10%)
|
||||
|
||||
&:active
|
||||
background darken($theme-color, 10%)
|
||||
|
||||
> mk-troubleshooter
|
||||
margin 1em auto 0 auto
|
||||
|
||||
> .thanks
|
||||
display block
|
||||
margin 2em auto 0 auto
|
||||
padding 2em 0 0 0
|
||||
max-width 600px
|
||||
font-size 0.9em
|
||||
font-style oblique
|
||||
color #aaa
|
||||
border-top solid 1px #eee
|
||||
|
||||
@media (max-width 500px)
|
||||
padding 24px 18px
|
||||
font-size 80%
|
||||
|
||||
> img
|
||||
height 150px
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.troubleshooting = false;
|
||||
|
||||
this.on('mount', () => {
|
||||
document.title = 'Oops!';
|
||||
document.documentElement.style.background = '#f8f8f8';
|
||||
});
|
||||
|
||||
this.reload = () => {
|
||||
location.reload();
|
||||
};
|
||||
|
||||
this.troubleshoot = () => {
|
||||
this.update({
|
||||
troubleshooting: true
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-error>
|
||||
|
||||
<mk-troubleshooter>
|
||||
<h1>%fa:wrench%%i18n:common.tags.mk-error.troubleshooter.title%</h1>
|
||||
<div>
|
||||
<p data-wip={ network == null }><virtual if={ network != null }><virtual if={ network }>%fa:check%</virtual><virtual if={ !network }>%fa:times%</virtual></virtual>{ network == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-network%' : '%i18n:common.tags.mk-error.troubleshooter.network%' }<mk-ellipsis if={ network == null }/></p>
|
||||
<p if={ network == true } data-wip={ internet == null }><virtual if={ internet != null }><virtual if={ internet }>%fa:check%</virtual><virtual if={ !internet }>%fa:times%</virtual></virtual>{ internet == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-internet%' : '%i18n:common.tags.mk-error.troubleshooter.internet%' }<mk-ellipsis if={ internet == null }/></p>
|
||||
<p if={ internet == true } data-wip={ server == null }><virtual if={ server != null }><virtual if={ server }>%fa:check%</virtual><virtual if={ !server }>%fa:times%</virtual></virtual>{ server == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-server%' : '%i18n:common.tags.mk-error.troubleshooter.server%' }<mk-ellipsis if={ server == null }/></p>
|
||||
</div>
|
||||
<p if={ !end }>%i18n:common.tags.mk-error.troubleshooter.finding%<mk-ellipsis/></p>
|
||||
<p if={ network === false }><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-network%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-network-desc%</p>
|
||||
<p if={ internet === false }><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-internet%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-internet-desc%</p>
|
||||
<p if={ server === false }><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-server%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-server-desc%</p>
|
||||
<p if={ server === true } class="success"><b>%fa:info-circle%%i18n:common.tags.mk-error.troubleshooter.success%</b><br>%i18n:common.tags.mk-error.troubleshooter.success-desc%</p>
|
||||
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
width 100%
|
||||
max-width 500px
|
||||
text-align left
|
||||
background #fff
|
||||
border-radius 8px
|
||||
border solid 1px #ddd
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
padding 0.6em 1.2em
|
||||
font-size 1em
|
||||
color #444
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
> [data-fa]
|
||||
margin-right 0.25em
|
||||
|
||||
> div
|
||||
overflow hidden
|
||||
padding 0.6em 1.2em
|
||||
|
||||
> p
|
||||
margin 0.5em 0
|
||||
font-size 0.9em
|
||||
color #444
|
||||
|
||||
&[data-wip]
|
||||
color #888
|
||||
|
||||
> [data-fa]
|
||||
margin-right 0.25em
|
||||
|
||||
&.times
|
||||
color #e03524
|
||||
|
||||
&.check
|
||||
color #84c32f
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 0.6em 1.2em
|
||||
font-size 1em
|
||||
color #444
|
||||
border-top solid 1px #eee
|
||||
|
||||
> b
|
||||
> [data-fa]
|
||||
margin-right 0.25em
|
||||
|
||||
&.success
|
||||
> b
|
||||
color #39adad
|
||||
|
||||
&:not(.success)
|
||||
> b
|
||||
color #ad4339
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.on('mount', () => {
|
||||
this.update({
|
||||
network: navigator.onLine
|
||||
});
|
||||
|
||||
if (!this.network) {
|
||||
this.update({
|
||||
end: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check internet connection
|
||||
fetch('https://google.com?rand=' + Math.random(), {
|
||||
mode: 'no-cors'
|
||||
}).then(() => {
|
||||
this.update({
|
||||
internet: true
|
||||
});
|
||||
|
||||
// Check misskey server is available
|
||||
fetch(`${_API_URL_}/meta`).then(() => {
|
||||
this.update({
|
||||
end: true,
|
||||
server: true
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.update({
|
||||
end: true,
|
||||
server: false
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.update({
|
||||
end: true,
|
||||
internet: false
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</mk-troubleshooter>
|
|
@ -1,10 +0,0 @@
|
|||
<mk-file-type-icon>
|
||||
<virtual if={ kind == 'image' }>%fa:file-image%</virtual>
|
||||
<style>
|
||||
:scope
|
||||
display inline
|
||||
</style>
|
||||
<script>
|
||||
this.kind = this.opts.type.split('/')[0];
|
||||
</script>
|
||||
</mk-file-type-icon>
|
|
@ -1,40 +0,0 @@
|
|||
<mk-forkit><a href="https://github.com/syuilo/misskey" target="_blank" title="%i18n:common.tags.mk-forkit.open-github-link%" aria-label="%i18n:common.tags.mk-forkit.open-github-link%">
|
||||
<svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden">
|
||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||
<path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path>
|
||||
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor"></path>
|
||||
</svg></a>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
|
||||
> a
|
||||
display block
|
||||
|
||||
> svg
|
||||
display block
|
||||
//fill #151513
|
||||
//color #fff
|
||||
fill $theme-color
|
||||
color $theme-color-foreground
|
||||
|
||||
.octo-arm
|
||||
transform-origin 130px 106px
|
||||
|
||||
&:hover
|
||||
.octo-arm
|
||||
animation octocat-wave 560ms ease-in-out
|
||||
|
||||
@keyframes octocat-wave
|
||||
0%, 100%
|
||||
transform rotate(0)
|
||||
20%, 60%
|
||||
transform rotate(-25deg)
|
||||
40%, 80%
|
||||
transform rotate(10deg)
|
||||
|
||||
</style>
|
||||
</mk-forkit>
|
|
@ -1,30 +0,0 @@
|
|||
require('./error.tag');
|
||||
require('./url.tag');
|
||||
require('./url-preview.tag');
|
||||
require('./time.tag');
|
||||
require('./file-type-icon.tag');
|
||||
require('./uploader.tag');
|
||||
require('./ellipsis.tag');
|
||||
require('./raw.tag');
|
||||
require('./number.tag');
|
||||
require('./special-message.tag');
|
||||
require('./signin.tag');
|
||||
require('./signup.tag');
|
||||
require('./forkit.tag');
|
||||
require('./introduction.tag');
|
||||
require('./signin-history.tag');
|
||||
require('./twitter-setting.tag');
|
||||
require('./authorized-apps.tag');
|
||||
require('./poll.tag');
|
||||
require('./poll-editor.tag');
|
||||
require('./messaging/room.tag');
|
||||
require('./messaging/message.tag');
|
||||
require('./messaging/index.tag');
|
||||
require('./messaging/form.tag');
|
||||
require('./stream-indicator.tag');
|
||||
require('./activity-table.tag');
|
||||
require('./reaction-picker.tag');
|
||||
require('./reactions-viewer.tag');
|
||||
require('./reaction-icon.tag');
|
||||
require('./post-menu.tag');
|
||||
require('./nav-links.tag');
|
|
@ -1,25 +0,0 @@
|
|||
<mk-introduction>
|
||||
<article>
|
||||
<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={ _DOCS_URL_ } target="_blank">もっと知りたい方はこちら</a></p>
|
||||
</article>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
h1
|
||||
margin 0
|
||||
text-align center
|
||||
font-size 1.2em
|
||||
|
||||
p
|
||||
margin 16px 0
|
||||
|
||||
&:last-child
|
||||
margin 0
|
||||
text-align center
|
||||
|
||||
</style>
|
||||
</mk-introduction>
|
|
@ -1,175 +0,0 @@
|
|||
<mk-messaging-form>
|
||||
<textarea ref="text" onkeypress={ onkeypress } onpaste={ onpaste } placeholder="%i18n:common.input-message-here%"></textarea>
|
||||
<div class="files"></div>
|
||||
<mk-uploader ref="uploader"/>
|
||||
<button class="send" onclick={ send } disabled={ sending } title="%i18n:common.send%">
|
||||
<virtual if={ !sending }>%fa:paper-plane%</virtual><virtual if={ sending }>%fa:spinner .spin%</virtual>
|
||||
</button>
|
||||
<button class="attach-from-local" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-local%">
|
||||
%fa:upload%
|
||||
</button>
|
||||
<button class="attach-from-drive" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-drive%">
|
||||
%fa:R folder-open%
|
||||
</button>
|
||||
<input name="file" type="file" accept="image/*"/>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> textarea
|
||||
cursor auto
|
||||
display block
|
||||
width 100%
|
||||
min-width 100%
|
||||
max-width 100%
|
||||
height 64px
|
||||
margin 0
|
||||
padding 8px
|
||||
font-size 1em
|
||||
color #000
|
||||
outline none
|
||||
border none
|
||||
border-top solid 1px #eee
|
||||
border-radius 0
|
||||
box-shadow none
|
||||
background transparent
|
||||
|
||||
> .send
|
||||
position absolute
|
||||
bottom 0
|
||||
right 0
|
||||
margin 0
|
||||
padding 10px 14px
|
||||
line-height 1em
|
||||
font-size 1em
|
||||
color #aaa
|
||||
transition color 0.1s ease
|
||||
|
||||
&:hover
|
||||
color $theme-color
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 10%)
|
||||
transition color 0s ease
|
||||
|
||||
.files
|
||||
display block
|
||||
margin 0
|
||||
padding 0 8px
|
||||
list-style none
|
||||
|
||||
&:after
|
||||
content ''
|
||||
display block
|
||||
clear both
|
||||
|
||||
> li
|
||||
display block
|
||||
float left
|
||||
margin 4px
|
||||
padding 0
|
||||
width 64px
|
||||
height 64px
|
||||
background-color #eee
|
||||
background-repeat no-repeat
|
||||
background-position center center
|
||||
background-size cover
|
||||
cursor move
|
||||
|
||||
&:hover
|
||||
> .remove
|
||||
display block
|
||||
|
||||
> .remove
|
||||
display none
|
||||
position absolute
|
||||
right -6px
|
||||
top -6px
|
||||
margin 0
|
||||
padding 0
|
||||
background transparent
|
||||
outline none
|
||||
border none
|
||||
border-radius 0
|
||||
box-shadow none
|
||||
cursor pointer
|
||||
|
||||
.attach-from-local
|
||||
.attach-from-drive
|
||||
margin 0
|
||||
padding 10px 14px
|
||||
line-height 1em
|
||||
font-size 1em
|
||||
font-weight normal
|
||||
text-decoration none
|
||||
color #aaa
|
||||
transition color 0.1s ease
|
||||
|
||||
&:hover
|
||||
color $theme-color
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 10%)
|
||||
transition color 0s ease
|
||||
|
||||
input[type=file]
|
||||
display none
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('api');
|
||||
|
||||
this.onpaste = e => {
|
||||
const data = e.clipboardData;
|
||||
const items = data.items;
|
||||
for (const item of items) {
|
||||
if (item.kind == 'file') {
|
||||
this.upload(item.getAsFile());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.onkeypress = e => {
|
||||
if ((e.which == 10 || e.which == 13) && e.ctrlKey) {
|
||||
this.send();
|
||||
}
|
||||
};
|
||||
|
||||
this.selectFile = () => {
|
||||
this.refs.file.click();
|
||||
};
|
||||
|
||||
this.selectFileFromDrive = () => {
|
||||
const browser = document.body.appendChild(document.createElement('mk-select-file-from-drive-window'));
|
||||
const event = riot.observable();
|
||||
riot.mount(browser, {
|
||||
multiple: true,
|
||||
event: event
|
||||
});
|
||||
event.one('selected', files => {
|
||||
files.forEach(this.addFile);
|
||||
});
|
||||
};
|
||||
|
||||
this.send = () => {
|
||||
this.sending = true;
|
||||
this.api('messaging/messages/create', {
|
||||
user_id: this.opts.user.id,
|
||||
text: this.refs.text.value
|
||||
}).then(message => {
|
||||
this.clear();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
}).then(() => {
|
||||
this.sending = false;
|
||||
this.update();
|
||||
});
|
||||
};
|
||||
|
||||
this.clear = () => {
|
||||
this.refs.text.value = '';
|
||||
this.files = [];
|
||||
this.update();
|
||||
};
|
||||
</script>
|
||||
</mk-messaging-form>
|
|
@ -1,456 +0,0 @@
|
|||
<mk-messaging data-compact={ opts.compact }>
|
||||
<div class="search" if={ !opts.compact }>
|
||||
<div class="form">
|
||||
<label for="search-input">%fa:search%</label>
|
||||
<input ref="search" type="search" oninput={ search } onkeydown={ onSearchKeydown } placeholder="%i18n:common.tags.mk-messaging.search-user%"/>
|
||||
</div>
|
||||
<div class="result">
|
||||
<ol class="users" if={ searchResult.length > 0 } ref="searchResult">
|
||||
<li each={ user, i in searchResult } onkeydown={ parent.onSearchResultKeydown.bind(null, i) } onclick={ user._click } tabindex="-1">
|
||||
<img class="avatar" src={ user.avatar_url + '?thumbnail&size=32' } alt=""/>
|
||||
<span class="name">{ user.name }</span>
|
||||
<span class="username">@{ user.username }</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history" if={ history.length > 0 }>
|
||||
<virtual each={ history }>
|
||||
<a class="user" data-is-me={ is_me } data-is-read={ is_read } onclick={ _click }>
|
||||
<div>
|
||||
<img class="avatar" src={ (is_me ? recipient.avatar_url : user.avatar_url) + '?thumbnail&size=64' } alt=""/>
|
||||
<header>
|
||||
<span class="name">{ is_me ? recipient.name : user.name }</span>
|
||||
<span class="username">{ '@' + (is_me ? recipient.username : user.username ) }</span>
|
||||
<mk-time time={ created_at }/>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p class="text"><span class="me" if={ is_me }>%i18n:common.tags.mk-messaging.you%:</span>{ text }</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</virtual>
|
||||
</div>
|
||||
<p class="no-history" if={ !fetching && history.length == 0 }>%i18n:common.tags.mk-messaging.no-history%</p>
|
||||
<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%%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
|
||||
position sticky
|
||||
top 0
|
||||
left 0
|
||||
z-index 1
|
||||
width 100%
|
||||
background #fff
|
||||
box-shadow 0 0px 2px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> .form
|
||||
padding 8px
|
||||
background #f7f7f7
|
||||
|
||||
> label
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 8px
|
||||
z-index 1
|
||||
height 100%
|
||||
width 38px
|
||||
pointer-events none
|
||||
|
||||
> [data-fa]
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
bottom 0
|
||||
left 0
|
||||
width 1em
|
||||
height 1em
|
||||
margin auto
|
||||
color #555
|
||||
|
||||
> input
|
||||
margin 0
|
||||
padding 0 0 0 38px
|
||||
width 100%
|
||||
font-size 1em
|
||||
line-height 38px
|
||||
color #000
|
||||
outline none
|
||||
border solid 1px #eee
|
||||
border-radius 5px
|
||||
box-shadow none
|
||||
transition color 0.5s ease, border 0.5s ease
|
||||
|
||||
&:hover
|
||||
border solid 1px #ddd
|
||||
transition border 0.2s ease
|
||||
|
||||
&:focus
|
||||
color darken($theme-color, 20%)
|
||||
border solid 1px $theme-color
|
||||
transition color 0, border 0
|
||||
|
||||
> .result
|
||||
display block
|
||||
top 0
|
||||
left 0
|
||||
z-index 2
|
||||
width 100%
|
||||
margin 0
|
||||
padding 0
|
||||
background #fff
|
||||
|
||||
> .users
|
||||
margin 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
> li
|
||||
display inline-block
|
||||
z-index 1
|
||||
width 100%
|
||||
padding 8px 32px
|
||||
vertical-align top
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
text-decoration none
|
||||
transition none
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
&:focus
|
||||
color #fff
|
||||
background $theme-color
|
||||
|
||||
.name
|
||||
color #fff
|
||||
|
||||
.username
|
||||
color #fff
|
||||
|
||||
&:active
|
||||
color #fff
|
||||
background darken($theme-color, 10%)
|
||||
|
||||
.name
|
||||
color #fff
|
||||
|
||||
.username
|
||||
color #fff
|
||||
|
||||
.avatar
|
||||
vertical-align middle
|
||||
min-width 32px
|
||||
min-height 32px
|
||||
max-width 32px
|
||||
max-height 32px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
.name
|
||||
margin 0 8px 0 0
|
||||
/*font-weight bold*/
|
||||
font-weight normal
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
|
||||
.username
|
||||
font-weight normal
|
||||
color rgba(0, 0, 0, 0.3)
|
||||
|
||||
> .history
|
||||
|
||||
> a
|
||||
display block
|
||||
text-decoration none
|
||||
background #fff
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
*
|
||||
pointer-events none
|
||||
user-select none
|
||||
|
||||
&:hover
|
||||
background #fafafa
|
||||
|
||||
> .avatar
|
||||
filter saturate(200%)
|
||||
|
||||
&:active
|
||||
background #eee
|
||||
|
||||
&[data-is-read]
|
||||
&[data-is-me]
|
||||
opacity 0.8
|
||||
|
||||
&:not([data-is-me]):not([data-is-read])
|
||||
> div
|
||||
background-image url("/assets/unread.svg")
|
||||
background-repeat no-repeat
|
||||
background-position 0 center
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> div
|
||||
max-width 500px
|
||||
margin 0 auto
|
||||
padding 20px 30px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> header
|
||||
margin-bottom 2px
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
|
||||
> .name
|
||||
text-align left
|
||||
display inline
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 1em
|
||||
color rgba(0, 0, 0, 0.9)
|
||||
font-weight bold
|
||||
transition all 0.1s ease
|
||||
|
||||
> .username
|
||||
text-align left
|
||||
margin 0 0 0 8px
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
|
||||
> mk-time
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
display inline
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
font-size 80%
|
||||
|
||||
> .avatar
|
||||
float left
|
||||
width 54px
|
||||
height 54px
|
||||
margin 0 16px 0 0
|
||||
border-radius 8px
|
||||
transition all 0.1s ease
|
||||
|
||||
> .body
|
||||
|
||||
> .text
|
||||
display block
|
||||
margin 0 0 0 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
overflow-wrap break-word
|
||||
font-size 1.1em
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
|
||||
.me
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
> .image
|
||||
display block
|
||||
max-width 100%
|
||||
max-height 512px
|
||||
|
||||
> .no-history
|
||||
margin 0
|
||||
padding 2em 1em
|
||||
text-align center
|
||||
color #999
|
||||
font-weight 500
|
||||
|
||||
> .fetching
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
// TODO: element base media query
|
||||
@media (max-width 400px)
|
||||
> .search
|
||||
> .result
|
||||
> .users
|
||||
> li
|
||||
padding 8px 16px
|
||||
|
||||
> .history
|
||||
> a
|
||||
&:not([data-is-me]):not([data-is-read])
|
||||
> div
|
||||
background-image none
|
||||
border-left solid 4px #3aa2dc
|
||||
|
||||
> div
|
||||
padding 16px
|
||||
font-size 14px
|
||||
|
||||
> .avatar
|
||||
margin 0 12px 0 0
|
||||
|
||||
</style>
|
||||
<script>
|
||||
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.fetching = false;
|
||||
history.forEach(message => {
|
||||
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 == '') {
|
||||
this.searchResult = [];
|
||||
return;
|
||||
}
|
||||
this.api('users/search', {
|
||||
query: q,
|
||||
max: 5
|
||||
}).then(users => {
|
||||
users.forEach(user => {
|
||||
user._click = () => {
|
||||
this.trigger('navigate-user', user);
|
||||
this.searchResult = [];
|
||||
};
|
||||
});
|
||||
this.update({
|
||||
searchResult: users
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.onSearchKeydown = e => {
|
||||
switch (e.which) {
|
||||
case 9: // [TAB]
|
||||
case 40: // [↓]
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.refs.searchResult.childNodes[0].focus();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this.onSearchResultKeydown = (i, e) => {
|
||||
const cancel = () => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
switch (true) {
|
||||
case e.which == 10: // [ENTER]
|
||||
case e.which == 13: // [ENTER]
|
||||
cancel();
|
||||
this.searchResult[i]._click();
|
||||
break;
|
||||
|
||||
case e.which == 27: // [ESC]
|
||||
cancel();
|
||||
this.refs.search.focus();
|
||||
break;
|
||||
|
||||
case e.which == 9 && e.shiftKey: // [TAB] + [Shift]
|
||||
case e.which == 38: // [↑]
|
||||
cancel();
|
||||
(this.refs.searchResult.childNodes[i].previousElementSibling || this.refs.searchResult.childNodes[this.searchResult.length - 1]).focus();
|
||||
break;
|
||||
|
||||
case e.which == 9: // [TAB]
|
||||
case e.which == 40: // [↓]
|
||||
cancel();
|
||||
(this.refs.searchResult.childNodes[i].nextElementSibling || this.refs.searchResult.childNodes[0]).focus();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
</mk-messaging>
|
|
@ -1,238 +0,0 @@
|
|||
<mk-messaging-message data-is-me={ message.is_me }>
|
||||
<a class="avatar-anchor" href={ '/' + message.user.username } title={ message.user.username } target="_blank">
|
||||
<img class="avatar" src={ message.user.avatar_url + '?thumbnail&size=80' } alt=""/>
|
||||
</a>
|
||||
<div class="content-container">
|
||||
<div class="balloon">
|
||||
<p class="read" if={ message.is_me && message.is_read }>%i18n:common.tags.mk-messaging-message.is-read%</p>
|
||||
<button class="delete-button" if={ message.is_me } title="%i18n:common.delete%"><img src="/assets/desktop/messaging/delete.png" alt="Delete"/></button>
|
||||
<div class="content" if={ !message.is_deleted }>
|
||||
<div ref="text"></div>
|
||||
<div class="image" if={ message.file }><img src={ message.file.url } alt="image" title={ message.file.name }/></div>
|
||||
</div>
|
||||
<div class="content" if={ message.is_deleted }>
|
||||
<p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<mk-time time={ message.created_at }/><virtual if={ message.is_edited }>%fa:pencil-alt%</virtual>
|
||||
</footer>
|
||||
</div>
|
||||
<style>
|
||||
:scope
|
||||
$me-balloon-color = #23A7B6
|
||||
|
||||
display block
|
||||
padding 10px 12px 10px 12px
|
||||
background-color transparent
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
min-width 54px
|
||||
min-height 54px
|
||||
max-width 54px
|
||||
max-height 54px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
transition all 0.1s ease
|
||||
|
||||
> .content-container
|
||||
display block
|
||||
margin 0 12px
|
||||
padding 0
|
||||
max-width calc(100% - 78px)
|
||||
|
||||
> .balloon
|
||||
display block
|
||||
float inherit
|
||||
margin 0
|
||||
padding 0
|
||||
max-width 100%
|
||||
min-height 38px
|
||||
border-radius 16px
|
||||
|
||||
&:before
|
||||
content ""
|
||||
pointer-events none
|
||||
display block
|
||||
position absolute
|
||||
top 12px
|
||||
|
||||
&:hover
|
||||
> .delete-button
|
||||
display block
|
||||
|
||||
> .delete-button
|
||||
display none
|
||||
position absolute
|
||||
z-index 1
|
||||
top -4px
|
||||
right -4px
|
||||
margin 0
|
||||
padding 0
|
||||
cursor pointer
|
||||
outline none
|
||||
border none
|
||||
border-radius 0
|
||||
box-shadow none
|
||||
background transparent
|
||||
|
||||
> img
|
||||
vertical-align bottom
|
||||
width 16px
|
||||
height 16px
|
||||
cursor pointer
|
||||
|
||||
> .read
|
||||
user-select none
|
||||
display block
|
||||
position absolute
|
||||
z-index 1
|
||||
bottom -4px
|
||||
left -12px
|
||||
margin 0
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
font-size 11px
|
||||
|
||||
> .content
|
||||
|
||||
> .is-deleted
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
overflow-wrap break-word
|
||||
font-size 1em
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
|
||||
> [ref='text']
|
||||
display block
|
||||
margin 0
|
||||
padding 8px 16px
|
||||
overflow hidden
|
||||
overflow-wrap break-word
|
||||
font-size 1em
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
|
||||
&, *
|
||||
user-select text
|
||||
cursor auto
|
||||
|
||||
& + .file
|
||||
&.image
|
||||
> img
|
||||
border-radius 0 0 16px 16px
|
||||
|
||||
> .file
|
||||
&.image
|
||||
> img
|
||||
display block
|
||||
max-width 100%
|
||||
max-height 512px
|
||||
border-radius 16px
|
||||
|
||||
> footer
|
||||
display block
|
||||
clear both
|
||||
margin 0
|
||||
padding 2px
|
||||
font-size 10px
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
> [data-fa]
|
||||
margin-left 4px
|
||||
|
||||
&:not([data-is-me='true'])
|
||||
> .avatar-anchor
|
||||
float left
|
||||
|
||||
> .content-container
|
||||
float left
|
||||
|
||||
> .balloon
|
||||
background #eee
|
||||
|
||||
&:before
|
||||
left -14px
|
||||
border-top solid 8px transparent
|
||||
border-right solid 8px #eee
|
||||
border-bottom solid 8px transparent
|
||||
border-left solid 8px transparent
|
||||
|
||||
> footer
|
||||
text-align left
|
||||
|
||||
&[data-is-me='true']
|
||||
> .avatar-anchor
|
||||
float right
|
||||
|
||||
> .content-container
|
||||
float right
|
||||
|
||||
> .balloon
|
||||
background $me-balloon-color
|
||||
|
||||
&:before
|
||||
right -14px
|
||||
left auto
|
||||
border-top solid 8px transparent
|
||||
border-right solid 8px transparent
|
||||
border-bottom solid 8px transparent
|
||||
border-left solid 8px $me-balloon-color
|
||||
|
||||
> .content
|
||||
|
||||
> p.is-deleted
|
||||
color rgba(255, 255, 255, 0.5)
|
||||
|
||||
> [ref='text']
|
||||
&, *
|
||||
color #fff !important
|
||||
|
||||
> footer
|
||||
text-align right
|
||||
|
||||
&[data-is-deleted='true']
|
||||
> .content-container
|
||||
opacity 0.5
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import compile from '../../../common/scripts/text-compiler';
|
||||
|
||||
this.mixin('i');
|
||||
|
||||
this.message = this.opts.message;
|
||||
this.message.is_me = this.message.user.id == this.I.id;
|
||||
|
||||
this.on('mount', () => {
|
||||
if (this.message.text) {
|
||||
const tokens = this.message.ast;
|
||||
|
||||
this.refs.text.innerHTML = compile(tokens);
|
||||
|
||||
Array.from(this.refs.text.children).forEach(e => {
|
||||
if (e.tagName == 'MK-URL') riot.mount(e);
|
||||
});
|
||||
|
||||
// URLをプレビュー
|
||||
tokens
|
||||
.filter(t => t.type == 'link')
|
||||
.map(t => {
|
||||
const el = this.refs.text.appendChild(document.createElement('mk-url-preview'));
|
||||
riot.mount(el, {
|
||||
url: t.content
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</mk-messaging-message>
|
|
@ -1,319 +0,0 @@
|
|||
<mk-messaging-room>
|
||||
<div class="stream">
|
||||
<p class="init" if={ init }>%fa:spinner .spin%%i18n:common.loading%</p>
|
||||
<p class="empty" if={ !init && messages.length == 0 }>%fa:info-circle%%i18n:common.tags.mk-messaging-room.empty%</p>
|
||||
<p class="no-history" if={ !init && messages.length > 0 && !moreMessagesIsInStock }>%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p>
|
||||
<button class="more { fetching: fetchingMoreMessages }" if={ moreMessagesIsInStock } onclick={ fetchMoreMessages } disabled={ fetchingMoreMessages }>
|
||||
<virtual if={ fetchingMoreMessages }>%fa:spinner .pulse .fw%</virtual>{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }
|
||||
</button>
|
||||
<virtual each={ message, i in messages }>
|
||||
<mk-messaging-message message={ message }/>
|
||||
<p class="date" if={ i != messages.length - 1 && message._date != messages[i + 1]._date }><span>{ messages[i + 1]._datetext }</span></p>
|
||||
</virtual>
|
||||
</div>
|
||||
<footer>
|
||||
<div ref="notifications"></div>
|
||||
<div class="grippie" title="%i18n:common.tags.mk-messaging-room.resize-form%"></div>
|
||||
<mk-messaging-form user={ user }/>
|
||||
</footer>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> .stream
|
||||
max-width 600px
|
||||
margin 0 auto
|
||||
|
||||
> .init
|
||||
width 100%
|
||||
margin 0
|
||||
padding 16px 8px 8px 8px
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
[data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .empty
|
||||
width 100%
|
||||
margin 0
|
||||
padding 16px 8px 8px 8px
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
[data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .no-history
|
||||
display block
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
[data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .more
|
||||
display block
|
||||
margin 16px auto
|
||||
padding 0 12px
|
||||
line-height 24px
|
||||
color #fff
|
||||
background rgba(0, 0, 0, 0.3)
|
||||
border-radius 12px
|
||||
|
||||
&:hover
|
||||
background rgba(0, 0, 0, 0.4)
|
||||
|
||||
&:active
|
||||
background rgba(0, 0, 0, 0.5)
|
||||
|
||||
&.fetching
|
||||
cursor wait
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .message
|
||||
// something
|
||||
|
||||
> .date
|
||||
display block
|
||||
margin 8px 0
|
||||
text-align center
|
||||
|
||||
&:before
|
||||
content ''
|
||||
display block
|
||||
position absolute
|
||||
height 1px
|
||||
width 90%
|
||||
top 16px
|
||||
left 0
|
||||
right 0
|
||||
margin 0 auto
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
|
||||
> span
|
||||
display inline-block
|
||||
margin 0
|
||||
padding 0 16px
|
||||
//font-weight bold
|
||||
line-height 32px
|
||||
color rgba(0, 0, 0, 0.3)
|
||||
background #fff
|
||||
|
||||
> footer
|
||||
position -webkit-sticky
|
||||
position sticky
|
||||
z-index 2
|
||||
bottom 0
|
||||
width 100%
|
||||
max-width 600px
|
||||
margin 0 auto
|
||||
padding 0
|
||||
background rgba(255, 255, 255, 0.95)
|
||||
background-clip content-box
|
||||
|
||||
> [ref='notifications']
|
||||
position absolute
|
||||
top -48px
|
||||
width 100%
|
||||
padding 8px 0
|
||||
text-align center
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> p
|
||||
display inline-block
|
||||
margin 0
|
||||
padding 0 12px 0 28px
|
||||
cursor pointer
|
||||
line-height 32px
|
||||
font-size 12px
|
||||
color $theme-color-foreground
|
||||
background $theme-color
|
||||
border-radius 16px
|
||||
transition opacity 1s ease
|
||||
|
||||
> [data-fa]
|
||||
position absolute
|
||||
top 0
|
||||
left 10px
|
||||
line-height 32px
|
||||
font-size 16px
|
||||
|
||||
> .grippie
|
||||
height 10px
|
||||
margin-top -10px
|
||||
background transparent
|
||||
cursor ns-resize
|
||||
|
||||
&:hover
|
||||
//background rgba(0, 0, 0, 0.1)
|
||||
|
||||
&:active
|
||||
//background rgba(0, 0, 0, 0.2)
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import MessagingStreamConnection from '../../scripts/streaming/messaging-stream';
|
||||
|
||||
this.mixin('i');
|
||||
this.mixin('api');
|
||||
|
||||
this.user = this.opts.user;
|
||||
this.init = true;
|
||||
this.sending = false;
|
||||
this.messages = [];
|
||||
this.isNaked = this.opts.isNaked;
|
||||
|
||||
this.connection = new MessagingStreamConnection(this.I, this.user.id);
|
||||
|
||||
this.on('mount', () => {
|
||||
this.connection.on('message', this.onMessage);
|
||||
this.connection.on('read', this.onRead);
|
||||
|
||||
document.addEventListener('visibilitychange', this.onVisibilitychange);
|
||||
|
||||
this.fetchMessages().then(() => {
|
||||
this.init = false;
|
||||
this.update();
|
||||
this.scrollToBottom();
|
||||
});
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.connection.off('message', this.onMessage);
|
||||
this.connection.off('read', this.onRead);
|
||||
this.connection.close();
|
||||
|
||||
document.removeEventListener('visibilitychange', this.onVisibilitychange);
|
||||
});
|
||||
|
||||
this.on('update', () => {
|
||||
this.messages.forEach(message => {
|
||||
const date = (new Date(message.created_at)).getDate();
|
||||
const month = (new Date(message.created_at)).getMonth() + 1;
|
||||
message._date = date;
|
||||
message._datetext = month + '月 ' + date + '日';
|
||||
});
|
||||
});
|
||||
|
||||
this.onMessage = (message) => {
|
||||
const isBottom = this.isBottom();
|
||||
|
||||
this.messages.push(message);
|
||||
if (message.user_id != this.I.id && !document.hidden) {
|
||||
this.connection.send({
|
||||
type: 'read',
|
||||
id: message.id
|
||||
});
|
||||
}
|
||||
this.update();
|
||||
|
||||
if (isBottom) {
|
||||
// Scroll to bottom
|
||||
this.scrollToBottom();
|
||||
} else if (message.user_id != this.I.id) {
|
||||
// Notify
|
||||
this.notify('%i18n:common.tags.mk-messaging-room.new-message%');
|
||||
}
|
||||
};
|
||||
|
||||
this.onRead = ids => {
|
||||
if (!Array.isArray(ids)) ids = [ids];
|
||||
ids.forEach(id => {
|
||||
if (this.messages.some(x => x.id == id)) {
|
||||
const exist = this.messages.map(x => x.id).indexOf(id);
|
||||
this.messages[exist].is_read = true;
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.fetchMoreMessages = () => {
|
||||
this.update({
|
||||
fetchingMoreMessages: true
|
||||
});
|
||||
this.fetchMessages().then(() => {
|
||||
this.update({
|
||||
fetchingMoreMessages: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.fetchMessages = () => new Promise((resolve, reject) => {
|
||||
const max = this.moreMessagesIsInStock ? 20 : 10;
|
||||
|
||||
this.api('messaging/messages', {
|
||||
user_id: this.user.id,
|
||||
limit: max + 1,
|
||||
until_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined
|
||||
}).then(messages => {
|
||||
if (messages.length == max + 1) {
|
||||
this.moreMessagesIsInStock = true;
|
||||
messages.pop();
|
||||
} else {
|
||||
this.moreMessagesIsInStock = false;
|
||||
}
|
||||
|
||||
this.messages.unshift.apply(this.messages, messages.reverse());
|
||||
this.update();
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
this.isBottom = () => {
|
||||
const asobi = 32;
|
||||
const current = this.isNaked
|
||||
? window.scrollY + window.innerHeight
|
||||
: this.root.scrollTop + this.root.offsetHeight;
|
||||
const max = this.isNaked
|
||||
? document.body.offsetHeight
|
||||
: this.root.scrollHeight;
|
||||
return current > (max - asobi);
|
||||
};
|
||||
|
||||
this.scrollToBottom = () => {
|
||||
if (this.isNaked) {
|
||||
window.scroll(0, document.body.offsetHeight);
|
||||
} else {
|
||||
this.root.scrollTop = this.root.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
this.notify = message => {
|
||||
const n = document.createElement('p');
|
||||
n.innerHTML = '%fa:arrow-circle-down%' + message;
|
||||
n.onclick = () => {
|
||||
this.scrollToBottom();
|
||||
n.parentNode.removeChild(n);
|
||||
};
|
||||
this.refs.notifications.appendChild(n);
|
||||
|
||||
setTimeout(() => {
|
||||
n.style.opacity = 0;
|
||||
setTimeout(() => n.parentNode.removeChild(n), 1000);
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
this.onVisibilitychange = () => {
|
||||
if (document.hidden) return;
|
||||
this.messages.forEach(message => {
|
||||
if (message.user_id !== this.I.id && !message.is_read) {
|
||||
this.connection.send({
|
||||
type: 'read',
|
||||
id: message.id
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-messaging-room>
|
|
@ -1,10 +0,0 @@
|
|||
<mk-nav-links>
|
||||
<a href={ aboutUrl }>%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 %fa:B twitter%</a>
|
||||
<style>
|
||||
:scope
|
||||
display inline
|
||||
</style>
|
||||
<script>
|
||||
this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/about`;
|
||||
</script>
|
||||
</mk-nav-links>
|
|
@ -1,16 +0,0 @@
|
|||
<mk-number>
|
||||
<style>
|
||||
:scope
|
||||
display inline
|
||||
</style>
|
||||
<script>
|
||||
this.on('mount', () => {
|
||||
let value = this.opts.value;
|
||||
const max = this.opts.max;
|
||||
|
||||
if (max != null && value > max) value = max;
|
||||
|
||||
this.root.innerHTML = value.toLocaleString();
|
||||
});
|
||||
</script>
|
||||
</mk-number>
|
|
@ -1,121 +0,0 @@
|
|||
<mk-poll-editor>
|
||||
<p class="caution" if={ choices.length < 2 }>
|
||||
%fa:exclamation-triangle%%i18n:common.tags.mk-poll-editor.no-only-one-choice%
|
||||
</p>
|
||||
<ul ref="choices">
|
||||
<li each={ choice, i in choices }>
|
||||
<input value={ choice } oninput={ oninput.bind(null, i) } placeholder={ '%i18n:common.tags.mk-poll-editor.choice-n%'.replace('{}', i + 1) }>
|
||||
<button onclick={ remove.bind(null, i) } title="%i18n:common.tags.mk-poll-editor.remove%">
|
||||
%fa:times%
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="add" if={ choices.length < 10 } onclick={ add }>%i18n:common.tags.mk-poll-editor.add%</button>
|
||||
<button class="destroy" onclick={ destroy } title="%i18n:common.tags.mk-poll-editor.destroy%">
|
||||
%fa:times%
|
||||
</button>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
padding 8px
|
||||
|
||||
> .caution
|
||||
margin 0 0 8px 0
|
||||
font-size 0.8em
|
||||
color #f00
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> ul
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
> li
|
||||
display block
|
||||
margin 8px 0
|
||||
padding 0
|
||||
width 100%
|
||||
|
||||
&:first-child
|
||||
margin-top 0
|
||||
|
||||
&:last-child
|
||||
margin-bottom 0
|
||||
|
||||
> input
|
||||
padding 6px
|
||||
border solid 1px rgba($theme-color, 0.1)
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
border-color rgba($theme-color, 0.2)
|
||||
|
||||
&:focus
|
||||
border-color rgba($theme-color, 0.5)
|
||||
|
||||
> button
|
||||
padding 4px 8px
|
||||
color rgba($theme-color, 0.4)
|
||||
|
||||
&:hover
|
||||
color rgba($theme-color, 0.6)
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 30%)
|
||||
|
||||
> .add
|
||||
margin 8px 0 0 0
|
||||
vertical-align top
|
||||
color $theme-color
|
||||
|
||||
> .destroy
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
padding 4px 8px
|
||||
color rgba($theme-color, 0.4)
|
||||
|
||||
&:hover
|
||||
color rgba($theme-color, 0.6)
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 30%)
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.choices = ['', ''];
|
||||
|
||||
this.oninput = (i, e) => {
|
||||
this.choices[i] = e.target.value;
|
||||
};
|
||||
|
||||
this.add = () => {
|
||||
this.choices.push('');
|
||||
this.update();
|
||||
this.refs.choices.childNodes[this.choices.length - 1].childNodes[0].focus();
|
||||
};
|
||||
|
||||
this.remove = (i) => {
|
||||
this.choices = this.choices.filter((_, _i) => _i != i);
|
||||
this.update();
|
||||
};
|
||||
|
||||
this.destroy = () => {
|
||||
this.opts.ondestroy();
|
||||
};
|
||||
|
||||
this.get = () => {
|
||||
return {
|
||||
choices: this.choices.filter(choice => choice != '')
|
||||
}
|
||||
};
|
||||
|
||||
this.set = data => {
|
||||
if (data.choices.length == 0) return;
|
||||
this.choices = data.choices;
|
||||
};
|
||||
</script>
|
||||
</mk-poll-editor>
|
|
@ -1,109 +0,0 @@
|
|||
<mk-poll data-is-voted={ isVoted }>
|
||||
<ul>
|
||||
<li each={ poll.choices } onclick={ vote.bind(null, id) } class={ voted: voted } title={ !parent.isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', text) : '' }>
|
||||
<div class="backdrop" style={ 'width:' + (parent.result ? (votes / parent.total * 100) : 0) + '%' }></div>
|
||||
<span>
|
||||
<virtual if={ is_voted }>%fa:check%</virtual>
|
||||
{ text }
|
||||
<span class="votes" if={ parent.result }>({ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', votes) })</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p if={ total > 0 }>
|
||||
<span>{ '%i18n:common.tags.mk-poll.total-users%'.replace('{}', total) }</span>
|
||||
・
|
||||
<a if={ !isVoted } onclick={ toggleResult }>{ result ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }</a>
|
||||
<span if={ isVoted }>%i18n:common.tags.mk-poll.voted%</span>
|
||||
</p>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> ul
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
> li
|
||||
display block
|
||||
margin 4px 0
|
||||
padding 4px 8px
|
||||
width 100%
|
||||
border solid 1px #eee
|
||||
border-radius 4px
|
||||
overflow hidden
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
background rgba(0, 0, 0, 0.05)
|
||||
|
||||
&:active
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
|
||||
> .backdrop
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
height 100%
|
||||
background $theme-color
|
||||
transition width 1s ease
|
||||
|
||||
> .votes
|
||||
margin-left 4px
|
||||
|
||||
> p
|
||||
a
|
||||
color inherit
|
||||
|
||||
&[data-is-voted]
|
||||
> ul > li
|
||||
cursor default
|
||||
|
||||
&:hover
|
||||
background transparent
|
||||
|
||||
&:active
|
||||
background transparent
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('api');
|
||||
|
||||
this.init = post => {
|
||||
this.post = post;
|
||||
this.poll = this.post.poll;
|
||||
this.total = this.poll.choices.reduce((a, b) => a + b.votes, 0);
|
||||
this.isVoted = this.poll.choices.some(c => c.is_voted);
|
||||
this.result = this.isVoted;
|
||||
this.update();
|
||||
};
|
||||
|
||||
this.init(this.opts.post);
|
||||
|
||||
this.toggleResult = () => {
|
||||
this.result = !this.result;
|
||||
};
|
||||
|
||||
this.vote = id => {
|
||||
if (this.poll.choices.some(c => c.is_voted)) return;
|
||||
this.api('posts/polls/vote', {
|
||||
post_id: this.post.id,
|
||||
choice: id
|
||||
}).then(() => {
|
||||
this.poll.choices.forEach(c => {
|
||||
if (c.id == id) {
|
||||
c.votes++;
|
||||
c.is_voted = true;
|
||||
}
|
||||
});
|
||||
this.update({
|
||||
poll: this.poll,
|
||||
isVoted: true,
|
||||
result: true,
|
||||
total: this.total + 1
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-poll>
|
|
@ -1,157 +0,0 @@
|
|||
<mk-post-menu>
|
||||
<div class="backdrop" ref="backdrop" onclick={ close }></div>
|
||||
<div class="popover { compact: opts.compact }" ref="popover">
|
||||
<button if={ post.user_id === I.id } onclick={ pin }>%i18n:common.tags.mk-post-menu.pin%</button>
|
||||
<div if={ I.is_pro && !post.is_category_verified }>
|
||||
<select ref="categorySelect">
|
||||
<option value="">%i18n:common.tags.mk-post-menu.select%</option>
|
||||
<option value="music">%i18n:common.post_categories.music%</option>
|
||||
<option value="game">%i18n:common.post_categories.game%</option>
|
||||
<option value="anime">%i18n:common.post_categories.anime%</option>
|
||||
<option value="it">%i18n:common.post_categories.it%</option>
|
||||
<option value="gadgets">%i18n:common.post_categories.gadgets%</option>
|
||||
<option value="photography">%i18n:common.post_categories.photography%</option>
|
||||
</select>
|
||||
<button onclick={ categorize }>%i18n:common.tags.mk-post-menu.categorize%</button>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
$border-color = rgba(27, 31, 35, 0.15)
|
||||
|
||||
:scope
|
||||
display block
|
||||
position initial
|
||||
|
||||
> .backdrop
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 10000
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
opacity 0
|
||||
|
||||
> .popover
|
||||
position absolute
|
||||
z-index 10001
|
||||
background #fff
|
||||
border 1px solid $border-color
|
||||
border-radius 4px
|
||||
box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
|
||||
transform scale(0.5)
|
||||
opacity 0
|
||||
|
||||
$balloon-size = 16px
|
||||
|
||||
&:not(.compact)
|
||||
margin-top $balloon-size
|
||||
transform-origin center -($balloon-size)
|
||||
|
||||
&:before
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top -($balloon-size * 2)
|
||||
left s('calc(50% - %s)', $balloon-size)
|
||||
border-top solid $balloon-size transparent
|
||||
border-left solid $balloon-size transparent
|
||||
border-right solid $balloon-size transparent
|
||||
border-bottom solid $balloon-size $border-color
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top -($balloon-size * 2) + 1.5px
|
||||
left s('calc(50% - %s)', $balloon-size)
|
||||
border-top solid $balloon-size transparent
|
||||
border-left solid $balloon-size transparent
|
||||
border-right solid $balloon-size transparent
|
||||
border-bottom solid $balloon-size #fff
|
||||
|
||||
> button
|
||||
display block
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import anime from 'animejs';
|
||||
|
||||
this.mixin('i');
|
||||
this.mixin('api');
|
||||
|
||||
this.post = this.opts.post;
|
||||
this.source = this.opts.source;
|
||||
|
||||
this.on('mount', () => {
|
||||
const rect = this.source.getBoundingClientRect();
|
||||
const width = this.refs.popover.offsetWidth;
|
||||
const height = this.refs.popover.offsetHeight;
|
||||
if (this.opts.compact) {
|
||||
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
|
||||
this.refs.popover.style.left = (x - (width / 2)) + 'px';
|
||||
this.refs.popover.style.top = (y - (height / 2)) + 'px';
|
||||
} else {
|
||||
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + window.pageYOffset + this.source.offsetHeight;
|
||||
this.refs.popover.style.left = (x - (width / 2)) + 'px';
|
||||
this.refs.popover.style.top = y + 'px';
|
||||
}
|
||||
|
||||
anime({
|
||||
targets: this.refs.backdrop,
|
||||
opacity: 1,
|
||||
duration: 100,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
anime({
|
||||
targets: this.refs.popover,
|
||||
opacity: 1,
|
||||
scale: [0.5, 1],
|
||||
duration: 500
|
||||
});
|
||||
});
|
||||
|
||||
this.pin = () => {
|
||||
this.api('i/pin', {
|
||||
post_id: this.post.id
|
||||
}).then(() => {
|
||||
if (this.opts.cb) this.opts.cb('pinned', '%i18n:common.tags.mk-post-menu.pinned%');
|
||||
this.unmount();
|
||||
});
|
||||
};
|
||||
|
||||
this.categorize = () => {
|
||||
const category = this.refs.categorySelect.options[this.refs.categorySelect.selectedIndex].value;
|
||||
this.api('posts/categorize', {
|
||||
post_id: this.post.id,
|
||||
category: category
|
||||
}).then(() => {
|
||||
if (this.opts.cb) this.opts.cb('categorized', '%i18n:common.tags.mk-post-menu.categorized%');
|
||||
this.unmount();
|
||||
});
|
||||
};
|
||||
|
||||
this.close = () => {
|
||||
this.refs.backdrop.style.pointerEvents = 'none';
|
||||
anime({
|
||||
targets: this.refs.backdrop,
|
||||
opacity: 0,
|
||||
duration: 200,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
this.refs.popover.style.pointerEvents = 'none';
|
||||
anime({
|
||||
targets: this.refs.popover,
|
||||
opacity: 0,
|
||||
scale: 0.5,
|
||||
duration: 200,
|
||||
easing: 'easeInBack',
|
||||
complete: () => this.unmount()
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-post-menu>
|
|
@ -1,13 +0,0 @@
|
|||
<mk-raw>
|
||||
<style>
|
||||
:scope
|
||||
display inline
|
||||
</style>
|
||||
<script>
|
||||
this.root.innerHTML = this.opts.content;
|
||||
|
||||
this.on('updated', () => {
|
||||
this.root.innerHTML = this.opts.content;
|
||||
});
|
||||
</script>
|
||||
</mk-raw>
|
|
@ -1,21 +0,0 @@
|
|||
<mk-reaction-icon>
|
||||
<virtual if={ opts.reaction == 'like' }><img src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%"></virtual>
|
||||
<virtual if={ opts.reaction == 'love' }><img src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%"></virtual>
|
||||
<virtual if={ opts.reaction == 'laugh' }><img src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%"></virtual>
|
||||
<virtual if={ opts.reaction == 'hmm' }><img src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%"></virtual>
|
||||
<virtual if={ opts.reaction == 'surprise' }><img src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%"></virtual>
|
||||
<virtual if={ opts.reaction == 'congrats' }><img src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%"></virtual>
|
||||
<virtual if={ opts.reaction == 'angry' }><img src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%"></virtual>
|
||||
<virtual if={ opts.reaction == 'confused' }><img src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%"></virtual>
|
||||
<virtual if={ opts.reaction == 'pudding' }><img src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%"></virtual>
|
||||
|
||||
<style>
|
||||
:scope
|
||||
display inline
|
||||
|
||||
img
|
||||
vertical-align middle
|
||||
width 1em
|
||||
height 1em
|
||||
</style>
|
||||
</mk-reaction-icon>
|
|
@ -1,184 +0,0 @@
|
|||
<mk-reaction-picker>
|
||||
<div class="backdrop" ref="backdrop" onclick={ close }></div>
|
||||
<div class="popover { compact: opts.compact }" ref="popover">
|
||||
<p if={ !opts.compact }>{ title }</p>
|
||||
<div>
|
||||
<button onclick={ react.bind(null, 'like') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
|
||||
<button onclick={ react.bind(null, 'love') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
|
||||
<button onclick={ react.bind(null, 'laugh') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
|
||||
<button onclick={ react.bind(null, 'hmm') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="4" title="%i18n:common.reactions.hmm%"><mk-reaction-icon reaction='hmm'/></button>
|
||||
<button onclick={ react.bind(null, 'surprise') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="5" title="%i18n:common.reactions.surprise%"><mk-reaction-icon reaction='surprise'/></button>
|
||||
<button onclick={ react.bind(null, 'congrats') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="6" title="%i18n:common.reactions.congrats%"><mk-reaction-icon reaction='congrats'/></button>
|
||||
<button onclick={ react.bind(null, 'angry') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="4" title="%i18n:common.reactions.angry%"><mk-reaction-icon reaction='angry'/></button>
|
||||
<button onclick={ react.bind(null, 'confused') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="5" title="%i18n:common.reactions.confused%"><mk-reaction-icon reaction='confused'/></button>
|
||||
<button onclick={ react.bind(null, 'pudding') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="6" title="%i18n:common.reactions.pudding%"><mk-reaction-icon reaction='pudding'/></button>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
$border-color = rgba(27, 31, 35, 0.15)
|
||||
|
||||
:scope
|
||||
display block
|
||||
position initial
|
||||
|
||||
> .backdrop
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 10000
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
opacity 0
|
||||
|
||||
> .popover
|
||||
position absolute
|
||||
z-index 10001
|
||||
background #fff
|
||||
border 1px solid $border-color
|
||||
border-radius 4px
|
||||
box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
|
||||
transform scale(0.5)
|
||||
opacity 0
|
||||
|
||||
$balloon-size = 16px
|
||||
|
||||
&:not(.compact)
|
||||
margin-top $balloon-size
|
||||
transform-origin center -($balloon-size)
|
||||
|
||||
&:before
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top -($balloon-size * 2)
|
||||
left s('calc(50% - %s)', $balloon-size)
|
||||
border-top solid $balloon-size transparent
|
||||
border-left solid $balloon-size transparent
|
||||
border-right solid $balloon-size transparent
|
||||
border-bottom solid $balloon-size $border-color
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top -($balloon-size * 2) + 1.5px
|
||||
left s('calc(50% - %s)', $balloon-size)
|
||||
border-top solid $balloon-size transparent
|
||||
border-left solid $balloon-size transparent
|
||||
border-right solid $balloon-size transparent
|
||||
border-bottom solid $balloon-size #fff
|
||||
|
||||
> p
|
||||
display block
|
||||
margin 0
|
||||
padding 8px 10px
|
||||
font-size 14px
|
||||
color #586069
|
||||
border-bottom solid 1px #e1e4e8
|
||||
|
||||
> div
|
||||
padding 4px
|
||||
width 240px
|
||||
text-align center
|
||||
|
||||
> button
|
||||
width 40px
|
||||
height 40px
|
||||
font-size 24px
|
||||
border-radius 2px
|
||||
|
||||
&:hover
|
||||
background #eee
|
||||
|
||||
&:active
|
||||
background $theme-color
|
||||
box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import anime from 'animejs';
|
||||
|
||||
this.mixin('api');
|
||||
|
||||
this.post = this.opts.post;
|
||||
this.source = this.opts.source;
|
||||
|
||||
const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
|
||||
|
||||
this.title = placeholder;
|
||||
|
||||
this.onmouseover = e => {
|
||||
this.update({
|
||||
title: e.target.title
|
||||
});
|
||||
};
|
||||
|
||||
this.onmouseout = () => {
|
||||
this.update({
|
||||
title: placeholder
|
||||
});
|
||||
};
|
||||
|
||||
this.on('mount', () => {
|
||||
const rect = this.source.getBoundingClientRect();
|
||||
const width = this.refs.popover.offsetWidth;
|
||||
const height = this.refs.popover.offsetHeight;
|
||||
if (this.opts.compact) {
|
||||
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
|
||||
this.refs.popover.style.left = (x - (width / 2)) + 'px';
|
||||
this.refs.popover.style.top = (y - (height / 2)) + 'px';
|
||||
} else {
|
||||
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + window.pageYOffset + this.source.offsetHeight;
|
||||
this.refs.popover.style.left = (x - (width / 2)) + 'px';
|
||||
this.refs.popover.style.top = y + 'px';
|
||||
}
|
||||
|
||||
anime({
|
||||
targets: this.refs.backdrop,
|
||||
opacity: 1,
|
||||
duration: 100,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
anime({
|
||||
targets: this.refs.popover,
|
||||
opacity: 1,
|
||||
scale: [0.5, 1],
|
||||
duration: 500
|
||||
});
|
||||
});
|
||||
|
||||
this.react = reaction => {
|
||||
this.api('posts/reactions/create', {
|
||||
post_id: this.post.id,
|
||||
reaction: reaction
|
||||
}).then(() => {
|
||||
if (this.opts.cb) this.opts.cb();
|
||||
this.unmount();
|
||||
});
|
||||
};
|
||||
|
||||
this.close = () => {
|
||||
this.refs.backdrop.style.pointerEvents = 'none';
|
||||
anime({
|
||||
targets: this.refs.backdrop,
|
||||
opacity: 0,
|
||||
duration: 200,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
this.refs.popover.style.pointerEvents = 'none';
|
||||
anime({
|
||||
targets: this.refs.popover,
|
||||
opacity: 0,
|
||||
scale: 0.5,
|
||||
duration: 200,
|
||||
easing: 'easeInBack',
|
||||
complete: () => this.unmount()
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-reaction-picker>
|
|
@ -1,46 +0,0 @@
|
|||
<mk-reactions-viewer>
|
||||
<virtual if={ reactions }>
|
||||
<span if={ reactions.like }><mk-reaction-icon reaction='like'/><span>{ reactions.like }</span></span>
|
||||
<span if={ reactions.love }><mk-reaction-icon reaction='love'/><span>{ reactions.love }</span></span>
|
||||
<span if={ reactions.laugh }><mk-reaction-icon reaction='laugh'/><span>{ reactions.laugh }</span></span>
|
||||
<span if={ reactions.hmm }><mk-reaction-icon reaction='hmm'/><span>{ reactions.hmm }</span></span>
|
||||
<span if={ reactions.surprise }><mk-reaction-icon reaction='surprise'/><span>{ reactions.surprise }</span></span>
|
||||
<span if={ reactions.congrats }><mk-reaction-icon reaction='congrats'/><span>{ reactions.congrats }</span></span>
|
||||
<span if={ reactions.angry }><mk-reaction-icon reaction='angry'/><span>{ reactions.angry }</span></span>
|
||||
<span if={ reactions.confused }><mk-reaction-icon reaction='confused'/><span>{ reactions.confused }</span></span>
|
||||
<span if={ reactions.pudding }><mk-reaction-icon reaction='pudding'/><span>{ reactions.pudding }</span></span>
|
||||
</virtual>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
border-top dashed 1px #eee
|
||||
border-bottom dashed 1px #eee
|
||||
margin 4px 0
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> span
|
||||
margin-right 8px
|
||||
|
||||
> mk-reaction-icon
|
||||
font-size 1.4em
|
||||
|
||||
> span
|
||||
margin-left 4px
|
||||
font-size 1.2em
|
||||
color #444
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.post = this.opts.post;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.update();
|
||||
});
|
||||
|
||||
this.on('update', () => {
|
||||
this.reactions = this.post.reaction_counts;
|
||||
});
|
||||
</script>
|
||||
</mk-reactions-viewer>
|
|
@ -1,155 +0,0 @@
|
|||
<mk-signin>
|
||||
<form class={ signing: signing } onsubmit={ onsubmit }>
|
||||
<label class="user-name">
|
||||
<input ref="username" type="text" pattern="^[a-zA-Z0-9-]+$" placeholder="%i18n:common.tags.mk-signin.username%" autofocus="autofocus" required="required" oninput={ oninput }/>%fa:at%
|
||||
</label>
|
||||
<label class="password">
|
||||
<input ref="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required="required"/>%fa:lock%
|
||||
</label>
|
||||
<label class="token" if={ user && user.two_factor_enabled }>
|
||||
<input ref="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required="required"/>%fa:lock%
|
||||
</label>
|
||||
<button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button>
|
||||
</form>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> form
|
||||
display block
|
||||
z-index 2
|
||||
|
||||
&.signing
|
||||
&, *
|
||||
cursor wait !important
|
||||
|
||||
label
|
||||
display block
|
||||
margin 12px 0
|
||||
|
||||
[data-fa]
|
||||
display block
|
||||
pointer-events none
|
||||
position absolute
|
||||
bottom 0
|
||||
top 0
|
||||
left 0
|
||||
z-index 1
|
||||
margin auto
|
||||
padding 0 16px
|
||||
height 1em
|
||||
color #898786
|
||||
|
||||
input[type=text]
|
||||
input[type=password]
|
||||
input[type=number]
|
||||
user-select text
|
||||
display inline-block
|
||||
cursor auto
|
||||
padding 0 0 0 38px
|
||||
margin 0
|
||||
width 100%
|
||||
line-height 44px
|
||||
font-size 1em
|
||||
color rgba(0, 0, 0, 0.7)
|
||||
background #fff
|
||||
outline none
|
||||
border solid 1px #eee
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
background rgba(255, 255, 255, 0.7)
|
||||
border-color #ddd
|
||||
|
||||
& + i
|
||||
color #797776
|
||||
|
||||
&:focus
|
||||
background #fff
|
||||
border-color #ccc
|
||||
|
||||
& + i
|
||||
color #797776
|
||||
|
||||
[type=submit]
|
||||
cursor pointer
|
||||
padding 16px
|
||||
margin -6px 0 0 0
|
||||
width 100%
|
||||
font-size 1.2em
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
outline none
|
||||
border none
|
||||
border-radius 0
|
||||
background transparent
|
||||
transition all .5s ease
|
||||
|
||||
&:hover
|
||||
color $theme-color
|
||||
transition all .2s ease
|
||||
|
||||
&:focus
|
||||
color $theme-color
|
||||
transition all .2s ease
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 30%)
|
||||
transition all .2s ease
|
||||
|
||||
&:disabled
|
||||
opacity 0.7
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('api');
|
||||
|
||||
this.user = null;
|
||||
this.signing = false;
|
||||
|
||||
this.oninput = () => {
|
||||
this.api('users/show', {
|
||||
username: this.refs.username.value
|
||||
}).then(user => {
|
||||
this.user = user;
|
||||
this.trigger('user', user);
|
||||
this.update();
|
||||
});
|
||||
};
|
||||
|
||||
this.onsubmit = e => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.refs.username.value == '') {
|
||||
this.refs.username.focus();
|
||||
return false;
|
||||
}
|
||||
if (this.refs.password.value == '') {
|
||||
this.refs.password.focus();
|
||||
return false;
|
||||
}
|
||||
if (this.user && this.user.two_factor_enabled && this.refs.token.value == '') {
|
||||
this.refs.token.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.update({
|
||||
signing: true
|
||||
});
|
||||
|
||||
this.api('signin', {
|
||||
username: this.refs.username.value,
|
||||
password: this.refs.password.value,
|
||||
token: this.user && this.user.two_factor_enabled ? this.refs.token.value : undefined
|
||||
}).then(() => {
|
||||
location.reload();
|
||||
}).catch(() => {
|
||||
alert('something happened');
|
||||
this.update({
|
||||
signing: false
|
||||
});
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
</mk-signin>
|
|
@ -1,307 +0,0 @@
|
|||
<mk-signup>
|
||||
<form onsubmit={ onsubmit } autocomplete="off">
|
||||
<label class="username">
|
||||
<p class="caption">%fa:at%%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' }>{ _URL_ + '/' + refs.username.value }</p>
|
||||
<p class="info" if={ usernameState == 'wait' } style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p>
|
||||
<p class="info" if={ usernameState == 'ok' } style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p>
|
||||
<p class="info" if={ usernameState == 'unavailable' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p>
|
||||
<p class="info" if={ usernameState == 'error' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.error%</p>
|
||||
<p class="info" if={ usernameState == 'invalid-format' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.invalid-format%</p>
|
||||
<p class="info" if={ usernameState == 'min-range' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-short%</p>
|
||||
<p class="info" if={ usernameState == 'max-range' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-long%</p>
|
||||
</label>
|
||||
<label class="password">
|
||||
<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%</p>
|
||||
<input ref="password" type="password" placeholder="%i18n:common.tags.mk-signup.password-placeholder%" autocomplete="off" required="required" onkeyup={ onChangePassword }/>
|
||||
<div class="meter" if={ passwordStrength != '' } data-strength={ passwordStrength }>
|
||||
<div class="value" ref="passwordMetar"></div>
|
||||
</div>
|
||||
<p class="info" if={ passwordStrength == 'low' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.weak-password%</p>
|
||||
<p class="info" if={ passwordStrength == 'medium' } style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.normal-password%</p>
|
||||
<p class="info" if={ passwordStrength == 'high' } style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.strong-password%</p>
|
||||
</label>
|
||||
<label class="retype-password">
|
||||
<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%(%i18n:common.tags.mk-signup.retype%)</p>
|
||||
<input ref="passwordRetype" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required="required" onkeyup={ onChangePasswordRetype }/>
|
||||
<p class="info" if={ passwordRetypeState == 'match' } style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.password-matched%</p>
|
||||
<p class="info" if={ passwordRetypeState == 'not-match' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.password-not-matched%</p>
|
||||
</label>
|
||||
<label class="recaptcha">
|
||||
<p class="caption"><virtual if={ recaptchaed }>%fa:toggle-on%</virtual><virtual if={ !recaptchaed }>%fa:toggle-off%</virtual>%i18n:common.tags.mk-signup.recaptcha%</p>
|
||||
<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"/>
|
||||
<p><a href={ touUrl } target="_blank">利用規約</a>に同意する</p>
|
||||
</label>
|
||||
<button onclick={ onsubmit }>%i18n:common.tags.mk-signup.create%</button>
|
||||
</form>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
min-width 302px
|
||||
overflow hidden
|
||||
|
||||
> form
|
||||
|
||||
label
|
||||
display block
|
||||
margin 16px 0
|
||||
|
||||
> .caption
|
||||
margin 0 0 4px 0
|
||||
color #828888
|
||||
font-size 0.95em
|
||||
|
||||
> [data-fa]
|
||||
margin-right 0.25em
|
||||
color #96adac
|
||||
|
||||
> .info
|
||||
display block
|
||||
margin 4px 0
|
||||
font-size 0.8em
|
||||
|
||||
> [data-fa]
|
||||
margin-right 0.3em
|
||||
|
||||
&.username
|
||||
.profile-page-url-preview
|
||||
display block
|
||||
margin 4px 8px 0 4px
|
||||
font-size 0.8em
|
||||
color #888
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
&:not(:empty) + .info
|
||||
margin-top 0
|
||||
|
||||
&.password
|
||||
.meter
|
||||
display block
|
||||
margin-top 8px
|
||||
width 100%
|
||||
height 8px
|
||||
|
||||
&[data-strength='']
|
||||
display none
|
||||
|
||||
&[data-strength='low']
|
||||
> .value
|
||||
background #d73612
|
||||
|
||||
&[data-strength='medium']
|
||||
> .value
|
||||
background #d7ca12
|
||||
|
||||
&[data-strength='high']
|
||||
> .value
|
||||
background #61bb22
|
||||
|
||||
> .value
|
||||
display block
|
||||
width 0%
|
||||
height 100%
|
||||
background transparent
|
||||
border-radius 4px
|
||||
transition all 0.1s ease
|
||||
|
||||
[type=text], [type=password]
|
||||
user-select text
|
||||
display inline-block
|
||||
cursor auto
|
||||
padding 0 12px
|
||||
margin 0
|
||||
width 100%
|
||||
line-height 44px
|
||||
font-size 1em
|
||||
color #333 !important
|
||||
background #fff !important
|
||||
outline none
|
||||
border solid 1px rgba(0, 0, 0, 0.1)
|
||||
border-radius 4px
|
||||
box-shadow 0 0 0 114514px #fff inset
|
||||
transition all .3s ease
|
||||
|
||||
&:hover
|
||||
border-color rgba(0, 0, 0, 0.2)
|
||||
transition all .1s ease
|
||||
|
||||
&:focus
|
||||
color $theme-color !important
|
||||
border-color $theme-color
|
||||
box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
|
||||
transition all 0s ease
|
||||
|
||||
&:disabled
|
||||
opacity 0.5
|
||||
|
||||
.agree-tou
|
||||
padding 4px
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
background #f4f4f4
|
||||
|
||||
&:active
|
||||
background #eee
|
||||
|
||||
&, *
|
||||
cursor pointer
|
||||
|
||||
p
|
||||
display inline
|
||||
color #555
|
||||
|
||||
button
|
||||
margin 0 0 32px 0
|
||||
padding 16px
|
||||
width 100%
|
||||
font-size 1em
|
||||
color #fff
|
||||
background $theme-color
|
||||
border-radius 3px
|
||||
|
||||
&:hover
|
||||
background lighten($theme-color, 5%)
|
||||
|
||||
&:active
|
||||
background darken($theme-color, 5%)
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('api');
|
||||
const getPasswordStrength = require('syuilo-password-strength');
|
||||
|
||||
this.usernameState = null;
|
||||
this.passwordStrength = '';
|
||||
this.passwordRetypeState = null;
|
||||
this.recaptchaed = false;
|
||||
|
||||
this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`;
|
||||
|
||||
window.onRecaptchaed = () => {
|
||||
this.recaptchaed = true;
|
||||
this.update();
|
||||
};
|
||||
|
||||
window.onRecaptchaExpired = () => {
|
||||
this.recaptchaed = false;
|
||||
this.update();
|
||||
};
|
||||
|
||||
this.on('mount', () => {
|
||||
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 = () => {
|
||||
const username = this.refs.username.value;
|
||||
|
||||
if (username == '') {
|
||||
this.update({
|
||||
usernameState: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const err =
|
||||
!username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
|
||||
username.length < 3 ? 'min-range' :
|
||||
username.length > 20 ? 'max-range' :
|
||||
null;
|
||||
|
||||
if (err) {
|
||||
this.update({
|
||||
usernameState: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.update({
|
||||
usernameState: 'wait'
|
||||
});
|
||||
|
||||
this.api('username/available', {
|
||||
username: username
|
||||
}).then(result => {
|
||||
this.update({
|
||||
usernameState: result.available ? 'ok' : 'unavailable'
|
||||
});
|
||||
}).catch(err => {
|
||||
this.update({
|
||||
usernameState: 'error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.onChangePassword = () => {
|
||||
const password = this.refs.password.value;
|
||||
|
||||
if (password == '') {
|
||||
this.passwordStrength = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const strength = getPasswordStrength(password);
|
||||
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||
this.update();
|
||||
this.refs.passwordMetar.style.width = `${strength * 100}%`;
|
||||
};
|
||||
|
||||
this.onChangePasswordRetype = () => {
|
||||
const password = this.refs.password.value;
|
||||
const retypedPassword = this.refs.passwordRetype.value;
|
||||
|
||||
if (retypedPassword == '') {
|
||||
this.passwordRetypeState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.passwordRetypeState = password == retypedPassword ? 'match' : 'not-match';
|
||||
};
|
||||
|
||||
this.onsubmit = e => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = this.refs.username.value;
|
||||
const password = this.refs.password.value;
|
||||
|
||||
const locker = document.body.appendChild(document.createElement('mk-locker'));
|
||||
|
||||
this.api('signup', {
|
||||
username: username,
|
||||
password: password,
|
||||
'g-recaptcha-response': grecaptcha.getResponse()
|
||||
}).then(() => {
|
||||
this.api('signin', {
|
||||
username: username,
|
||||
password: password
|
||||
}).then(() => {
|
||||
location.href = '/';
|
||||
});
|
||||
}).catch(() => {
|
||||
alert('%i18n:common.tags.mk-signup.some-error%');
|
||||
|
||||
grecaptcha.reset();
|
||||
this.recaptchaed = false;
|
||||
|
||||
locker.parentNode.removeChild(locker);
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
</mk-signup>
|
|
@ -1,27 +0,0 @@
|
|||
<mk-special-message>
|
||||
<p if={ m == 1 && d == 1 }>%i18n:common.tags.mk-special-message.new-year%</p>
|
||||
<p if={ m == 12 && d == 25 }>%i18n:common.tags.mk-special-message.christmas%</p>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 4px
|
||||
text-align center
|
||||
font-size 14px
|
||||
font-weight bold
|
||||
text-transform uppercase
|
||||
color #fff
|
||||
background #ff1036
|
||||
|
||||
</style>
|
||||
<script>
|
||||
const now = new Date();
|
||||
this.d = now.getDate();
|
||||
this.m = now.getMonth() + 1;
|
||||
</script>
|
||||
</mk-special-message>
|
|
@ -1,78 +0,0 @@
|
|||
<mk-stream-indicator>
|
||||
<p if={ connection.state == 'initializing' }>
|
||||
%fa:spinner .pulse%
|
||||
<span>%i18n:common.tags.mk-stream-indicator.connecting%<mk-ellipsis/></span>
|
||||
</p>
|
||||
<p if={ connection.state == 'reconnecting' }>
|
||||
%fa:spinner .pulse%
|
||||
<span>%i18n:common.tags.mk-stream-indicator.reconnecting%<mk-ellipsis/></span>
|
||||
</p>
|
||||
<p if={ connection.state == 'connected' }>
|
||||
%fa:check%
|
||||
<span>%i18n:common.tags.mk-stream-indicator.connected%</span>
|
||||
</p>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
pointer-events none
|
||||
position fixed
|
||||
z-index 16384
|
||||
bottom 8px
|
||||
right 8px
|
||||
margin 0
|
||||
padding 6px 12px
|
||||
font-size 0.9em
|
||||
color #fff
|
||||
background rgba(0, 0, 0, 0.8)
|
||||
border-radius 4px
|
||||
|
||||
> p
|
||||
display block
|
||||
margin 0
|
||||
|
||||
> [data-fa]
|
||||
margin-right 0.25em
|
||||
|
||||
</style>
|
||||
<script>
|
||||
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.connection.state == 'connected') {
|
||||
this.root.style.opacity = 0;
|
||||
}
|
||||
|
||||
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: 1,
|
||||
easing: 'linear',
|
||||
duration: 100
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.stream.dispose(this.connectionId);
|
||||
});
|
||||
</script>
|
||||
</mk-stream-indicator>
|
|
@ -1,50 +0,0 @@
|
|||
<mk-time>
|
||||
<time datetime={ opts.time }>
|
||||
<span if={ mode == 'relative' }>{ relative }</span>
|
||||
<span if={ mode == 'absolute' }>{ absolute }</span>
|
||||
<span if={ mode == 'detail' }>{ absolute } ({ relative })</span>
|
||||
</time>
|
||||
<script>
|
||||
this.time = new Date(this.opts.time);
|
||||
this.mode = this.opts.mode || 'relative';
|
||||
this.tickid = null;
|
||||
|
||||
this.absolute =
|
||||
this.time.getFullYear() + '年' +
|
||||
(this.time.getMonth() + 1) + '月' +
|
||||
this.time.getDate() + '日' +
|
||||
' ' +
|
||||
this.time.getHours() + '時' +
|
||||
this.time.getMinutes() + '分';
|
||||
|
||||
this.on('mount', () => {
|
||||
if (this.mode == 'relative' || this.mode == 'detail') {
|
||||
this.tick();
|
||||
this.tickid = setInterval(this.tick, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
if (this.mode === 'relative' || this.mode === 'detail') {
|
||||
clearInterval(this.tickid);
|
||||
}
|
||||
});
|
||||
|
||||
this.tick = () => {
|
||||
const now = new Date();
|
||||
const ago = (now - this.time) / 1000/*ms*/;
|
||||
this.relative =
|
||||
ago >= 31536000 ? '%i18n:common.time.years_ago%' .replace('{}', ~~(ago / 31536000)) :
|
||||
ago >= 2592000 ? '%i18n:common.time.months_ago%' .replace('{}', ~~(ago / 2592000)) :
|
||||
ago >= 604800 ? '%i18n:common.time.weeks_ago%' .replace('{}', ~~(ago / 604800)) :
|
||||
ago >= 86400 ? '%i18n:common.time.days_ago%' .replace('{}', ~~(ago / 86400)) :
|
||||
ago >= 3600 ? '%i18n:common.time.hours_ago%' .replace('{}', ~~(ago / 3600)) :
|
||||
ago >= 60 ? '%i18n:common.time.minutes_ago%'.replace('{}', ~~(ago / 60)) :
|
||||
ago >= 10 ? '%i18n:common.time.seconds_ago%'.replace('{}', ~~(ago % 60)) :
|
||||
ago >= 0 ? '%i18n:common.time.just_now%' :
|
||||
ago < 0 ? '%i18n:common.time.future%' :
|
||||
'%i18n:common.time.unknown%';
|
||||
this.update();
|
||||
};
|
||||
</script>
|
||||
</mk-time>
|
|
@ -1,62 +0,0 @@
|
|||
<mk-twitter-setting>
|
||||
<p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _DOCS_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={ _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={ _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>
|
||||
:scope
|
||||
display block
|
||||
color #4a535a
|
||||
|
||||
.account
|
||||
border solid 1px #e1e8ed
|
||||
border-radius 4px
|
||||
padding 16px
|
||||
|
||||
a
|
||||
font-weight bold
|
||||
color inherit
|
||||
|
||||
.id
|
||||
color #8899a6
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('i');
|
||||
|
||||
this.form = null;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.I.on('updated', this.onMeUpdated);
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.I.off('updated', this.onMeUpdated);
|
||||
});
|
||||
|
||||
this.onMeUpdated = () => {
|
||||
if (this.I.twitter) {
|
||||
if (this.form) this.form.close();
|
||||
}
|
||||
};
|
||||
|
||||
this.connect = e => {
|
||||
e.preventDefault();
|
||||
this.form = window.open(_API_URL_ + '/connect/twitter',
|
||||
'twitter_connect_window',
|
||||
'height=570,width=520');
|
||||
return false;
|
||||
};
|
||||
|
||||
this.disconnect = e => {
|
||||
e.preventDefault();
|
||||
window.open(_API_URL_ + '/disconnect/twitter',
|
||||
'twitter_disconnect_window',
|
||||
'height=570,width=520');
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
</mk-twitter-setting>
|
|
@ -1,199 +0,0 @@
|
|||
<mk-uploader>
|
||||
<ol if={ uploads.length > 0 }>
|
||||
<li each={ uploads }>
|
||||
<div class="img" style="background-image: url({ img })"></div>
|
||||
<p class="name">%fa:spinner .pulse%{ name }</p>
|
||||
<p class="status"><span class="initing" if={ progress == undefined }>%i18n:common.tags.mk-uploader.waiting%<mk-ellipsis/></span><span class="kb" if={ progress != undefined }>{ String(Math.floor(progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i> / { String(Math.floor(progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i></span><span class="percentage" if={ progress != undefined }>{ Math.floor((progress.value / progress.max) * 100) }</span></p>
|
||||
<progress if={ progress != undefined && progress.value != progress.max } value={ progress.value } max={ progress.max }></progress>
|
||||
<div class="progress initing" if={ progress == undefined }></div>
|
||||
<div class="progress waiting" if={ progress != undefined && progress.value == progress.max }></div>
|
||||
</li>
|
||||
</ol>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
overflow auto
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> ol
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
> li
|
||||
display block
|
||||
margin 8px 0 0 0
|
||||
padding 0
|
||||
height 36px
|
||||
box-shadow 0 -1px 0 rgba($theme-color, 0.1)
|
||||
border-top solid 8px transparent
|
||||
|
||||
&:first-child
|
||||
margin 0
|
||||
box-shadow none
|
||||
border-top none
|
||||
|
||||
> .img
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 36px
|
||||
height 36px
|
||||
background-size cover
|
||||
background-position center center
|
||||
|
||||
> .name
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 44px
|
||||
margin 0
|
||||
padding 0
|
||||
max-width 256px
|
||||
font-size 0.8em
|
||||
color rgba($theme-color, 0.7)
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .status
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 0.8em
|
||||
|
||||
> .initing
|
||||
color rgba($theme-color, 0.5)
|
||||
|
||||
> .kb
|
||||
color rgba($theme-color, 0.5)
|
||||
|
||||
> .percentage
|
||||
display inline-block
|
||||
width 48px
|
||||
text-align right
|
||||
|
||||
color rgba($theme-color, 0.7)
|
||||
|
||||
&:after
|
||||
content '%'
|
||||
|
||||
> progress
|
||||
display block
|
||||
position absolute
|
||||
bottom 0
|
||||
right 0
|
||||
margin 0
|
||||
width calc(100% - 44px)
|
||||
height 8px
|
||||
background transparent
|
||||
border none
|
||||
border-radius 4px
|
||||
overflow hidden
|
||||
|
||||
&::-webkit-progress-value
|
||||
background $theme-color
|
||||
|
||||
&::-webkit-progress-bar
|
||||
background rgba($theme-color, 0.1)
|
||||
|
||||
> .progress
|
||||
display block
|
||||
position absolute
|
||||
bottom 0
|
||||
right 0
|
||||
margin 0
|
||||
width calc(100% - 44px)
|
||||
height 8px
|
||||
border none
|
||||
border-radius 4px
|
||||
background linear-gradient(
|
||||
45deg,
|
||||
lighten($theme-color, 30%) 25%,
|
||||
$theme-color 25%,
|
||||
$theme-color 50%,
|
||||
lighten($theme-color, 30%) 50%,
|
||||
lighten($theme-color, 30%) 75%,
|
||||
$theme-color 75%,
|
||||
$theme-color
|
||||
)
|
||||
background-size 32px 32px
|
||||
animation bg 1.5s linear infinite
|
||||
|
||||
&.initing
|
||||
opacity 0.3
|
||||
|
||||
@keyframes bg
|
||||
from {background-position: 0 0;}
|
||||
to {background-position: -64px 32px;}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('i');
|
||||
|
||||
this.uploads = [];
|
||||
|
||||
this.upload = (file, folder) => {
|
||||
if (folder && typeof folder == 'object') folder = folder.id;
|
||||
|
||||
const id = Math.random();
|
||||
|
||||
const ctx = {
|
||||
id: id,
|
||||
name: file.name || 'untitled',
|
||||
progress: undefined
|
||||
};
|
||||
|
||||
this.uploads.push(ctx);
|
||||
this.trigger('change-uploads', this.uploads);
|
||||
this.update();
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
ctx.img = e.target.result;
|
||||
this.update();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
const data = new FormData();
|
||||
data.append('i', this.I.token);
|
||||
data.append('file', file);
|
||||
|
||||
if (folder) data.append('folder_id', folder);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', _API_URL_ + '/drive/files/create', true);
|
||||
xhr.onload = e => {
|
||||
const driveFile = JSON.parse(e.target.response);
|
||||
|
||||
this.trigger('uploaded', driveFile);
|
||||
|
||||
this.uploads = this.uploads.filter(x => x.id != id);
|
||||
this.trigger('change-uploads', this.uploads);
|
||||
|
||||
this.update();
|
||||
};
|
||||
|
||||
xhr.upload.onprogress = e => {
|
||||
if (e.lengthComputable) {
|
||||
if (ctx.progress == undefined) ctx.progress = {};
|
||||
ctx.progress.max = e.total;
|
||||
ctx.progress.value = e.loaded;
|
||||
this.update();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(data);
|
||||
};
|
||||
</script>
|
||||
</mk-uploader>
|
|
@ -1,117 +0,0 @@
|
|||
<mk-url-preview>
|
||||
<a href={ url } target="_blank" title={ url } if={ !loading }>
|
||||
<div class="thumbnail" if={ thumbnail } style={ 'background-image: url(' + thumbnail + ')' }></div>
|
||||
<article>
|
||||
<header>
|
||||
<h1>{ title }</h1>
|
||||
</header>
|
||||
<p>{ description }</p>
|
||||
<footer>
|
||||
<img class="icon" if={ icon } src={ icon }/>
|
||||
<p>{ sitename }</p>
|
||||
</footer>
|
||||
</article>
|
||||
</a>
|
||||
<style>
|
||||
:scope
|
||||
display block
|
||||
font-size 16px
|
||||
|
||||
> a
|
||||
display block
|
||||
border solid 1px #eee
|
||||
border-radius 4px
|
||||
overflow hidden
|
||||
|
||||
&:hover
|
||||
text-decoration none
|
||||
border-color #ddd
|
||||
|
||||
> article > header > h1
|
||||
text-decoration underline
|
||||
|
||||
> .thumbnail
|
||||
position absolute
|
||||
width 100px
|
||||
height 100%
|
||||
background-position center
|
||||
background-size cover
|
||||
|
||||
& + article
|
||||
left 100px
|
||||
width calc(100% - 100px)
|
||||
|
||||
> article
|
||||
padding 16px
|
||||
|
||||
> header
|
||||
margin-bottom 8px
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
font-size 1em
|
||||
color #555
|
||||
|
||||
> p
|
||||
margin 0
|
||||
color #777
|
||||
font-size 0.8em
|
||||
|
||||
> footer
|
||||
margin-top 8px
|
||||
height 16px
|
||||
|
||||
> img
|
||||
display inline-block
|
||||
width 16px
|
||||
height 16px
|
||||
margin-right 4px
|
||||
vertical-align top
|
||||
|
||||
> p
|
||||
display inline-block
|
||||
margin 0
|
||||
color #666
|
||||
font-size 0.8em
|
||||
line-height 16px
|
||||
vertical-align top
|
||||
|
||||
@media (max-width 500px)
|
||||
font-size 8px
|
||||
|
||||
> a
|
||||
border none
|
||||
|
||||
> .thumbnail
|
||||
width 70px
|
||||
|
||||
& + article
|
||||
left 70px
|
||||
width calc(100% - 70px)
|
||||
|
||||
> article
|
||||
padding 8px
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.mixin('api');
|
||||
|
||||
this.url = this.opts.url;
|
||||
this.loading = true;
|
||||
|
||||
this.on('mount', () => {
|
||||
fetch('/api:url?url=' + this.url).then(res => {
|
||||
res.json().then(info => {
|
||||
this.title = info.title;
|
||||
this.description = info.description;
|
||||
this.thumbnail = info.thumbnail;
|
||||
this.icon = info.icon;
|
||||
this.sitename = info.sitename;
|
||||
|
||||
this.loading = false;
|
||||
this.update();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</mk-url-preview>
|
|
@ -1,54 +0,0 @@
|
|||
<mk-url>
|
||||
<a href={ url } target={ opts.target }>
|
||||
<span class="schema">{ schema }//</span>
|
||||
<span class="hostname">{ hostname }</span>
|
||||
<span class="port" if={ port != '' }>:{ port }</span>
|
||||
<span class="pathname" if={ pathname != '' }>{ pathname }</span>
|
||||
<span class="query">{ query }</span>
|
||||
<span class="hash">{ hash }</span>
|
||||
%fa:external-link-square-alt%
|
||||
</a>
|
||||
<style>
|
||||
:scope
|
||||
word-break break-all
|
||||
|
||||
> a
|
||||
> [data-fa]
|
||||
padding-left 2px
|
||||
font-size .9em
|
||||
font-weight 400
|
||||
font-style normal
|
||||
|
||||
> .schema
|
||||
opacity 0.5
|
||||
|
||||
> .hostname
|
||||
font-weight bold
|
||||
|
||||
> .pathname
|
||||
opacity 0.8
|
||||
|
||||
> .query
|
||||
opacity 0.5
|
||||
|
||||
> .hash
|
||||
font-style italic
|
||||
|
||||
</style>
|
||||
<script>
|
||||
this.url = this.opts.href;
|
||||
|
||||
this.on('before-mount', () => {
|
||||
const url = new URL(this.url);
|
||||
|
||||
this.schema = url.protocol;
|
||||
this.hostname = url.hostname;
|
||||
this.port = url.port;
|
||||
this.pathname = url.pathname;
|
||||
this.query = url.search;
|
||||
this.hash = url.hash;
|
||||
|
||||
this.update();
|
||||
});
|
||||
</script>
|
||||
</mk-url>
|
|
@ -0,0 +1,137 @@
|
|||
<template>
|
||||
<div class="troubleshooter">
|
||||
<h1>%fa:wrench%%i18n:common.tags.mk-error.troubleshooter.title%</h1>
|
||||
<div>
|
||||
<p :data-wip="network == null">
|
||||
<template v-if="network != null">
|
||||
<template v-if="network">%fa:check%</template>
|
||||
<template v-if="!network">%fa:times%</template>
|
||||
</template>
|
||||
{{ network == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-network%' : '%i18n:common.tags.mk-error.troubleshooter.network%' }}<mk-ellipsis v-if="network == null"/>
|
||||
</p>
|
||||
<p v-if="network == true" :data-wip="internet == null">
|
||||
<template v-if="internet != null">
|
||||
<template v-if="internet">%fa:check%</template>
|
||||
<template v-if="!internet">%fa:times%</template>
|
||||
</template>
|
||||
{{ internet == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-internet%' : '%i18n:common.tags.mk-error.troubleshooter.internet%' }}<mk-ellipsis v-if="internet == null"/>
|
||||
</p>
|
||||
<p v-if="internet == true" :data-wip="server == null">
|
||||
<template v-if="server != null">
|
||||
<template v-if="server">%fa:check%</template>
|
||||
<template v-if="!server">%fa:times%</template>
|
||||
</template>
|
||||
{{ server == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-server%' : '%i18n:common.tags.mk-error.troubleshooter.server%' }}<mk-ellipsis v-if="server == null"/>
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="!end">%i18n:common.tags.mk-error.troubleshooter.finding%<mk-ellipsis/></p>
|
||||
<p v-if="network === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-network%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-network-desc%</p>
|
||||
<p v-if="internet === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-internet%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-internet-desc%</p>
|
||||
<p v-if="server === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-server%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-server-desc%</p>
|
||||
<p v-if="server === true" class="success"><b>%fa:info-circle%%i18n:common.tags.mk-error.troubleshooter.success%</b><br>%i18n:common.tags.mk-error.troubleshooter.success-desc%</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { apiUrl } from '../../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
network: navigator.onLine,
|
||||
end: false,
|
||||
internet: null,
|
||||
server: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (!this.network) {
|
||||
this.end = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check internet connection
|
||||
fetch('https://google.com?rand=' + Math.random(), {
|
||||
mode: 'no-cors'
|
||||
}).then(() => {
|
||||
this.internet = true;
|
||||
|
||||
// Check misskey server is available
|
||||
fetch(`${apiUrl}/meta`).then(() => {
|
||||
this.end = true;
|
||||
this.server = true;
|
||||
})
|
||||
.catch(() => {
|
||||
this.end = true;
|
||||
this.server = false;
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.end = true;
|
||||
this.internet = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.troubleshooter
|
||||
width 100%
|
||||
max-width 500px
|
||||
text-align left
|
||||
background #fff
|
||||
border-radius 8px
|
||||
border solid 1px #ddd
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
padding 0.6em 1.2em
|
||||
font-size 1em
|
||||
color #444
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
> [data-fa]
|
||||
margin-right 0.25em
|
||||
|
||||
> div
|
||||
overflow hidden
|
||||
padding 0.6em 1.2em
|
||||
|
||||
> p
|
||||
margin 0.5em 0
|
||||
font-size 0.9em
|
||||
color #444
|
||||
|
||||
&[data-wip]
|
||||
color #888
|
||||
|
||||
> [data-fa]
|
||||
margin-right 0.25em
|
||||
|
||||
&.times
|
||||
color #e03524
|
||||
|
||||
&.check
|
||||
color #84c32f
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 0.6em 1.2em
|
||||
font-size 1em
|
||||
color #444
|
||||
border-top solid 1px #eee
|
||||
|
||||
> b
|
||||
> [data-fa]
|
||||
margin-right 0.25em
|
||||
|
||||
&.success
|
||||
> b
|
||||
color #39adad
|
||||
|
||||
&:not(.success)
|
||||
> b
|
||||
color #ad4339
|
||||
|
||||
</style>
|
104
src/web/app/common/views/components/connect-failed.vue
Normal file
104
src/web/app/common/views/components/connect-failed.vue
Normal file
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<div class="mk-connect-failed">
|
||||
<img src="data:image/jpeg;base64,%base64:/assets/error.jpg%" alt=""/>
|
||||
<h1>%i18n:common.tags.mk-error.title%</h1>
|
||||
<p class="text">
|
||||
{{ '%i18n:common.tags.mk-error.description%'.substr(0, '%i18n:common.tags.mk-error.description%'.indexOf('{')) }}
|
||||
<a @click="reload">{{ '%i18n:common.tags.mk-error.description%'.match(/\{(.+?)\}/)[1] }}</a>
|
||||
{{ '%i18n:common.tags.mk-error.description%'.substr('%i18n:common.tags.mk-error.description%'.indexOf('}') + 1) }}
|
||||
</p>
|
||||
<button v-if="!troubleshooting" @click="troubleshooting = true">%i18n:common.tags.mk-error.troubleshoot%</button>
|
||||
<x-troubleshooter v-if="troubleshooting"/>
|
||||
<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XTroubleshooter from './connect-failed.troubleshooter.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XTroubleshooter
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
troubleshooting: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.title = 'Oops!';
|
||||
document.documentElement.style.background = '#f8f8f8';
|
||||
},
|
||||
methods: {
|
||||
reload() {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-connect-failed
|
||||
width 100%
|
||||
padding 32px 18px
|
||||
text-align center
|
||||
|
||||
> img
|
||||
display block
|
||||
height 200px
|
||||
margin 0 auto
|
||||
pointer-events none
|
||||
user-select none
|
||||
|
||||
> h1
|
||||
display block
|
||||
margin 1.25em auto 0.65em auto
|
||||
font-size 1.5em
|
||||
color #555
|
||||
|
||||
> .text
|
||||
display block
|
||||
margin 0 auto
|
||||
max-width 600px
|
||||
font-size 1em
|
||||
color #666
|
||||
|
||||
> button
|
||||
display block
|
||||
margin 1em auto 0 auto
|
||||
padding 8px 10px
|
||||
color $theme-color-foreground
|
||||
background $theme-color
|
||||
|
||||
&:focus
|
||||
outline solid 3px rgba($theme-color, 0.3)
|
||||
|
||||
&:hover
|
||||
background lighten($theme-color, 10%)
|
||||
|
||||
&:active
|
||||
background darken($theme-color, 10%)
|
||||
|
||||
> .troubleshooter
|
||||
margin 1em auto 0 auto
|
||||
|
||||
> .thanks
|
||||
display block
|
||||
margin 2em auto 0 auto
|
||||
padding 2em 0 0 0
|
||||
max-width 600px
|
||||
font-size 0.9em
|
||||
font-style oblique
|
||||
color #aaa
|
||||
border-top solid 1px #eee
|
||||
|
||||
@media (max-width 500px)
|
||||
padding 24px 18px
|
||||
font-size 80%
|
||||
|
||||
> img
|
||||
height 150px
|
||||
|
||||
</style>
|
||||
|
26
src/web/app/common/views/components/ellipsis.vue
Normal file
26
src/web/app/common/views/components/ellipsis.vue
Normal file
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<span class="mk-ellipsis">
|
||||
<span>.</span><span>.</span><span>.</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-ellipsis
|
||||
> span
|
||||
animation ellipsis 1.4s infinite ease-in-out both
|
||||
|
||||
&:nth-child(1)
|
||||
animation-delay 0s
|
||||
|
||||
&:nth-child(2)
|
||||
animation-delay 0.16s
|
||||
|
||||
&:nth-child(3)
|
||||
animation-delay 0.32s
|
||||
|
||||
@keyframes ellipsis
|
||||
0%, 80%, 100%
|
||||
opacity 1
|
||||
40%
|
||||
opacity 0
|
||||
</style>
|
17
src/web/app/common/views/components/file-type-icon.vue
Normal file
17
src/web/app/common/views/components/file-type-icon.vue
Normal file
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<span>
|
||||
<template v-if="kind == 'image'">%fa:file-image%</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: ['type'],
|
||||
computed: {
|
||||
kind(): string {
|
||||
return this.type.split('/')[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
40
src/web/app/common/views/components/forkit.vue
Normal file
40
src/web/app/common/views/components/forkit.vue
Normal file
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<a class="a" href="https://github.com/syuilo/misskey" target="_blank" title="%i18n:common.tags.mk-forkit.open-github-link%" aria-label="%i18n:common.tags.mk-forkit.open-github-link%">
|
||||
<svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden">
|
||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||
<path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path>
|
||||
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.a
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
|
||||
> svg
|
||||
display block
|
||||
//fill #151513
|
||||
//color #fff
|
||||
fill $theme-color
|
||||
color $theme-color-foreground
|
||||
|
||||
.octo-arm
|
||||
transform-origin 130px 106px
|
||||
|
||||
&:hover
|
||||
.octo-arm
|
||||
animation octocat-wave 560ms ease-in-out
|
||||
|
||||
@keyframes octocat-wave
|
||||
0%, 100%
|
||||
transform rotate(0)
|
||||
20%, 60%
|
||||
transform rotate(-25deg)
|
||||
40%, 80%
|
||||
transform rotate(10deg)
|
||||
|
||||
</style>
|
63
src/web/app/common/views/components/images.vue
Normal file
63
src/web/app/common/views/components/images.vue
Normal file
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div class="mk-images">
|
||||
<mk-images-image v-for="image in images" ref="image" :image="image" :key="image.id"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['images'],
|
||||
mounted() {
|
||||
const tags = this.$refs.image as Vue[];
|
||||
|
||||
if (this.images.length == 1) {
|
||||
(this.$el.style as any).gridTemplateRows = '1fr';
|
||||
|
||||
(tags[0].$el.style as any).gridColumn = '1 / 2';
|
||||
(tags[0].$el.style as any).gridRow = '1 / 2';
|
||||
} else if (this.images.length == 2) {
|
||||
(this.$el.style as any).gridTemplateColumns = '1fr 1fr';
|
||||
(this.$el.style as any).gridTemplateRows = '1fr';
|
||||
|
||||
(tags[0].$el.style as any).gridColumn = '1 / 2';
|
||||
(tags[0].$el.style as any).gridRow = '1 / 2';
|
||||
(tags[1].$el.style as any).gridColumn = '2 / 3';
|
||||
(tags[1].$el.style as any).gridRow = '1 / 2';
|
||||
} else if (this.images.length == 3) {
|
||||
(this.$el.style as any).gridTemplateColumns = '1fr 0.5fr';
|
||||
(this.$el.style as any).gridTemplateRows = '1fr 1fr';
|
||||
|
||||
(tags[0].$el.style as any).gridColumn = '1 / 2';
|
||||
(tags[0].$el.style as any).gridRow = '1 / 3';
|
||||
(tags[1].$el.style as any).gridColumn = '2 / 3';
|
||||
(tags[1].$el.style as any).gridRow = '1 / 2';
|
||||
(tags[2].$el.style as any).gridColumn = '2 / 3';
|
||||
(tags[2].$el.style as any).gridRow = '2 / 3';
|
||||
} else if (this.images.length == 4) {
|
||||
(this.$el.style as any).gridTemplateColumns = '1fr 1fr';
|
||||
(this.$el.style as any).gridTemplateRows = '1fr 1fr';
|
||||
|
||||
(tags[0].$el.style as any).gridColumn = '1 / 2';
|
||||
(tags[0].$el.style as any).gridRow = '1 / 2';
|
||||
(tags[1].$el.style as any).gridColumn = '2 / 3';
|
||||
(tags[1].$el.style as any).gridRow = '1 / 2';
|
||||
(tags[2].$el.style as any).gridColumn = '1 / 2';
|
||||
(tags[2].$el.style as any).gridRow = '2 / 3';
|
||||
(tags[3].$el.style as any).gridColumn = '2 / 3';
|
||||
(tags[3].$el.style as any).gridRow = '2 / 3';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-images
|
||||
display grid
|
||||
grid-gap 4px
|
||||
height 256px
|
||||
|
||||
@media (max-width 500px)
|
||||
height 192px
|
||||
</style>
|
43
src/web/app/common/views/components/index.ts
Normal file
43
src/web/app/common/views/components/index.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import signin from './signin.vue';
|
||||
import signup from './signup.vue';
|
||||
import forkit from './forkit.vue';
|
||||
import nav from './nav.vue';
|
||||
import postHtml from './post-html';
|
||||
import poll from './poll.vue';
|
||||
import pollEditor from './poll-editor.vue';
|
||||
import reactionIcon from './reaction-icon.vue';
|
||||
import reactionsViewer from './reactions-viewer.vue';
|
||||
import time from './time.vue';
|
||||
import images from './images.vue';
|
||||
import uploader from './uploader.vue';
|
||||
import specialMessage from './special-message.vue';
|
||||
import streamIndicator from './stream-indicator.vue';
|
||||
import ellipsis from './ellipsis.vue';
|
||||
import messaging from './messaging.vue';
|
||||
import messagingRoom from './messaging-room.vue';
|
||||
import urlPreview from './url-preview.vue';
|
||||
import twitterSetting from './twitter-setting.vue';
|
||||
import fileTypeIcon from './file-type-icon.vue';
|
||||
|
||||
Vue.component('mk-signin', signin);
|
||||
Vue.component('mk-signup', signup);
|
||||
Vue.component('mk-forkit', forkit);
|
||||
Vue.component('mk-nav', nav);
|
||||
Vue.component('mk-post-html', postHtml);
|
||||
Vue.component('mk-poll', poll);
|
||||
Vue.component('mk-poll-editor', pollEditor);
|
||||
Vue.component('mk-reaction-icon', reactionIcon);
|
||||
Vue.component('mk-reactions-viewer', reactionsViewer);
|
||||
Vue.component('mk-time', time);
|
||||
Vue.component('mk-images', images);
|
||||
Vue.component('mk-uploader', uploader);
|
||||
Vue.component('mk-special-message', specialMessage);
|
||||
Vue.component('mk-stream-indicator', streamIndicator);
|
||||
Vue.component('mk-ellipsis', ellipsis);
|
||||
Vue.component('mk-messaging', messaging);
|
||||
Vue.component('mk-messaging-room', messagingRoom);
|
||||
Vue.component('mk-url-preview', urlPreview);
|
||||
Vue.component('mk-twitter-setting', twitterSetting);
|
||||
Vue.component('mk-file-type-icon', fileTypeIcon);
|
186
src/web/app/common/views/components/messaging-room.form.vue
Normal file
186
src/web/app/common/views/components/messaging-room.form.vue
Normal file
|
@ -0,0 +1,186 @@
|
|||
<template>
|
||||
<div class="mk-messaging-form">
|
||||
<textarea v-model="text" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%"></textarea>
|
||||
<div class="file" v-if="file">{{ file.name }}</div>
|
||||
<mk-uploader ref="uploader"/>
|
||||
<button class="send" @click="send" :disabled="sending" title="%i18n:common.send%">
|
||||
<template v-if="!sending">%fa:paper-plane%</template><template v-if="sending">%fa:spinner .spin%</template>
|
||||
</button>
|
||||
<button class="attach-from-local" title="%i18n:common.tags.mk-messaging-form.attach-from-local%">
|
||||
%fa:upload%
|
||||
</button>
|
||||
<button class="attach-from-drive" @click="chooseFileFromDrive" title="%i18n:common.tags.mk-messaging-form.attach-from-drive%">
|
||||
%fa:R folder-open%
|
||||
</button>
|
||||
<input name="file" type="file" accept="image/*"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: ['user'],
|
||||
data() {
|
||||
return {
|
||||
text: null,
|
||||
file: null,
|
||||
sending: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onPaste(e) {
|
||||
const data = e.clipboardData;
|
||||
const items = data.items;
|
||||
for (const item of items) {
|
||||
if (item.kind == 'file') {
|
||||
//this.upload(item.getAsFile());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onKeypress(e) {
|
||||
if ((e.which == 10 || e.which == 13) && e.ctrlKey) {
|
||||
this.send();
|
||||
}
|
||||
},
|
||||
|
||||
chooseFile() {
|
||||
(this.$refs.file as any).click();
|
||||
},
|
||||
|
||||
chooseFileFromDrive() {
|
||||
(this as any).apis.chooseDriveFile({
|
||||
multiple: false
|
||||
}).then(file => {
|
||||
this.file = file;
|
||||
});
|
||||
},
|
||||
|
||||
upload() {
|
||||
// TODO
|
||||
},
|
||||
|
||||
send() {
|
||||
this.sending = true;
|
||||
(this as any).api('messaging/messages/create', {
|
||||
user_id: this.user.id,
|
||||
text: this.text
|
||||
}).then(message => {
|
||||
this.clear();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
}).then(() => {
|
||||
this.sending = false;
|
||||
});
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.text = '';
|
||||
this.file = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-messaging-form
|
||||
> textarea
|
||||
cursor auto
|
||||
display block
|
||||
width 100%
|
||||
min-width 100%
|
||||
max-width 100%
|
||||
height 64px
|
||||
margin 0
|
||||
padding 8px
|
||||
font-size 1em
|
||||
color #000
|
||||
outline none
|
||||
border none
|
||||
border-top solid 1px #eee
|
||||
border-radius 0
|
||||
box-shadow none
|
||||
background transparent
|
||||
|
||||
> .send
|
||||
position absolute
|
||||
bottom 0
|
||||
right 0
|
||||
margin 0
|
||||
padding 10px 14px
|
||||
line-height 1em
|
||||
font-size 1em
|
||||
color #aaa
|
||||
transition color 0.1s ease
|
||||
|
||||
&:hover
|
||||
color $theme-color
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 10%)
|
||||
transition color 0s ease
|
||||
|
||||
.files
|
||||
display block
|
||||
margin 0
|
||||
padding 0 8px
|
||||
list-style none
|
||||
|
||||
&:after
|
||||
content ''
|
||||
display block
|
||||
clear both
|
||||
|
||||
> li
|
||||
display block
|
||||
float left
|
||||
margin 4px
|
||||
padding 0
|
||||
width 64px
|
||||
height 64px
|
||||
background-color #eee
|
||||
background-repeat no-repeat
|
||||
background-position center center
|
||||
background-size cover
|
||||
cursor move
|
||||
|
||||
&:hover
|
||||
> .remove
|
||||
display block
|
||||
|
||||
> .remove
|
||||
display none
|
||||
position absolute
|
||||
right -6px
|
||||
top -6px
|
||||
margin 0
|
||||
padding 0
|
||||
background transparent
|
||||
outline none
|
||||
border none
|
||||
border-radius 0
|
||||
box-shadow none
|
||||
cursor pointer
|
||||
|
||||
.attach-from-local
|
||||
.attach-from-drive
|
||||
margin 0
|
||||
padding 10px 14px
|
||||
line-height 1em
|
||||
font-size 1em
|
||||
font-weight normal
|
||||
text-decoration none
|
||||
color #aaa
|
||||
transition color 0.1s ease
|
||||
|
||||
&:hover
|
||||
color $theme-color
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 10%)
|
||||
transition color 0s ease
|
||||
|
||||
input[type=file]
|
||||
display none
|
||||
|
||||
</style>
|
238
src/web/app/common/views/components/messaging-room.message.vue
Normal file
238
src/web/app/common/views/components/messaging-room.message.vue
Normal file
|
@ -0,0 +1,238 @@
|
|||
<template>
|
||||
<div class="message" :data-is-me="isMe">
|
||||
<a class="avatar-anchor" :href="`/${message.user.username}`" :title="message.user.username" target="_blank">
|
||||
<img class="avatar" :src="`${message.user.avatar_url}?thumbnail&size=80`" alt=""/>
|
||||
</a>
|
||||
<div class="content-container">
|
||||
<div class="balloon">
|
||||
<p class="read" v-if="isMe && message.is_read">%i18n:common.tags.mk-messaging-message.is-read%</p>
|
||||
<button class="delete-button" v-if="isMe" title="%i18n:common.delete%">
|
||||
<img src="/assets/desktop/messaging/delete.png" alt="Delete"/>
|
||||
</button>
|
||||
<div class="content" v-if="!message.is_deleted">
|
||||
<mk-post-html class="text" v-if="message.ast" :ast="message.ast" :i="os.i"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||
<div class="image" v-if="message.file">
|
||||
<img :src="message.file.url" alt="image" :title="message.file.name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" v-if="message.is_deleted">
|
||||
<p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<mk-time :time="message.created_at"/>
|
||||
<template v-if="message.is_edited">%fa:pencil-alt%</template>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['message'],
|
||||
computed: {
|
||||
isMe(): boolean {
|
||||
return this.message.user_id == (this as any).os.i.id;
|
||||
},
|
||||
urls(): string[] {
|
||||
if (this.message.ast) {
|
||||
return this.message.ast
|
||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
||||
.map(t => t.url);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.message
|
||||
$me-balloon-color = #23A7B6
|
||||
|
||||
padding 10px 12px 10px 12px
|
||||
background-color transparent
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> .avatar-anchor
|
||||
display block
|
||||
|
||||
> .avatar
|
||||
display block
|
||||
min-width 54px
|
||||
min-height 54px
|
||||
max-width 54px
|
||||
max-height 54px
|
||||
margin 0
|
||||
border-radius 8px
|
||||
transition all 0.1s ease
|
||||
|
||||
> .content-container
|
||||
display block
|
||||
margin 0 12px
|
||||
padding 0
|
||||
max-width calc(100% - 78px)
|
||||
|
||||
> .balloon
|
||||
display block
|
||||
float inherit
|
||||
margin 0
|
||||
padding 0
|
||||
max-width 100%
|
||||
min-height 38px
|
||||
border-radius 16px
|
||||
|
||||
&:before
|
||||
content ""
|
||||
pointer-events none
|
||||
display block
|
||||
position absolute
|
||||
top 12px
|
||||
|
||||
&:hover
|
||||
> .delete-button
|
||||
display block
|
||||
|
||||
> .delete-button
|
||||
display none
|
||||
position absolute
|
||||
z-index 1
|
||||
top -4px
|
||||
right -4px
|
||||
margin 0
|
||||
padding 0
|
||||
cursor pointer
|
||||
outline none
|
||||
border none
|
||||
border-radius 0
|
||||
box-shadow none
|
||||
background transparent
|
||||
|
||||
> img
|
||||
vertical-align bottom
|
||||
width 16px
|
||||
height 16px
|
||||
cursor pointer
|
||||
|
||||
> .read
|
||||
user-select none
|
||||
display block
|
||||
position absolute
|
||||
z-index 1
|
||||
bottom -4px
|
||||
left -12px
|
||||
margin 0
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
font-size 11px
|
||||
|
||||
> .content
|
||||
|
||||
> .is-deleted
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
overflow-wrap break-word
|
||||
font-size 1em
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
|
||||
> .text
|
||||
display block
|
||||
margin 0
|
||||
padding 8px 16px
|
||||
overflow hidden
|
||||
overflow-wrap break-word
|
||||
font-size 1em
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
|
||||
&, *
|
||||
user-select text
|
||||
cursor auto
|
||||
|
||||
& + .file
|
||||
&.image
|
||||
> img
|
||||
border-radius 0 0 16px 16px
|
||||
|
||||
> .file
|
||||
&.image
|
||||
> img
|
||||
display block
|
||||
max-width 100%
|
||||
max-height 512px
|
||||
border-radius 16px
|
||||
|
||||
> footer
|
||||
display block
|
||||
clear both
|
||||
margin 0
|
||||
padding 2px
|
||||
font-size 10px
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
> [data-fa]
|
||||
margin-left 4px
|
||||
|
||||
&:not([data-is-me])
|
||||
> .avatar-anchor
|
||||
float left
|
||||
|
||||
> .content-container
|
||||
float left
|
||||
|
||||
> .balloon
|
||||
background #eee
|
||||
|
||||
&:before
|
||||
left -14px
|
||||
border-top solid 8px transparent
|
||||
border-right solid 8px #eee
|
||||
border-bottom solid 8px transparent
|
||||
border-left solid 8px transparent
|
||||
|
||||
> footer
|
||||
text-align left
|
||||
|
||||
&[data-is-me]
|
||||
> .avatar-anchor
|
||||
float right
|
||||
|
||||
> .content-container
|
||||
float right
|
||||
|
||||
> .balloon
|
||||
background $me-balloon-color
|
||||
|
||||
&:before
|
||||
right -14px
|
||||
left auto
|
||||
border-top solid 8px transparent
|
||||
border-right solid 8px transparent
|
||||
border-bottom solid 8px transparent
|
||||
border-left solid 8px $me-balloon-color
|
||||
|
||||
> .content
|
||||
|
||||
> p.is-deleted
|
||||
color rgba(255, 255, 255, 0.5)
|
||||
|
||||
> .text
|
||||
&, *
|
||||
color #fff !important
|
||||
|
||||
> footer
|
||||
text-align right
|
||||
|
||||
&[data-is-deleted]
|
||||
> .content-container
|
||||
opacity 0.5
|
||||
|
||||
</style>
|
322
src/web/app/common/views/components/messaging-room.vue
Normal file
322
src/web/app/common/views/components/messaging-room.vue
Normal file
|
@ -0,0 +1,322 @@
|
|||
<template>
|
||||
<div class="mk-messaging-room">
|
||||
<div class="stream">
|
||||
<p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p>
|
||||
<p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:common.tags.mk-messaging-room.empty%</p>
|
||||
<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p>
|
||||
<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
|
||||
<template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }}
|
||||
</button>
|
||||
<template v-for="(message, i) in _messages">
|
||||
<x-message :message="message" :key="message.id"/>
|
||||
<p class="date" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date">
|
||||
<span>{{ _messages[i + 1]._datetext }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<footer>
|
||||
<div ref="notifications" class="notifications"></div>
|
||||
<div class="grippie" title="%i18n:common.tags.mk-messaging-room.resize-form%"></div>
|
||||
<x-form :user="user"/>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import MessagingStreamConnection from '../../scripts/streaming/messaging-stream';
|
||||
import XMessage from './messaging-room.message.vue';
|
||||
import XForm from './messaging-room.form.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XMessage,
|
||||
XForm
|
||||
},
|
||||
props: ['user', 'isNaked'],
|
||||
data() {
|
||||
return {
|
||||
init: true,
|
||||
fetchingMoreMessages: false,
|
||||
messages: [],
|
||||
existMoreMessages: false,
|
||||
connection: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
_messages(): any[] {
|
||||
return (this.messages as any).map(message => {
|
||||
const date = new Date(message.created_at).getDate();
|
||||
const month = new Date(message.created_at).getMonth() + 1;
|
||||
message._date = date;
|
||||
message._datetext = `${month}月 ${date}日`;
|
||||
return message;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.connection = new MessagingStreamConnection((this as any).os.i, this.user.id);
|
||||
|
||||
this.connection.on('message', this.onMessage);
|
||||
this.connection.on('read', this.onRead);
|
||||
|
||||
document.addEventListener('visibilitychange', this.onVisibilitychange);
|
||||
|
||||
this.fetchMessages().then(() => {
|
||||
this.init = false;
|
||||
this.scrollToBottom();
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.connection.off('message', this.onMessage);
|
||||
this.connection.off('read', this.onRead);
|
||||
this.connection.close();
|
||||
|
||||
document.removeEventListener('visibilitychange', this.onVisibilitychange);
|
||||
},
|
||||
methods: {
|
||||
fetchMessages() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const max = this.existMoreMessages ? 20 : 10;
|
||||
|
||||
(this as any).api('messaging/messages', {
|
||||
user_id: this.user.id,
|
||||
limit: max + 1,
|
||||
until_id: this.existMoreMessages ? this.messages[0].id : undefined
|
||||
}).then(messages => {
|
||||
if (messages.length == max + 1) {
|
||||
this.existMoreMessages = true;
|
||||
messages.pop();
|
||||
} else {
|
||||
this.existMoreMessages = false;
|
||||
}
|
||||
|
||||
this.messages.unshift.apply(this.messages, messages.reverse());
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
fetchMoreMessages() {
|
||||
this.fetchingMoreMessages = true;
|
||||
this.fetchMessages().then(() => {
|
||||
this.fetchingMoreMessages = false;
|
||||
});
|
||||
},
|
||||
onMessage(message) {
|
||||
const isBottom = this.isBottom();
|
||||
|
||||
this.messages.push(message);
|
||||
if (message.user_id != (this as any).os.i.id && !document.hidden) {
|
||||
this.connection.send({
|
||||
type: 'read',
|
||||
id: message.id
|
||||
});
|
||||
}
|
||||
|
||||
if (isBottom) {
|
||||
// Scroll to bottom
|
||||
this.scrollToBottom();
|
||||
} else if (message.user_id != (this as any).os.i.id) {
|
||||
// Notify
|
||||
this.notify('%i18n:common.tags.mk-messaging-room.new-message%');
|
||||
}
|
||||
},
|
||||
onRead(ids) {
|
||||
if (!Array.isArray(ids)) ids = [ids];
|
||||
ids.forEach(id => {
|
||||
if (this.messages.some(x => x.id == id)) {
|
||||
const exist = this.messages.map(x => x.id).indexOf(id);
|
||||
this.messages[exist].is_read = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
isBottom() {
|
||||
const asobi = 32;
|
||||
const current = this.isNaked
|
||||
? window.scrollY + window.innerHeight
|
||||
: this.$el.scrollTop + this.$el.offsetHeight;
|
||||
const max = this.isNaked
|
||||
? document.body.offsetHeight
|
||||
: this.$el.scrollHeight;
|
||||
return current > (max - asobi);
|
||||
},
|
||||
scrollToBottom() {
|
||||
if (this.isNaked) {
|
||||
window.scroll(0, document.body.offsetHeight);
|
||||
} else {
|
||||
this.$el.scrollTop = this.$el.scrollHeight;
|
||||
}
|
||||
},
|
||||
notify(message) {
|
||||
const n = document.createElement('p') as any;
|
||||
n.innerHTML = '%fa:arrow-circle-down%' + message;
|
||||
n.onclick = () => {
|
||||
this.scrollToBottom();
|
||||
n.parentNode.removeChild(n);
|
||||
};
|
||||
(this.$refs.notifications as any).appendChild(n);
|
||||
|
||||
setTimeout(() => {
|
||||
n.style.opacity = 0;
|
||||
setTimeout(() => n.parentNode.removeChild(n), 1000);
|
||||
}, 4000);
|
||||
},
|
||||
onVisibilitychange() {
|
||||
if (document.hidden) return;
|
||||
this.messages.forEach(message => {
|
||||
if (message.user_id !== (this as any).os.i.id && !message.is_read) {
|
||||
this.connection.send({
|
||||
type: 'read',
|
||||
id: message.id
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-messaging-room
|
||||
> .stream
|
||||
max-width 600px
|
||||
margin 0 auto
|
||||
|
||||
> .init
|
||||
width 100%
|
||||
margin 0
|
||||
padding 16px 8px 8px 8px
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
[data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .empty
|
||||
width 100%
|
||||
margin 0
|
||||
padding 16px 8px 8px 8px
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
[data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .no-history
|
||||
display block
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
[data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .more
|
||||
display block
|
||||
margin 16px auto
|
||||
padding 0 12px
|
||||
line-height 24px
|
||||
color #fff
|
||||
background rgba(0, 0, 0, 0.3)
|
||||
border-radius 12px
|
||||
|
||||
&:hover
|
||||
background rgba(0, 0, 0, 0.4)
|
||||
|
||||
&:active
|
||||
background rgba(0, 0, 0, 0.5)
|
||||
|
||||
&.fetching
|
||||
cursor wait
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .message
|
||||
// something
|
||||
|
||||
> .date
|
||||
display block
|
||||
margin 8px 0
|
||||
text-align center
|
||||
|
||||
&:before
|
||||
content ''
|
||||
display block
|
||||
position absolute
|
||||
height 1px
|
||||
width 90%
|
||||
top 16px
|
||||
left 0
|
||||
right 0
|
||||
margin 0 auto
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
|
||||
> span
|
||||
display inline-block
|
||||
margin 0
|
||||
padding 0 16px
|
||||
//font-weight bold
|
||||
line-height 32px
|
||||
color rgba(0, 0, 0, 0.3)
|
||||
background #fff
|
||||
|
||||
> footer
|
||||
position -webkit-sticky
|
||||
position sticky
|
||||
z-index 2
|
||||
bottom 0
|
||||
width 100%
|
||||
max-width 600px
|
||||
margin 0 auto
|
||||
padding 0
|
||||
background rgba(255, 255, 255, 0.95)
|
||||
background-clip content-box
|
||||
|
||||
> .notifications
|
||||
position absolute
|
||||
top -48px
|
||||
width 100%
|
||||
padding 8px 0
|
||||
text-align center
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> p
|
||||
display inline-block
|
||||
margin 0
|
||||
padding 0 12px 0 28px
|
||||
cursor pointer
|
||||
line-height 32px
|
||||
font-size 12px
|
||||
color $theme-color-foreground
|
||||
background $theme-color
|
||||
border-radius 16px
|
||||
transition opacity 1s ease
|
||||
|
||||
> [data-fa]
|
||||
position absolute
|
||||
top 0
|
||||
left 10px
|
||||
line-height 32px
|
||||
font-size 16px
|
||||
|
||||
> .grippie
|
||||
height 10px
|
||||
margin-top -10px
|
||||
background transparent
|
||||
cursor ns-resize
|
||||
|
||||
&:hover
|
||||
//background rgba(0, 0, 0, 0.1)
|
||||
|
||||
&:active
|
||||
//background rgba(0, 0, 0, 0.2)
|
||||
|
||||
</style>
|
457
src/web/app/common/views/components/messaging.vue
Normal file
457
src/web/app/common/views/components/messaging.vue
Normal file
|
@ -0,0 +1,457 @@
|
|||
<template>
|
||||
<div class="mk-messaging" :data-compact="compact">
|
||||
<div class="search" v-if="!compact">
|
||||
<div class="form">
|
||||
<label for="search-input">%fa:search%</label>
|
||||
<input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" placeholder="%i18n:common.tags.mk-messaging.search-user%"/>
|
||||
</div>
|
||||
<div class="result">
|
||||
<ol class="users" v-if="result.length > 0" ref="searchResult">
|
||||
<li v-for="(user, i) in result"
|
||||
@keydown.enter="navigate(user)"
|
||||
@keydown="onSearchResultKeydown(i)"
|
||||
@click="navigate(user)"
|
||||
tabindex="-1"
|
||||
>
|
||||
<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=32`" alt=""/>
|
||||
<span class="name">{{ user.name }}</span>
|
||||
<span class="username">@{{ user.username }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history" v-if="messages.length > 0">
|
||||
<template>
|
||||
<a v-for="message in messages"
|
||||
class="user"
|
||||
:href="`/i/messaging/${isMe(message) ? message.recipient.username : message.user.username}`"
|
||||
:data-is-me="isMe(message)"
|
||||
:data-is-read="message.is_read"
|
||||
@click.prevent="navigate(isMe(message) ? message.recipient : message.user)"
|
||||
:key="message.id"
|
||||
>
|
||||
<div>
|
||||
<img class="avatar" :src="`${isMe(message) ? message.recipient.avatar_url : message.user.avatar_url}?thumbnail&size=64`" alt=""/>
|
||||
<header>
|
||||
<span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span>
|
||||
<span class="username">@{{ isMe(message) ? message.recipient.username : message.user.username }}</span>
|
||||
<mk-time :time="message.created_at"/>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p class="text"><span class="me" v-if="isMe(message)">%i18n:common.tags.mk-messaging.you%:</span>{{ message.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
<p class="no-history" v-if="!fetching && messages.length == 0">%i18n:common.tags.mk-messaging.no-history%</p>
|
||||
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
messages: [],
|
||||
q: null,
|
||||
result: [],
|
||||
connection: null,
|
||||
connectionId: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.connection = (this as any).os.streams.messagingIndexStream.getConnection();
|
||||
this.connectionId = (this as any).os.streams.messagingIndexStream.use();
|
||||
|
||||
this.connection.on('message', this.onMessage);
|
||||
this.connection.on('read', this.onRead);
|
||||
|
||||
(this as any).api('messaging/history').then(messages => {
|
||||
this.messages = messages;
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.connection.off('message', this.onMessage);
|
||||
this.connection.off('read', this.onRead);
|
||||
(this as any).os.stream.dispose(this.connectionId);
|
||||
},
|
||||
methods: {
|
||||
isMe(message) {
|
||||
return message.user_id == (this as any).os.i.id;
|
||||
},
|
||||
onMessage(message) {
|
||||
this.messages = this.messages.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.messages.unshift(message);
|
||||
},
|
||||
onRead(ids) {
|
||||
ids.forEach(id => {
|
||||
const found = this.messages.find(m => m.id == id);
|
||||
if (found) found.is_read = true;
|
||||
});
|
||||
},
|
||||
search() {
|
||||
if (this.q == '') {
|
||||
this.result = [];
|
||||
return;
|
||||
}
|
||||
(this as any).api('users/search', {
|
||||
query: this.q,
|
||||
max: 5
|
||||
}).then(users => {
|
||||
this.result = users;
|
||||
});
|
||||
},
|
||||
navigate(user) {
|
||||
this.$emit('navigate', user);
|
||||
},
|
||||
onSearchKeydown(e) {
|
||||
switch (e.which) {
|
||||
case 9: // [TAB]
|
||||
case 40: // [↓]
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
(this.$refs.searchResult as any).childNodes[0].focus();
|
||||
break;
|
||||
}
|
||||
},
|
||||
onSearchResultKeydown(i, e) {
|
||||
const list = this.$refs.searchResult as any;
|
||||
|
||||
const cancel = () => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
switch (true) {
|
||||
case e.which == 27: // [ESC]
|
||||
cancel();
|
||||
(this.$refs.search as any).focus();
|
||||
break;
|
||||
|
||||
case e.which == 9 && e.shiftKey: // [TAB] + [Shift]
|
||||
case e.which == 38: // [↑]
|
||||
cancel();
|
||||
(list.childNodes[i].previousElementSibling || list.childNodes[this.result.length - 1]).focus();
|
||||
break;
|
||||
|
||||
case e.which == 9: // [TAB]
|
||||
case e.which == 40: // [↓]
|
||||
cancel();
|
||||
(list.childNodes[i].nextElementSibling || list.childNodes[0]).focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-messaging
|
||||
|
||||
&[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
|
||||
position sticky
|
||||
top 0
|
||||
left 0
|
||||
z-index 1
|
||||
width 100%
|
||||
background #fff
|
||||
box-shadow 0 0px 2px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> .form
|
||||
padding 8px
|
||||
background #f7f7f7
|
||||
|
||||
> label
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 8px
|
||||
z-index 1
|
||||
height 100%
|
||||
width 38px
|
||||
pointer-events none
|
||||
|
||||
> [data-fa]
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
bottom 0
|
||||
left 0
|
||||
width 1em
|
||||
line-height 56px
|
||||
margin auto
|
||||
color #555
|
||||
|
||||
> input
|
||||
margin 0
|
||||
padding 0 0 0 32px
|
||||
width 100%
|
||||
font-size 1em
|
||||
line-height 38px
|
||||
color #000
|
||||
outline none
|
||||
border solid 1px #eee
|
||||
border-radius 5px
|
||||
box-shadow none
|
||||
transition color 0.5s ease, border 0.5s ease
|
||||
|
||||
&:hover
|
||||
border solid 1px #ddd
|
||||
transition border 0.2s ease
|
||||
|
||||
&:focus
|
||||
color darken($theme-color, 20%)
|
||||
border solid 1px $theme-color
|
||||
transition color 0, border 0
|
||||
|
||||
> .result
|
||||
display block
|
||||
top 0
|
||||
left 0
|
||||
z-index 2
|
||||
width 100%
|
||||
margin 0
|
||||
padding 0
|
||||
background #fff
|
||||
|
||||
> .users
|
||||
margin 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
> li
|
||||
display inline-block
|
||||
z-index 1
|
||||
width 100%
|
||||
padding 8px 32px
|
||||
vertical-align top
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
text-decoration none
|
||||
transition none
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
&:focus
|
||||
color #fff
|
||||
background $theme-color
|
||||
|
||||
.name
|
||||
color #fff
|
||||
|
||||
.username
|
||||
color #fff
|
||||
|
||||
&:active
|
||||
color #fff
|
||||
background darken($theme-color, 10%)
|
||||
|
||||
.name
|
||||
color #fff
|
||||
|
||||
.username
|
||||
color #fff
|
||||
|
||||
.avatar
|
||||
vertical-align middle
|
||||
min-width 32px
|
||||
min-height 32px
|
||||
max-width 32px
|
||||
max-height 32px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
.name
|
||||
margin 0 8px 0 0
|
||||
/*font-weight bold*/
|
||||
font-weight normal
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
|
||||
.username
|
||||
font-weight normal
|
||||
color rgba(0, 0, 0, 0.3)
|
||||
|
||||
> .history
|
||||
|
||||
> a
|
||||
display block
|
||||
text-decoration none
|
||||
background #fff
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
*
|
||||
pointer-events none
|
||||
user-select none
|
||||
|
||||
&:hover
|
||||
background #fafafa
|
||||
|
||||
> .avatar
|
||||
filter saturate(200%)
|
||||
|
||||
&:active
|
||||
background #eee
|
||||
|
||||
&[data-is-read]
|
||||
&[data-is-me]
|
||||
opacity 0.8
|
||||
|
||||
&:not([data-is-me]):not([data-is-read])
|
||||
> div
|
||||
background-image url("/assets/unread.svg")
|
||||
background-repeat no-repeat
|
||||
background-position 0 center
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> div
|
||||
max-width 500px
|
||||
margin 0 auto
|
||||
padding 20px 30px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> header
|
||||
margin-bottom 2px
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
|
||||
> .name
|
||||
text-align left
|
||||
display inline
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 1em
|
||||
color rgba(0, 0, 0, 0.9)
|
||||
font-weight bold
|
||||
transition all 0.1s ease
|
||||
|
||||
> .username
|
||||
text-align left
|
||||
margin 0 0 0 8px
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
|
||||
> .mk-time
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
display inline
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
font-size 80%
|
||||
|
||||
> .avatar
|
||||
float left
|
||||
width 54px
|
||||
height 54px
|
||||
margin 0 16px 0 0
|
||||
border-radius 8px
|
||||
transition all 0.1s ease
|
||||
|
||||
> .body
|
||||
|
||||
> .text
|
||||
display block
|
||||
margin 0 0 0 0
|
||||
padding 0
|
||||
overflow hidden
|
||||
overflow-wrap break-word
|
||||
font-size 1.1em
|
||||
color rgba(0, 0, 0, 0.8)
|
||||
|
||||
.me
|
||||
color rgba(0, 0, 0, 0.4)
|
||||
|
||||
> .image
|
||||
display block
|
||||
max-width 100%
|
||||
max-height 512px
|
||||
|
||||
> .no-history
|
||||
margin 0
|
||||
padding 2em 1em
|
||||
text-align center
|
||||
color #999
|
||||
font-weight 500
|
||||
|
||||
> .fetching
|
||||
margin 0
|
||||
padding 16px
|
||||
text-align center
|
||||
color #aaa
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
// TODO: element base media query
|
||||
@media (max-width 400px)
|
||||
> .search
|
||||
> .result
|
||||
> .users
|
||||
> li
|
||||
padding 8px 16px
|
||||
|
||||
> .history
|
||||
> a
|
||||
&:not([data-is-me]):not([data-is-read])
|
||||
> div
|
||||
background-image none
|
||||
border-left solid 4px #3aa2dc
|
||||
|
||||
> div
|
||||
padding 16px
|
||||
font-size 14px
|
||||
|
||||
> .avatar
|
||||
margin 0 12px 0 0
|
||||
|
||||
</style>
|
41
src/web/app/common/views/components/nav.vue
Normal file
41
src/web/app/common/views/components/nav.vue
Normal file
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<span class="mk-nav">
|
||||
<a :href="aboutUrl">%i18n:common.tags.mk-nav-links.about%</a>
|
||||
<i>・</i>
|
||||
<a :href="statsUrl">%i18n:common.tags.mk-nav-links.stats%</a>
|
||||
<i>・</i>
|
||||
<a :href="statusUrl">%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="devUrl">%i18n:common.tags.mk-nav-links.develop%</a>
|
||||
<i>・</i>
|
||||
<a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { docsUrl, statsUrl, statusUrl, devUrl, lang } from '../../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
aboutUrl: `${docsUrl}/${lang}/about`,
|
||||
statsUrl,
|
||||
statusUrl,
|
||||
devUrl
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-nav
|
||||
a
|
||||
color inherit
|
||||
</style>
|
138
src/web/app/common/views/components/poll-editor.vue
Normal file
138
src/web/app/common/views/components/poll-editor.vue
Normal file
|
@ -0,0 +1,138 @@
|
|||
<template>
|
||||
<div class="mk-poll-editor">
|
||||
<p class="caution" v-if="choices.length < 2">
|
||||
%fa:exclamation-triangle%%i18n:common.tags.mk-poll-editor.no-only-one-choice%
|
||||
</p>
|
||||
<ul ref="choices">
|
||||
<li v-for="(choice, i) in choices">
|
||||
<input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:common.tags.mk-poll-editor.choice-n%'.replace('{}', i + 1)">
|
||||
<button @click="remove(i)" title="%i18n:common.tags.mk-poll-editor.remove%">
|
||||
%fa:times%
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="add" v-if="choices.length < 10" @click="add">%i18n:common.tags.mk-poll-editor.add%</button>
|
||||
<button class="destroy" @click="destroy" title="%i18n:common.tags.mk-poll-editor.destroy%">
|
||||
%fa:times%
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
choices: ['', '']
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
choices() {
|
||||
this.$emit('updated');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInput(i, e) {
|
||||
Vue.set(this.choices, i, e.target.value);
|
||||
},
|
||||
|
||||
add() {
|
||||
this.choices.push('');
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
|
||||
});
|
||||
},
|
||||
|
||||
remove(i) {
|
||||
this.choices = this.choices.filter((_, _i) => _i != i);
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this.$emit('destroyed');
|
||||
},
|
||||
|
||||
get() {
|
||||
return {
|
||||
choices: this.choices.filter(choice => choice != '')
|
||||
}
|
||||
},
|
||||
|
||||
set(data) {
|
||||
if (data.choices.length == 0) return;
|
||||
this.choices = data.choices;
|
||||
if (data.choices.length == 1) this.choices = this.choices.concat('');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-poll-editor
|
||||
padding 8px
|
||||
|
||||
> .caution
|
||||
margin 0 0 8px 0
|
||||
font-size 0.8em
|
||||
color #f00
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> ul
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
> li
|
||||
display block
|
||||
margin 8px 0
|
||||
padding 0
|
||||
width 100%
|
||||
|
||||
&:first-child
|
||||
margin-top 0
|
||||
|
||||
&:last-child
|
||||
margin-bottom 0
|
||||
|
||||
> input
|
||||
padding 6px
|
||||
border solid 1px rgba($theme-color, 0.1)
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
border-color rgba($theme-color, 0.2)
|
||||
|
||||
&:focus
|
||||
border-color rgba($theme-color, 0.5)
|
||||
|
||||
> button
|
||||
padding 4px 8px
|
||||
color rgba($theme-color, 0.4)
|
||||
|
||||
&:hover
|
||||
color rgba($theme-color, 0.6)
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 30%)
|
||||
|
||||
> .add
|
||||
margin 8px 0 0 0
|
||||
vertical-align top
|
||||
color $theme-color
|
||||
|
||||
> .destroy
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
padding 4px 8px
|
||||
color rgba($theme-color, 0.4)
|
||||
|
||||
&:hover
|
||||
color rgba($theme-color, 0.6)
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 30%)
|
||||
|
||||
</style>
|
118
src/web/app/common/views/components/poll.vue
Normal file
118
src/web/app/common/views/components/poll.vue
Normal file
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<div class="mk-poll" :data-is-voted="isVoted">
|
||||
<ul>
|
||||
<li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', choice.text) : ''">
|
||||
<div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div>
|
||||
<span>
|
||||
<template v-if="choice.is_voted">%fa:check%</template>
|
||||
{{ choice.text }}
|
||||
<span class="votes" v-if="showResult">({{ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', choice.votes) }})</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="total > 0">
|
||||
<span>{{ '%i18n:common.tags.mk-poll.total-users%'.replace('{}', total) }}</span>
|
||||
・
|
||||
<a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }}</a>
|
||||
<span v-if="isVoted">%i18n:common.tags.mk-poll.voted%</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: ['post'],
|
||||
data() {
|
||||
return {
|
||||
showResult: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
poll(): any {
|
||||
return this.post.poll;
|
||||
},
|
||||
total(): number {
|
||||
return this.poll.choices.reduce((a, b) => a + b.votes, 0);
|
||||
},
|
||||
isVoted(): boolean {
|
||||
return this.poll.choices.some(c => c.is_voted);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.showResult = this.isVoted;
|
||||
},
|
||||
methods: {
|
||||
toggleShowResult() {
|
||||
this.showResult = !this.showResult;
|
||||
},
|
||||
vote(id) {
|
||||
if (this.poll.choices.some(c => c.is_voted)) return;
|
||||
(this as any).api('posts/polls/vote', {
|
||||
post_id: this.post.id,
|
||||
choice: id
|
||||
}).then(() => {
|
||||
this.poll.choices.forEach(c => {
|
||||
if (c.id == id) {
|
||||
c.votes++;
|
||||
Vue.set(c, 'is_voted', true);
|
||||
}
|
||||
});
|
||||
this.showResult = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-poll
|
||||
|
||||
> ul
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
> li
|
||||
display block
|
||||
margin 4px 0
|
||||
padding 4px 8px
|
||||
width 100%
|
||||
border solid 1px #eee
|
||||
border-radius 4px
|
||||
overflow hidden
|
||||
cursor pointer
|
||||
|
||||
&:hover
|
||||
background rgba(0, 0, 0, 0.05)
|
||||
|
||||
&:active
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
|
||||
> .backdrop
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
height 100%
|
||||
background $theme-color
|
||||
transition width 1s ease
|
||||
|
||||
> .votes
|
||||
margin-left 4px
|
||||
|
||||
> p
|
||||
a
|
||||
color inherit
|
||||
|
||||
&[data-is-voted]
|
||||
> ul > li
|
||||
cursor default
|
||||
|
||||
&:hover
|
||||
background transparent
|
||||
|
||||
&:active
|
||||
background transparent
|
||||
|
||||
</style>
|
102
src/web/app/common/views/components/post-html.ts
Normal file
102
src/web/app/common/views/components/post-html.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
declare const _URL_: string;
|
||||
|
||||
import Vue from 'vue';
|
||||
import * as pictograph from 'pictograph';
|
||||
|
||||
import MkUrl from './url.vue';
|
||||
|
||||
const escape = text =>
|
||||
text
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<');
|
||||
|
||||
export default Vue.component('mk-post-html', {
|
||||
props: {
|
||||
ast: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
shouldBreak: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
i: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
render(createElement) {
|
||||
const els = [].concat.apply([], (this as any).ast.map(token => {
|
||||
switch (token.type) {
|
||||
case 'text':
|
||||
const text = escape(token.content)
|
||||
.replace(/(\r\n|\n|\r)/g, '\n');
|
||||
|
||||
if ((this as any).shouldBreak) {
|
||||
if (text.indexOf('\n') != -1) {
|
||||
return text.split('\n').map(t => [createElement('span', t), createElement('br')]);
|
||||
} else {
|
||||
return createElement('span', text);
|
||||
}
|
||||
} else {
|
||||
return createElement('span', text.replace(/\n/g, ' '));
|
||||
}
|
||||
|
||||
case 'bold':
|
||||
return createElement('strong', escape(token.bold));
|
||||
|
||||
case 'url':
|
||||
return createElement(MkUrl, {
|
||||
props: {
|
||||
url: escape(token.content),
|
||||
target: '_blank'
|
||||
}
|
||||
});
|
||||
|
||||
case 'link':
|
||||
return createElement('a', {
|
||||
attrs: {
|
||||
class: 'link',
|
||||
href: escape(token.url),
|
||||
target: '_blank',
|
||||
title: escape(token.url)
|
||||
}
|
||||
}, escape(token.title));
|
||||
|
||||
case 'mention':
|
||||
return (createElement as any)('a', {
|
||||
attrs: {
|
||||
href: `${_URL_}/${escape(token.username)}`,
|
||||
target: '_blank',
|
||||
dataIsMe: (this as any).i && (this as any).i.username == token.username
|
||||
},
|
||||
directives: [{
|
||||
name: 'user-preview',
|
||||
value: token.content
|
||||
}]
|
||||
}, token.content);
|
||||
|
||||
case 'hashtag':
|
||||
return createElement('a', {
|
||||
attrs: {
|
||||
href: `${_URL_}/search?q=${escape(token.content)}`,
|
||||
target: '_blank'
|
||||
}
|
||||
}, escape(token.content));
|
||||
|
||||
case 'code':
|
||||
return createElement('pre', [
|
||||
createElement('code', token.html)
|
||||
]);
|
||||
|
||||
case 'inline-code':
|
||||
return createElement('code', token.html);
|
||||
|
||||
case 'emoji':
|
||||
return createElement('span', pictograph.dic[token.emoji] || token.content);
|
||||
}
|
||||
}));
|
||||
|
||||
return createElement('span', els);
|
||||
}
|
||||
});
|
141
src/web/app/common/views/components/post-menu.vue
Normal file
141
src/web/app/common/views/components/post-menu.vue
Normal file
|
@ -0,0 +1,141 @@
|
|||
<template>
|
||||
<div class="mk-post-menu">
|
||||
<div class="backdrop" ref="backdrop" @click="close"></div>
|
||||
<div class="popover" :class="{ compact }" ref="popover">
|
||||
<button v-if="post.user_id == os.i.id" @click="pin">%i18n:common.tags.mk-post-menu.pin%</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as anime from 'animejs';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['post', 'source', 'compact'],
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const popover = this.$refs.popover as any;
|
||||
|
||||
const rect = this.source.getBoundingClientRect();
|
||||
const width = popover.offsetWidth;
|
||||
const height = popover.offsetHeight;
|
||||
|
||||
if (this.compact) {
|
||||
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
|
||||
popover.style.left = (x - (width / 2)) + 'px';
|
||||
popover.style.top = (y - (height / 2)) + 'px';
|
||||
} else {
|
||||
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + window.pageYOffset + this.source.offsetHeight;
|
||||
popover.style.left = (x - (width / 2)) + 'px';
|
||||
popover.style.top = y + 'px';
|
||||
}
|
||||
|
||||
anime({
|
||||
targets: this.$refs.backdrop,
|
||||
opacity: 1,
|
||||
duration: 100,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
anime({
|
||||
targets: this.$refs.popover,
|
||||
opacity: 1,
|
||||
scale: [0.5, 1],
|
||||
duration: 500
|
||||
});
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
pin() {
|
||||
(this as any).api('i/pin', {
|
||||
post_id: this.post.id
|
||||
}).then(() => {
|
||||
this.$destroy();
|
||||
});
|
||||
},
|
||||
|
||||
close() {
|
||||
(this.$refs.backdrop as any).style.pointerEvents = 'none';
|
||||
anime({
|
||||
targets: this.$refs.backdrop,
|
||||
opacity: 0,
|
||||
duration: 200,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
(this.$refs.popover as any).style.pointerEvents = 'none';
|
||||
anime({
|
||||
targets: this.$refs.popover,
|
||||
opacity: 0,
|
||||
scale: 0.5,
|
||||
duration: 200,
|
||||
easing: 'easeInBack',
|
||||
complete: () => this.$destroy()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
$border-color = rgba(27, 31, 35, 0.15)
|
||||
|
||||
.mk-post-menu
|
||||
position initial
|
||||
|
||||
> .backdrop
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 10000
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
opacity 0
|
||||
|
||||
> .popover
|
||||
position absolute
|
||||
z-index 10001
|
||||
background #fff
|
||||
border 1px solid $border-color
|
||||
border-radius 4px
|
||||
box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
|
||||
transform scale(0.5)
|
||||
opacity 0
|
||||
|
||||
$balloon-size = 16px
|
||||
|
||||
&:not(.compact)
|
||||
margin-top $balloon-size
|
||||
transform-origin center -($balloon-size)
|
||||
|
||||
&:before
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top -($balloon-size * 2)
|
||||
left s('calc(50% - %s)', $balloon-size)
|
||||
border-top solid $balloon-size transparent
|
||||
border-left solid $balloon-size transparent
|
||||
border-right solid $balloon-size transparent
|
||||
border-bottom solid $balloon-size $border-color
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top -($balloon-size * 2) + 1.5px
|
||||
left s('calc(50% - %s)', $balloon-size)
|
||||
border-top solid $balloon-size transparent
|
||||
border-left solid $balloon-size transparent
|
||||
border-right solid $balloon-size transparent
|
||||
border-bottom solid $balloon-size #fff
|
||||
|
||||
> button
|
||||
display block
|
||||
padding 16px
|
||||
|
||||
</style>
|
28
src/web/app/common/views/components/reaction-icon.vue
Normal file
28
src/web/app/common/views/components/reaction-icon.vue
Normal file
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<span class="mk-reaction-icon">
|
||||
<img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%">
|
||||
<img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%">
|
||||
<img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%">
|
||||
<img v-if="reaction == 'hmm'" src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%">
|
||||
<img v-if="reaction == 'surprise'" src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%">
|
||||
<img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%">
|
||||
<img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%">
|
||||
<img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%">
|
||||
<img v-if="reaction == 'pudding'" src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%">
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: ['reaction']
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-reaction-icon
|
||||
img
|
||||
vertical-align middle
|
||||
width 1em
|
||||
height 1em
|
||||
</style>
|
188
src/web/app/common/views/components/reaction-picker.vue
Normal file
188
src/web/app/common/views/components/reaction-picker.vue
Normal file
|
@ -0,0 +1,188 @@
|
|||
<template>
|
||||
<div class="mk-reaction-picker">
|
||||
<div class="backdrop" ref="backdrop" @click="close"></div>
|
||||
<div class="popover" :class="{ compact }" ref="popover">
|
||||
<p v-if="!compact">{{ title }}</p>
|
||||
<div>
|
||||
<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
|
||||
<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
|
||||
<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
|
||||
<button @click="react('hmm')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.hmm%"><mk-reaction-icon reaction='hmm'/></button>
|
||||
<button @click="react('surprise')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.surprise%"><mk-reaction-icon reaction='surprise'/></button>
|
||||
<button @click="react('congrats')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.congrats%"><mk-reaction-icon reaction='congrats'/></button>
|
||||
<button @click="react('angry')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.angry%"><mk-reaction-icon reaction='angry'/></button>
|
||||
<button @click="react('confused')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.confused%"><mk-reaction-icon reaction='confused'/></button>
|
||||
<button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.pudding%"><mk-reaction-icon reaction='pudding'/></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as anime from 'animejs';
|
||||
|
||||
const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['post', 'source', 'compact', 'cb'],
|
||||
data() {
|
||||
return {
|
||||
title: placeholder
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const popover = this.$refs.popover as any;
|
||||
|
||||
const rect = this.source.getBoundingClientRect();
|
||||
const width = popover.offsetWidth;
|
||||
const height = popover.offsetHeight;
|
||||
|
||||
if (this.compact) {
|
||||
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
|
||||
popover.style.left = (x - (width / 2)) + 'px';
|
||||
popover.style.top = (y - (height / 2)) + 'px';
|
||||
} else {
|
||||
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + window.pageYOffset + this.source.offsetHeight;
|
||||
popover.style.left = (x - (width / 2)) + 'px';
|
||||
popover.style.top = y + 'px';
|
||||
}
|
||||
|
||||
anime({
|
||||
targets: this.$refs.backdrop,
|
||||
opacity: 1,
|
||||
duration: 100,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
anime({
|
||||
targets: this.$refs.popover,
|
||||
opacity: 1,
|
||||
scale: [0.5, 1],
|
||||
duration: 500
|
||||
});
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
react(reaction) {
|
||||
(this as any).api('posts/reactions/create', {
|
||||
post_id: this.post.id,
|
||||
reaction: reaction
|
||||
}).then(() => {
|
||||
if (this.cb) this.cb();
|
||||
this.$destroy();
|
||||
});
|
||||
},
|
||||
onMouseover(e) {
|
||||
this.title = e.target.title;
|
||||
},
|
||||
onMouseout(e) {
|
||||
this.title = placeholder;
|
||||
},
|
||||
close() {
|
||||
(this.$refs.backdrop as any).style.pointerEvents = 'none';
|
||||
anime({
|
||||
targets: this.$refs.backdrop,
|
||||
opacity: 0,
|
||||
duration: 200,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
(this.$refs.popover as any).style.pointerEvents = 'none';
|
||||
anime({
|
||||
targets: this.$refs.popover,
|
||||
opacity: 0,
|
||||
scale: 0.5,
|
||||
duration: 200,
|
||||
easing: 'easeInBack',
|
||||
complete: () => this.$destroy()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
$border-color = rgba(27, 31, 35, 0.15)
|
||||
|
||||
.mk-reaction-picker
|
||||
position initial
|
||||
|
||||
> .backdrop
|
||||
position fixed
|
||||
top 0
|
||||
left 0
|
||||
z-index 10000
|
||||
width 100%
|
||||
height 100%
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
opacity 0
|
||||
|
||||
> .popover
|
||||
position absolute
|
||||
z-index 10001
|
||||
background #fff
|
||||
border 1px solid $border-color
|
||||
border-radius 4px
|
||||
box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
|
||||
transform scale(0.5)
|
||||
opacity 0
|
||||
|
||||
$balloon-size = 16px
|
||||
|
||||
&:not(.compact)
|
||||
margin-top $balloon-size
|
||||
transform-origin center -($balloon-size)
|
||||
|
||||
&:before
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top -($balloon-size * 2)
|
||||
left s('calc(50% - %s)', $balloon-size)
|
||||
border-top solid $balloon-size transparent
|
||||
border-left solid $balloon-size transparent
|
||||
border-right solid $balloon-size transparent
|
||||
border-bottom solid $balloon-size $border-color
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
position absolute
|
||||
top -($balloon-size * 2) + 1.5px
|
||||
left s('calc(50% - %s)', $balloon-size)
|
||||
border-top solid $balloon-size transparent
|
||||
border-left solid $balloon-size transparent
|
||||
border-right solid $balloon-size transparent
|
||||
border-bottom solid $balloon-size #fff
|
||||
|
||||
> p
|
||||
display block
|
||||
margin 0
|
||||
padding 8px 10px
|
||||
font-size 14px
|
||||
color #586069
|
||||
border-bottom solid 1px #e1e4e8
|
||||
|
||||
> div
|
||||
padding 4px
|
||||
width 240px
|
||||
text-align center
|
||||
|
||||
> button
|
||||
width 40px
|
||||
height 40px
|
||||
font-size 24px
|
||||
border-radius 2px
|
||||
|
||||
&:hover
|
||||
background #eee
|
||||
|
||||
&:active
|
||||
background $theme-color
|
||||
box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
|
||||
|
||||
</style>
|
49
src/web/app/common/views/components/reactions-viewer.vue
Normal file
49
src/web/app/common/views/components/reactions-viewer.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<div class="mk-reactions-viewer">
|
||||
<template v-if="reactions">
|
||||
<span v-if="reactions.like"><mk-reaction-icon reaction='like'/><span>{{ reactions.like }}</span></span>
|
||||
<span v-if="reactions.love"><mk-reaction-icon reaction='love'/><span>{{ reactions.love }}</span></span>
|
||||
<span v-if="reactions.laugh"><mk-reaction-icon reaction='laugh'/><span>{{ reactions.laugh }}</span></span>
|
||||
<span v-if="reactions.hmm"><mk-reaction-icon reaction='hmm'/><span>{{ reactions.hmm }}</span></span>
|
||||
<span v-if="reactions.surprise"><mk-reaction-icon reaction='surprise'/><span>{{ reactions.surprise }}</span></span>
|
||||
<span v-if="reactions.congrats"><mk-reaction-icon reaction='congrats'/><span>{{ reactions.congrats }}</span></span>
|
||||
<span v-if="reactions.angry"><mk-reaction-icon reaction='angry'/><span>{{ reactions.angry }}</span></span>
|
||||
<span v-if="reactions.confused"><mk-reaction-icon reaction='confused'/><span>{{ reactions.confused }}</span></span>
|
||||
<span v-if="reactions.pudding"><mk-reaction-icon reaction='pudding'/><span>{{ reactions.pudding }}</span></span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
props: ['post'],
|
||||
computed: {
|
||||
reactions(): number {
|
||||
return this.post.reaction_counts;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-reactions-viewer
|
||||
border-top dashed 1px #eee
|
||||
border-bottom dashed 1px #eee
|
||||
margin 4px 0
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> span
|
||||
margin-right 8px
|
||||
|
||||
> .mk-reaction-icon
|
||||
font-size 1.4em
|
||||
|
||||
> span
|
||||
margin-left 4px
|
||||
font-size 1.2em
|
||||
color #444
|
||||
|
||||
</style>
|
137
src/web/app/common/views/components/signin.vue
Normal file
137
src/web/app/common/views/components/signin.vue
Normal file
|
@ -0,0 +1,137 @@
|
|||
<template>
|
||||
<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
|
||||
<label class="user-name">
|
||||
<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]+$" placeholder="%i18n:common.tags.mk-signin.username%" autofocus required @change="onUsernameChange"/>%fa:at%
|
||||
</label>
|
||||
<label class="password">
|
||||
<input v-model="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required/>%fa:lock%
|
||||
</label>
|
||||
<label class="token" v-if="user && user.two_factor_enabled">
|
||||
<input v-model="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required/>%fa:lock%
|
||||
</label>
|
||||
<button type="submit" :disabled="signing">{{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }}</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
signing: false,
|
||||
user: null,
|
||||
username: '',
|
||||
password: '',
|
||||
token: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onUsernameChange() {
|
||||
(this as any).api('users/show', {
|
||||
username: this.username
|
||||
}).then(user => {
|
||||
this.user = user;
|
||||
});
|
||||
},
|
||||
onSubmit() {
|
||||
this.signing = true;
|
||||
|
||||
(this as any).api('signin', {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
token: this.user && this.user.two_factor_enabled ? this.token : undefined
|
||||
}).then(() => {
|
||||
location.reload();
|
||||
}).catch(() => {
|
||||
alert('something happened');
|
||||
this.signing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-signin
|
||||
&.signing
|
||||
&, *
|
||||
cursor wait !important
|
||||
|
||||
label
|
||||
display block
|
||||
margin 12px 0
|
||||
|
||||
[data-fa]
|
||||
display block
|
||||
pointer-events none
|
||||
position absolute
|
||||
bottom 0
|
||||
top 0
|
||||
left 0
|
||||
z-index 1
|
||||
margin auto
|
||||
padding 0 16px
|
||||
height 1em
|
||||
color #898786
|
||||
|
||||
input[type=text]
|
||||
input[type=password]
|
||||
input[type=number]
|
||||
user-select text
|
||||
display inline-block
|
||||
cursor auto
|
||||
padding 0 0 0 38px
|
||||
margin 0
|
||||
width 100%
|
||||
line-height 44px
|
||||
font-size 1em
|
||||
color rgba(0, 0, 0, 0.7)
|
||||
background #fff
|
||||
outline none
|
||||
border solid 1px #eee
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
background rgba(255, 255, 255, 0.7)
|
||||
border-color #ddd
|
||||
|
||||
& + i
|
||||
color #797776
|
||||
|
||||
&:focus
|
||||
background #fff
|
||||
border-color #ccc
|
||||
|
||||
& + i
|
||||
color #797776
|
||||
|
||||
[type=submit]
|
||||
cursor pointer
|
||||
padding 16px
|
||||
margin -6px 0 0 0
|
||||
width 100%
|
||||
font-size 1.2em
|
||||
color rgba(0, 0, 0, 0.5)
|
||||
outline none
|
||||
border none
|
||||
border-radius 0
|
||||
background transparent
|
||||
transition all .5s ease
|
||||
|
||||
&:hover
|
||||
color $theme-color
|
||||
transition all .2s ease
|
||||
|
||||
&:focus
|
||||
color $theme-color
|
||||
transition all .2s ease
|
||||
|
||||
&:active
|
||||
color darken($theme-color, 30%)
|
||||
transition all .2s ease
|
||||
|
||||
&:disabled
|
||||
opacity 0.7
|
||||
|
||||
</style>
|
285
src/web/app/common/views/components/signup.vue
Normal file
285
src/web/app/common/views/components/signup.vue
Normal file
|
@ -0,0 +1,285 @@
|
|||
<template>
|
||||
<form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off">
|
||||
<label class="username">
|
||||
<p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p>
|
||||
<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/>
|
||||
<p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/${username}` }}</p>
|
||||
<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p>
|
||||
<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p>
|
||||
<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p>
|
||||
<p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.error%</p>
|
||||
<p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.invalid-format%</p>
|
||||
<p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-short%</p>
|
||||
<p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-long%</p>
|
||||
</label>
|
||||
<label class="password">
|
||||
<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%</p>
|
||||
<input v-model="password" type="password" placeholder="%i18n:common.tags.mk-signup.password-placeholder%" autocomplete="off" required @input="onChangePassword"/>
|
||||
<div class="meter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
|
||||
<div class="value" ref="passwordMetar"></div>
|
||||
</div>
|
||||
<p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.weak-password%</p>
|
||||
<p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.normal-password%</p>
|
||||
<p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.strong-password%</p>
|
||||
</label>
|
||||
<label class="retype-password">
|
||||
<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%(%i18n:common.tags.mk-signup.retype%)</p>
|
||||
<input v-model="retypedPassword" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required @input="onChangePasswordRetype"/>
|
||||
<p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.password-matched%</p>
|
||||
<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.password-not-matched%</p>
|
||||
</label>
|
||||
<label class="recaptcha">
|
||||
<p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:common.tags.mk-signup.recaptcha%</p>
|
||||
<div class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div>
|
||||
</label>
|
||||
<label class="agree-tou">
|
||||
<input name="agree-tou" type="checkbox" autocomplete="off" required/>
|
||||
<p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p>
|
||||
</label>
|
||||
<button type="submit">%i18n:common.tags.mk-signup.create%</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
const getPasswordStrength = require('syuilo-password-strength');
|
||||
import { url, docsUrl, lang, recaptchaSitekey } from '../../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
retypedPassword: '',
|
||||
url,
|
||||
touUrl: `${docsUrl}/${lang}/tou`,
|
||||
recaptchaSitekey,
|
||||
recaptchaed: false,
|
||||
usernameState: null,
|
||||
passwordStrength: '',
|
||||
passwordRetypeState: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldShowProfileUrl(): boolean {
|
||||
return (this.username != '' &&
|
||||
this.usernameState != 'invalid-format' &&
|
||||
this.usernameState != 'min-range' &&
|
||||
this.usernameState != 'max-range');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChangeUsername() {
|
||||
if (this.username == '') {
|
||||
this.usernameState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const err =
|
||||
!this.username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
|
||||
this.username.length < 3 ? 'min-range' :
|
||||
this.username.length > 20 ? 'max-range' :
|
||||
null;
|
||||
|
||||
if (err) {
|
||||
this.usernameState = err;
|
||||
return;
|
||||
}
|
||||
|
||||
this.usernameState = 'wait';
|
||||
|
||||
(this as any).api('username/available', {
|
||||
username: this.username
|
||||
}).then(result => {
|
||||
this.usernameState = result.available ? 'ok' : 'unavailable';
|
||||
}).catch(err => {
|
||||
this.usernameState = 'error';
|
||||
});
|
||||
},
|
||||
onChangePassword() {
|
||||
if (this.password == '') {
|
||||
this.passwordStrength = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const strength = getPasswordStrength(this.password);
|
||||
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||
(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
|
||||
},
|
||||
onChangePasswordRetype() {
|
||||
if (this.retypedPassword == '') {
|
||||
this.passwordRetypeState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match';
|
||||
},
|
||||
onSubmit() {
|
||||
(this as any).api('signup', {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
'g-recaptcha-response': (window as any).grecaptcha.getResponse()
|
||||
}).then(() => {
|
||||
(this as any).api('signin', {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
}).then(() => {
|
||||
location.href = '/';
|
||||
});
|
||||
}).catch(() => {
|
||||
alert('%i18n:common.tags.mk-signup.some-error%');
|
||||
|
||||
(window as any).grecaptcha.reset();
|
||||
this.recaptchaed = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
(window as any).onRecaptchaed = () => {
|
||||
this.recaptchaed = true;
|
||||
};
|
||||
|
||||
(window as any).onRecaptchaExpired = () => {
|
||||
this.recaptchaed = false;
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
|
||||
head.appendChild(script);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-signup
|
||||
min-width 302px
|
||||
|
||||
label
|
||||
display block
|
||||
margin 0 0 16px 0
|
||||
|
||||
> .caption
|
||||
margin 0 0 4px 0
|
||||
color #828888
|
||||
font-size 0.95em
|
||||
|
||||
> [data-fa]
|
||||
margin-right 0.25em
|
||||
color #96adac
|
||||
|
||||
> .info
|
||||
display block
|
||||
margin 4px 0
|
||||
font-size 0.8em
|
||||
|
||||
> [data-fa]
|
||||
margin-right 0.3em
|
||||
|
||||
&.username
|
||||
.profile-page-url-preview
|
||||
display block
|
||||
margin 4px 8px 0 4px
|
||||
font-size 0.8em
|
||||
color #888
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
&:not(:empty) + .info
|
||||
margin-top 0
|
||||
|
||||
&.password
|
||||
.meter
|
||||
display block
|
||||
margin-top 8px
|
||||
width 100%
|
||||
height 8px
|
||||
|
||||
&[data-strength='']
|
||||
display none
|
||||
|
||||
&[data-strength='low']
|
||||
> .value
|
||||
background #d73612
|
||||
|
||||
&[data-strength='medium']
|
||||
> .value
|
||||
background #d7ca12
|
||||
|
||||
&[data-strength='high']
|
||||
> .value
|
||||
background #61bb22
|
||||
|
||||
> .value
|
||||
display block
|
||||
width 0%
|
||||
height 100%
|
||||
background transparent
|
||||
border-radius 4px
|
||||
transition all 0.1s ease
|
||||
|
||||
[type=text], [type=password]
|
||||
user-select text
|
||||
display inline-block
|
||||
cursor auto
|
||||
padding 0 12px
|
||||
margin 0
|
||||
width 100%
|
||||
line-height 44px
|
||||
font-size 1em
|
||||
color #333 !important
|
||||
background #fff !important
|
||||
outline none
|
||||
border solid 1px rgba(0, 0, 0, 0.1)
|
||||
border-radius 4px
|
||||
box-shadow 0 0 0 114514px #fff inset
|
||||
transition all .3s ease
|
||||
|
||||
&:hover
|
||||
border-color rgba(0, 0, 0, 0.2)
|
||||
transition all .1s ease
|
||||
|
||||
&:focus
|
||||
color $theme-color !important
|
||||
border-color $theme-color
|
||||
box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
|
||||
transition all 0s ease
|
||||
|
||||
&:disabled
|
||||
opacity 0.5
|
||||
|
||||
.agree-tou
|
||||
padding 4px
|
||||
border-radius 4px
|
||||
|
||||
&:hover
|
||||
background #f4f4f4
|
||||
|
||||
&:active
|
||||
background #eee
|
||||
|
||||
&, *
|
||||
cursor pointer
|
||||
|
||||
p
|
||||
display inline
|
||||
color #555
|
||||
|
||||
button
|
||||
margin 0
|
||||
padding 16px
|
||||
width 100%
|
||||
font-size 1em
|
||||
color #fff
|
||||
background $theme-color
|
||||
border-radius 3px
|
||||
|
||||
&:hover
|
||||
background lighten($theme-color, 5%)
|
||||
|
||||
&:active
|
||||
background darken($theme-color, 5%)
|
||||
|
||||
</style>
|
42
src/web/app/common/views/components/special-message.vue
Normal file
42
src/web/app/common/views/components/special-message.vue
Normal file
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div class="mk-special-message">
|
||||
<p v-if="m == 1 && d == 1">%i18n:common.tags.mk-special-message.new-year%</p>
|
||||
<p v-if="m == 12 && d == 25">%i18n:common.tags.mk-special-message.christmas%</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
now: new Date()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
d(): number {
|
||||
return this.now.getDate();
|
||||
},
|
||||
m(): number {
|
||||
return this.now.getMonth() + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-special-message
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 4px
|
||||
text-align center
|
||||
font-size 14px
|
||||
font-weight bold
|
||||
text-transform uppercase
|
||||
color #fff
|
||||
background #ff1036
|
||||
|
||||
</style>
|
92
src/web/app/common/views/components/stream-indicator.vue
Normal file
92
src/web/app/common/views/components/stream-indicator.vue
Normal file
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<div class="mk-stream-indicator" v-if="stream">
|
||||
<p v-if=" stream.state == 'initializing' ">
|
||||
%fa:spinner .pulse%
|
||||
<span>%i18n:common.tags.mk-stream-indicator.connecting%<mk-ellipsis/></span>
|
||||
</p>
|
||||
<p v-if=" stream.state == 'reconnecting' ">
|
||||
%fa:spinner .pulse%
|
||||
<span>%i18n:common.tags.mk-stream-indicator.reconnecting%<mk-ellipsis/></span>
|
||||
</p>
|
||||
<p v-if=" stream.state == 'connected' ">
|
||||
%fa:check%
|
||||
<span>%i18n:common.tags.mk-stream-indicator.connected%</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as anime from 'animejs';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
stream: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.stream = (this as any).os.stream.borrow();
|
||||
|
||||
(this as any).os.stream.on('connected', this.onConnected);
|
||||
(this as any).os.stream.on('disconnected', this.onDisconnected);
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.stream.state == 'connected') {
|
||||
this.$el.style.opacity = '0';
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
(this as any).os.stream.off('connected', this.onConnected);
|
||||
(this as any).os.stream.off('disconnected', this.onDisconnected);
|
||||
},
|
||||
methods: {
|
||||
onConnected() {
|
||||
this.stream = (this as any).os.stream.borrow();
|
||||
|
||||
setTimeout(() => {
|
||||
anime({
|
||||
targets: this.$el,
|
||||
opacity: 0,
|
||||
easing: 'linear',
|
||||
duration: 200
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
onDisconnected() {
|
||||
this.stream = null;
|
||||
|
||||
anime({
|
||||
targets: this.$el,
|
||||
opacity: 1,
|
||||
easing: 'linear',
|
||||
duration: 100
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-stream-indicator
|
||||
pointer-events none
|
||||
position fixed
|
||||
z-index 16384
|
||||
bottom 8px
|
||||
right 8px
|
||||
margin 0
|
||||
padding 6px 12px
|
||||
font-size 0.9em
|
||||
color #fff
|
||||
background rgba(0, 0, 0, 0.8)
|
||||
border-radius 4px
|
||||
|
||||
> p
|
||||
display block
|
||||
margin 0
|
||||
|
||||
> [data-fa]
|
||||
margin-right 0.25em
|
||||
|
||||
</style>
|
76
src/web/app/common/views/components/time.vue
Normal file
76
src/web/app/common/views/components/time.vue
Normal file
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<time class="mk-time">
|
||||
<span v-if=" mode == 'relative' ">{{ relative }}</span>
|
||||
<span v-if=" mode == 'absolute' ">{{ absolute }}</span>
|
||||
<span v-if=" mode == 'detail' ">{{ absolute }} ({{ relative }})</span>
|
||||
</time>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
time: {
|
||||
type: [Date, String],
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'relative'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tickId: null,
|
||||
now: new Date()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
_time(): Date {
|
||||
return typeof this.time == 'string' ? new Date(this.time) : this.time;
|
||||
},
|
||||
absolute(): string {
|
||||
const time = this._time;
|
||||
return (
|
||||
time.getFullYear() + '年' +
|
||||
(time.getMonth() + 1) + '月' +
|
||||
time.getDate() + '日' +
|
||||
' ' +
|
||||
time.getHours() + '時' +
|
||||
time.getMinutes() + '分');
|
||||
},
|
||||
relative(): string {
|
||||
const time = this._time;
|
||||
const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/;
|
||||
return (
|
||||
ago >= 31536000 ? '%i18n:common.time.years_ago%' .replace('{}', (~~(ago / 31536000)).toString()) :
|
||||
ago >= 2592000 ? '%i18n:common.time.months_ago%' .replace('{}', (~~(ago / 2592000)).toString()) :
|
||||
ago >= 604800 ? '%i18n:common.time.weeks_ago%' .replace('{}', (~~(ago / 604800)).toString()) :
|
||||
ago >= 86400 ? '%i18n:common.time.days_ago%' .replace('{}', (~~(ago / 86400)).toString()) :
|
||||
ago >= 3600 ? '%i18n:common.time.hours_ago%' .replace('{}', (~~(ago / 3600)).toString()) :
|
||||
ago >= 60 ? '%i18n:common.time.minutes_ago%'.replace('{}', (~~(ago / 60)).toString()) :
|
||||
ago >= 10 ? '%i18n:common.time.seconds_ago%'.replace('{}', (~~(ago % 60)).toString()) :
|
||||
ago >= 0 ? '%i18n:common.time.just_now%' :
|
||||
ago < 0 ? '%i18n:common.time.future%' :
|
||||
'%i18n:common.time.unknown%');
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.mode == 'relative' || this.mode == 'detail') {
|
||||
this.tick();
|
||||
this.tickId = setInterval(this.tick, 1000);
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
if (this.mode === 'relative' || this.mode === 'detail') {
|
||||
clearInterval(this.tickId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tick() {
|
||||
this.now = new Date();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
64
src/web/app/common/views/components/twitter-setting.vue
Normal file
64
src/web/app/common/views/components/twitter-setting.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="mk-twitter-setting">
|
||||
<p>%i18n:common.tags.mk-twitter-setting.description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
|
||||
<p class="account" v-if="os.i.twitter" :title="`Twitter ID: ${os.i.twitter.user_id}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.twitter.screen_name}`" target="_blank">@{{ os.i.twitter.screen_name }}</a></p>
|
||||
<p>
|
||||
<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }}</a>
|
||||
<span v-if="os.i.twitter"> or </span>
|
||||
<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.twitter" @click.prevent="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a>
|
||||
</p>
|
||||
<p class="id" v-if="os.i.twitter">Twitter ID: {{ os.i.twitter.user_id }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { apiUrl, docsUrl } from '../../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
form: null,
|
||||
apiUrl,
|
||||
docsUrl
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'os.i'() {
|
||||
if ((this as any).os.i.twitter) {
|
||||
if (this.form) this.form.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
connect() {
|
||||
this.form = window.open(apiUrl + '/connect/twitter',
|
||||
'twitter_connect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
disconnect() {
|
||||
window.open(apiUrl + '/disconnect/twitter',
|
||||
'twitter_disconnect_window',
|
||||
'height=570, width=520');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-twitter-setting
|
||||
color #4a535a
|
||||
|
||||
.account
|
||||
border solid 1px #e1e8ed
|
||||
border-radius 4px
|
||||
padding 16px
|
||||
|
||||
a
|
||||
font-weight bold
|
||||
color inherit
|
||||
|
||||
.id
|
||||
color #8899a6
|
||||
</style>
|
210
src/web/app/common/views/components/uploader.vue
Normal file
210
src/web/app/common/views/components/uploader.vue
Normal file
|
@ -0,0 +1,210 @@
|
|||
<template>
|
||||
<div class="mk-uploader">
|
||||
<ol v-if="uploads.length > 0">
|
||||
<li v-for="ctx in uploads" :key="ctx.id">
|
||||
<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
|
||||
<p class="name">%fa:spinner .pulse%{{ ctx.name }}</p>
|
||||
<p class="status">
|
||||
<span class="initing" v-if="ctx.progress == undefined">%i18n:common.tags.mk-uploader.waiting%<mk-ellipsis/></span>
|
||||
<span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
|
||||
<span class="percentage" v-if="ctx.progress != undefined">{{ Math.floor((ctx.progress.value / ctx.progress.max) * 100) }}</span>
|
||||
</p>
|
||||
<progress v-if="ctx.progress != undefined && ctx.progress.value != ctx.progress.max" :value="ctx.progress.value" :max="ctx.progress.max"></progress>
|
||||
<div class="progress initing" v-if="ctx.progress == undefined"></div>
|
||||
<div class="progress waiting" v-if="ctx.progress != undefined && ctx.progress.value == ctx.progress.max"></div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { apiUrl } from '../../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
uploads: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
upload(file, folder) {
|
||||
if (folder && typeof folder == 'object') folder = folder.id;
|
||||
|
||||
const id = Math.random();
|
||||
|
||||
const ctx = {
|
||||
id: id,
|
||||
name: file.name || 'untitled',
|
||||
progress: undefined,
|
||||
img: undefined
|
||||
};
|
||||
|
||||
this.uploads.push(ctx);
|
||||
this.$emit('change', this.uploads);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e: any) => {
|
||||
ctx.img = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
const data = new FormData();
|
||||
data.append('i', (this as any).os.i.token);
|
||||
data.append('file', file);
|
||||
|
||||
if (folder) data.append('folder_id', folder);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||
xhr.onload = (e: any) => {
|
||||
const driveFile = JSON.parse(e.target.response);
|
||||
|
||||
this.$emit('uploaded', driveFile);
|
||||
|
||||
this.uploads = this.uploads.filter(x => x.id != id);
|
||||
this.$emit('change', this.uploads);
|
||||
};
|
||||
|
||||
xhr.upload.onprogress = e => {
|
||||
if (e.lengthComputable) {
|
||||
if (ctx.progress == undefined) ctx.progress = {};
|
||||
ctx.progress.max = e.total;
|
||||
ctx.progress.value = e.loaded;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-uploader
|
||||
overflow auto
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> ol
|
||||
display block
|
||||
margin 0
|
||||
padding 0
|
||||
list-style none
|
||||
|
||||
> li
|
||||
display block
|
||||
margin 8px 0 0 0
|
||||
padding 0
|
||||
height 36px
|
||||
box-shadow 0 -1px 0 rgba($theme-color, 0.1)
|
||||
border-top solid 8px transparent
|
||||
|
||||
&:first-child
|
||||
margin 0
|
||||
box-shadow none
|
||||
border-top none
|
||||
|
||||
> .img
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 36px
|
||||
height 36px
|
||||
background-size cover
|
||||
background-position center center
|
||||
|
||||
> .name
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
left 44px
|
||||
margin 0
|
||||
padding 0
|
||||
max-width 256px
|
||||
font-size 0.8em
|
||||
color rgba($theme-color, 0.7)
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
> .status
|
||||
display block
|
||||
position absolute
|
||||
top 0
|
||||
right 0
|
||||
margin 0
|
||||
padding 0
|
||||
font-size 0.8em
|
||||
|
||||
> .initing
|
||||
color rgba($theme-color, 0.5)
|
||||
|
||||
> .kb
|
||||
color rgba($theme-color, 0.5)
|
||||
|
||||
> .percentage
|
||||
display inline-block
|
||||
width 48px
|
||||
text-align right
|
||||
|
||||
color rgba($theme-color, 0.7)
|
||||
|
||||
&:after
|
||||
content '%'
|
||||
|
||||
> progress
|
||||
display block
|
||||
position absolute
|
||||
bottom 0
|
||||
right 0
|
||||
margin 0
|
||||
width calc(100% - 44px)
|
||||
height 8px
|
||||
background transparent
|
||||
border none
|
||||
border-radius 4px
|
||||
overflow hidden
|
||||
|
||||
&::-webkit-progress-value
|
||||
background $theme-color
|
||||
|
||||
&::-webkit-progress-bar
|
||||
background rgba($theme-color, 0.1)
|
||||
|
||||
> .progress
|
||||
display block
|
||||
position absolute
|
||||
bottom 0
|
||||
right 0
|
||||
margin 0
|
||||
width calc(100% - 44px)
|
||||
height 8px
|
||||
border none
|
||||
border-radius 4px
|
||||
background linear-gradient(
|
||||
45deg,
|
||||
lighten($theme-color, 30%) 25%,
|
||||
$theme-color 25%,
|
||||
$theme-color 50%,
|
||||
lighten($theme-color, 30%) 50%,
|
||||
lighten($theme-color, 30%) 75%,
|
||||
$theme-color 75%,
|
||||
$theme-color
|
||||
)
|
||||
background-size 32px 32px
|
||||
animation bg 1.5s linear infinite
|
||||
|
||||
&.initing
|
||||
opacity 0.3
|
||||
|
||||
@keyframes bg
|
||||
from {background-position: 0 0;}
|
||||
to {background-position: -64px 32px;}
|
||||
|
||||
</style>
|
123
src/web/app/common/views/components/url-preview.vue
Normal file
123
src/web/app/common/views/components/url-preview.vue
Normal file
|
@ -0,0 +1,123 @@
|
|||
<template>
|
||||
<a class="mk-url-preview" :href="url" target="_blank" :title="url" v-if="!fetching">
|
||||
<div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div>
|
||||
<article>
|
||||
<header>
|
||||
<h1>{{ title }}</h1>
|
||||
</header>
|
||||
<p>{{ description }}</p>
|
||||
<footer>
|
||||
<img class="icon" v-if="icon" :src="icon"/>
|
||||
<p>{{ sitename }}</p>
|
||||
</footer>
|
||||
</article>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['url'],
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
title: null,
|
||||
description: null,
|
||||
thumbnail: null,
|
||||
icon: null,
|
||||
sitename: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
fetch('/api:url?url=' + this.url).then(res => {
|
||||
res.json().then(info => {
|
||||
this.title = info.title;
|
||||
this.description = info.description;
|
||||
this.thumbnail = info.thumbnail;
|
||||
this.icon = info.icon;
|
||||
this.sitename = info.sitename;
|
||||
|
||||
this.fetching = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-url-preview
|
||||
display block
|
||||
font-size 16px
|
||||
border solid 1px #eee
|
||||
border-radius 4px
|
||||
overflow hidden
|
||||
|
||||
&:hover
|
||||
text-decoration none
|
||||
border-color #ddd
|
||||
|
||||
> article > header > h1
|
||||
text-decoration underline
|
||||
|
||||
> .thumbnail
|
||||
position absolute
|
||||
width 100px
|
||||
height 100%
|
||||
background-position center
|
||||
background-size cover
|
||||
|
||||
& + article
|
||||
left 100px
|
||||
width calc(100% - 100px)
|
||||
|
||||
> article
|
||||
padding 16px
|
||||
|
||||
> header
|
||||
margin-bottom 8px
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
font-size 1em
|
||||
color #555
|
||||
|
||||
> p
|
||||
margin 0
|
||||
color #777
|
||||
font-size 0.8em
|
||||
|
||||
> footer
|
||||
margin-top 8px
|
||||
height 16px
|
||||
|
||||
> img
|
||||
display inline-block
|
||||
width 16px
|
||||
height 16px
|
||||
margin-right 4px
|
||||
vertical-align top
|
||||
|
||||
> p
|
||||
display inline-block
|
||||
margin 0
|
||||
color #666
|
||||
font-size 0.8em
|
||||
line-height 16px
|
||||
vertical-align top
|
||||
|
||||
@media (max-width 500px)
|
||||
font-size 8px
|
||||
border none
|
||||
|
||||
> .thumbnail
|
||||
width 70px
|
||||
|
||||
& + article
|
||||
left 70px
|
||||
width calc(100% - 70px)
|
||||
|
||||
> article
|
||||
padding 8px
|
||||
|
||||
</style>
|
66
src/web/app/common/views/components/url.vue
Normal file
66
src/web/app/common/views/components/url.vue
Normal file
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<a class="mk-url" :href="url" :target="target">
|
||||
<span class="schema">{{ schema }}//</span>
|
||||
<span class="hostname">{{ hostname }}</span>
|
||||
<span class="port" v-if="port != ''">:{{ port }}</span>
|
||||
<span class="pathname" v-if="pathname != ''">{{ pathname }}</span>
|
||||
<span class="query">{{ query }}</span>
|
||||
<span class="hash">{{ hash }}</span>
|
||||
%fa:external-link-square-alt%
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['url', 'target'],
|
||||
data() {
|
||||
return {
|
||||
schema: null,
|
||||
hostname: null,
|
||||
port: null,
|
||||
pathname: null,
|
||||
query: null,
|
||||
hash: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
const url = new URL(this.url);
|
||||
|
||||
this.schema = url.protocol;
|
||||
this.hostname = url.hostname;
|
||||
this.port = url.port;
|
||||
this.pathname = url.pathname;
|
||||
this.query = url.search;
|
||||
this.hash = url.hash;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-url
|
||||
word-break break-all
|
||||
|
||||
> [data-fa]
|
||||
padding-left 2px
|
||||
font-size .9em
|
||||
font-weight 400
|
||||
font-style normal
|
||||
|
||||
> .schema
|
||||
opacity 0.5
|
||||
|
||||
> .hostname
|
||||
font-weight bold
|
||||
|
||||
> .pathname
|
||||
opacity 0.8
|
||||
|
||||
> .query
|
||||
opacity 0.5
|
||||
|
||||
> .hash
|
||||
font-style italic
|
||||
|
||||
</style>
|
5
src/web/app/common/views/directives/focus.ts
Normal file
5
src/web/app/common/views/directives/focus.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
inserted(el) {
|
||||
el.focus();
|
||||
}
|
||||
};
|
5
src/web/app/common/views/directives/index.ts
Normal file
5
src/web/app/common/views/directives/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import focus from './focus';
|
||||
|
||||
Vue.directive('focus', focus);
|
29
src/web/app/config.ts
Normal file
29
src/web/app/config.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
declare const _HOST_: string;
|
||||
declare const _URL_: string;
|
||||
declare const _API_URL_: string;
|
||||
declare const _DOCS_URL_: string;
|
||||
declare const _STATS_URL_: string;
|
||||
declare const _STATUS_URL_: string;
|
||||
declare const _DEV_URL_: string;
|
||||
declare const _CH_URL_: string;
|
||||
declare const _LANG_: string;
|
||||
declare const _RECAPTCHA_SITEKEY_: string;
|
||||
declare const _SW_PUBLICKEY_: string;
|
||||
declare const _THEME_COLOR_: string;
|
||||
declare const _COPYRIGHT_: string;
|
||||
declare const _VERSION_: string;
|
||||
|
||||
export const host = _HOST_;
|
||||
export const url = _URL_;
|
||||
export const apiUrl = _API_URL_;
|
||||
export const docsUrl = _DOCS_URL_;
|
||||
export const statsUrl = _STATS_URL_;
|
||||
export const statusUrl = _STATUS_URL_;
|
||||
export const devUrl = _DEV_URL_;
|
||||
export const chUrl = _CH_URL_;
|
||||
export const lang = _LANG_;
|
||||
export const recaptchaSitekey = _RECAPTCHA_SITEKEY_;
|
||||
export const swPublickey = _SW_PUBLICKEY_;
|
||||
export const themeColor = _THEME_COLOR_;
|
||||
export const copyright = _COPYRIGHT_;
|
||||
export const version = _VERSION_;
|
30
src/web/app/desktop/api/choose-drive-file.ts
Normal file
30
src/web/app/desktop/api/choose-drive-file.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { url } from '../../config';
|
||||
import MkChooseFileFromDriveWindow from '../views/components/choose-file-from-drive-window.vue';
|
||||
|
||||
export default function(opts) {
|
||||
return new Promise((res, rej) => {
|
||||
const o = opts || {};
|
||||
|
||||
if (document.body.clientWidth > 800) {
|
||||
const w = new MkChooseFileFromDriveWindow({
|
||||
propsData: {
|
||||
title: o.title,
|
||||
multiple: o.multiple,
|
||||
initFolder: o.currentFolder
|
||||
}
|
||||
}).$mount();
|
||||
w.$once('selected', file => {
|
||||
res(file);
|
||||
});
|
||||
document.body.appendChild(w.$el);
|
||||
} else {
|
||||
window['cb'] = file => {
|
||||
res(file);
|
||||
};
|
||||
|
||||
window.open(url + '/selectdrive',
|
||||
'drive_window',
|
||||
'height=500, width=800');
|
||||
}
|
||||
});
|
||||
}
|
17
src/web/app/desktop/api/choose-drive-folder.ts
Normal file
17
src/web/app/desktop/api/choose-drive-folder.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import MkChooseFolderFromDriveWindow from '../views/components/choose-folder-from-drive-window.vue';
|
||||
|
||||
export default function(opts) {
|
||||
return new Promise((res, rej) => {
|
||||
const o = opts || {};
|
||||
const w = new MkChooseFolderFromDriveWindow({
|
||||
propsData: {
|
||||
title: o.title,
|
||||
initFolder: o.currentFolder
|
||||
}
|
||||
}).$mount();
|
||||
w.$once('selected', folder => {
|
||||
res(folder);
|
||||
});
|
||||
document.body.appendChild(w.$el);
|
||||
});
|
||||
}
|
16
src/web/app/desktop/api/contextmenu.ts
Normal file
16
src/web/app/desktop/api/contextmenu.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Ctx from '../views/components/context-menu.vue';
|
||||
|
||||
export default function(e, menu, opts?) {
|
||||
const o = opts || {};
|
||||
const vm = new Ctx({
|
||||
propsData: {
|
||||
menu,
|
||||
x: e.pageX - window.pageXOffset,
|
||||
y: e.pageY - window.pageYOffset,
|
||||
}
|
||||
}).$mount();
|
||||
vm.$once('closed', () => {
|
||||
if (o.closed) o.closed();
|
||||
});
|
||||
document.body.appendChild(vm.$el);
|
||||
}
|
19
src/web/app/desktop/api/dialog.ts
Normal file
19
src/web/app/desktop/api/dialog.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Dialog from '../views/components/dialog.vue';
|
||||
|
||||
export default function(opts) {
|
||||
return new Promise<string>((res, rej) => {
|
||||
const o = opts || {};
|
||||
const d = new Dialog({
|
||||
propsData: {
|
||||
title: o.title,
|
||||
text: o.text,
|
||||
modal: o.modal,
|
||||
buttons: o.actions
|
||||
}
|
||||
}).$mount();
|
||||
d.$once('clicked', id => {
|
||||
res(id);
|
||||
});
|
||||
document.body.appendChild(d.$el);
|
||||
});
|
||||
}
|
20
src/web/app/desktop/api/input.ts
Normal file
20
src/web/app/desktop/api/input.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import InputDialog from '../views/components/input-dialog.vue';
|
||||
|
||||
export default function(opts) {
|
||||
return new Promise<string>((res, rej) => {
|
||||
const o = opts || {};
|
||||
const d = new InputDialog({
|
||||
propsData: {
|
||||
title: o.title,
|
||||
placeholder: o.placeholder,
|
||||
default: o.default,
|
||||
type: o.type || 'text',
|
||||
allowEmpty: o.allowEmpty
|
||||
}
|
||||
}).$mount();
|
||||
d.$once('done', text => {
|
||||
res(text);
|
||||
});
|
||||
document.body.appendChild(d.$el);
|
||||
});
|
||||
}
|
10
src/web/app/desktop/api/notify.ts
Normal file
10
src/web/app/desktop/api/notify.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import Notification from '../views/components/ui-notification.vue';
|
||||
|
||||
export default function(message) {
|
||||
const vm = new Notification({
|
||||
propsData: {
|
||||
message
|
||||
}
|
||||
}).$mount();
|
||||
document.body.appendChild(vm.$el);
|
||||
}
|
21
src/web/app/desktop/api/post.ts
Normal file
21
src/web/app/desktop/api/post.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import PostFormWindow from '../views/components/post-form-window.vue';
|
||||
import RepostFormWindow from '../views/components/repost-form-window.vue';
|
||||
|
||||
export default function(opts) {
|
||||
const o = opts || {};
|
||||
if (o.repost) {
|
||||
const vm = new RepostFormWindow({
|
||||
propsData: {
|
||||
repost: o.repost
|
||||
}
|
||||
}).$mount();
|
||||
document.body.appendChild(vm.$el);
|
||||
} else {
|
||||
const vm = new PostFormWindow({
|
||||
propsData: {
|
||||
reply: o.reply
|
||||
}
|
||||
}).$mount();
|
||||
document.body.appendChild(vm.$el);
|
||||
}
|
||||
}
|
98
src/web/app/desktop/api/update-avatar.ts
Normal file
98
src/web/app/desktop/api/update-avatar.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
import OS from '../../common/mios';
|
||||
import { apiUrl } from '../../config';
|
||||
import CropWindow from '../views/components/crop-window.vue';
|
||||
import ProgressDialog from '../views/components/progress-dialog.vue';
|
||||
|
||||
export default (os: OS) => (cb, file = null) => {
|
||||
const fileSelected = file => {
|
||||
|
||||
const w = new CropWindow({
|
||||
propsData: {
|
||||
image: file,
|
||||
title: 'アバターとして表示する部分を選択',
|
||||
aspectRatio: 1 / 1
|
||||
}
|
||||
}).$mount();
|
||||
|
||||
w.$once('cropped', blob => {
|
||||
const data = new FormData();
|
||||
data.append('i', os.i.token);
|
||||
data.append('file', blob, file.name + '.cropped.png');
|
||||
|
||||
os.api('drive/folders/find', {
|
||||
name: 'アイコン'
|
||||
}).then(iconFolder => {
|
||||
if (iconFolder.length === 0) {
|
||||
os.api('drive/folders/create', {
|
||||
name: 'アイコン'
|
||||
}).then(iconFolder => {
|
||||
upload(data, iconFolder);
|
||||
});
|
||||
} else {
|
||||
upload(data, iconFolder[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
w.$once('skipped', () => {
|
||||
set(file);
|
||||
});
|
||||
|
||||
document.body.appendChild(w.$el);
|
||||
};
|
||||
|
||||
const upload = (data, folder) => {
|
||||
const dialog = new ProgressDialog({
|
||||
propsData: {
|
||||
title: '新しいアバターをアップロードしています'
|
||||
}
|
||||
}).$mount();
|
||||
document.body.appendChild(dialog.$el);
|
||||
|
||||
if (folder) data.append('folder_id', folder.id);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||
xhr.onload = e => {
|
||||
const file = JSON.parse((e.target as any).response);
|
||||
(dialog as any).close();
|
||||
set(file);
|
||||
};
|
||||
|
||||
xhr.upload.onprogress = e => {
|
||||
if (e.lengthComputable) (dialog as any).update(e.loaded, e.total);
|
||||
};
|
||||
|
||||
xhr.send(data);
|
||||
};
|
||||
|
||||
const set = file => {
|
||||
os.api('i/update', {
|
||||
avatar_id: file.id
|
||||
}).then(i => {
|
||||
os.i.avatar_id = i.avatar_id;
|
||||
os.i.avatar_url = i.avatar_url;
|
||||
|
||||
os.apis.dialog({
|
||||
title: '%fa:info-circle%アバターを更新しました',
|
||||
text: '新しいアバターが反映されるまで時間がかかる場合があります。',
|
||||
actions: [{
|
||||
text: 'わかった'
|
||||
}]
|
||||
});
|
||||
|
||||
if (cb) cb(i);
|
||||
});
|
||||
};
|
||||
|
||||
if (file) {
|
||||
fileSelected(file);
|
||||
} else {
|
||||
os.apis.chooseDriveFile({
|
||||
multiple: false,
|
||||
title: '%fa:image%アバターにする画像を選択'
|
||||
}).then(file => {
|
||||
fileSelected(file);
|
||||
});
|
||||
}
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue