Compare commits

..

67 Commits

Author SHA1 Message Date
7f7d7edc7f 2.40.0 2018-06-15 09:53:53 +09:00
bd827f946a ドライブのファイルの削除を実装 2018-06-15 09:53:30 +09:00
ad8aa1c179 Fix 2018-06-15 09:33:25 +09:00
3ebaf83ce0 2.39.0 2018-06-15 08:00:14 +09:00
39b1978ff3 Merge pull request #1713 from syuilo/without-vue-material
Without vue material
2018-06-15 07:58:58 +09:00
bddff17e5e wip 2018-06-15 07:58:27 +09:00
0ac9120064 wip 2018-06-15 07:56:56 +09:00
d90f75425f wip 2018-06-14 21:38:39 +09:00
dec7d537dc wip 2018-06-14 20:23:50 +09:00
11e95ea092 wip 2018-06-14 18:57:54 +09:00
c5e9b69eb3 wip 2018-06-14 18:53:02 +09:00
120c11b181 wip 2018-06-14 16:48:49 +09:00
a1ae832129 wip 2018-06-14 14:52:37 +09:00
3a4833818f wip 2018-06-14 09:51:55 +09:00
8814fc9c9c wip 2018-06-14 07:22:50 +09:00
e6e02ece89 wip 2018-06-14 06:29:01 +09:00
9059c149dd 2.38.3 2018-06-13 05:40:27 +09:00
7d8e70b2ac Fix bug 2018-06-13 05:40:12 +09:00
89105f5641 2.38.2 2018-06-13 05:25:27 +09:00
1813d17b4c Fix bug 2018-06-13 05:24:44 +09:00
ce27b36fd0 Fix bug 2018-06-13 05:21:55 +09:00
e635a87628 2.38.1 2018-06-13 05:16:53 +09:00
80c52433cc Fix bug 2018-06-13 05:15:26 +09:00
1472f0b141 Fix #1712 2018-06-13 05:11:55 +09:00
4d914f5c0a 2.38.0 2018-06-12 19:07:36 +09:00
0318f7344f Fix bug 2018-06-12 19:05:40 +09:00
413fbb3d0c モバイルでもハッシュタグを検索できるように 2018-06-12 19:03:57 +09:00
8bc47baf4f #1710 2018-06-12 18:54:36 +09:00
e3f6d42a47 Fix bug 2018-06-12 11:38:44 +09:00
8230935fd3 🎨 2018-06-12 11:27:35 +09:00
f968d05ea0 2.37.7 2018-06-12 09:10:52 +09:00
d6e5dc2167 Fix bugs 2018-06-12 09:10:34 +09:00
460147fea2 2.37.6 2018-06-12 08:59:36 +09:00
cea44834bb Improve usability 2018-06-12 08:58:50 +09:00
1af50fd7b8 冗長なハッシュタグの表示を無くした 2018-06-12 08:43:48 +09:00
b18013025f 🎨 2018-06-12 08:09:27 +09:00
399eb60809 2.37.5 2018-06-12 02:47:17 +09:00
ed67e3506b ✌️ 2018-06-12 02:46:54 +09:00
d8ff37fc45 ✌️ 2018-06-12 02:28:28 +09:00
2fcc3bb1ea 2.37.4 2018-06-12 02:19:00 +09:00
2e680c3d1e Fix bug 2018-06-12 02:18:29 +09:00
af0a0ef41b Merge branch 'master' of https://github.com/syuilo/misskey 2018-06-12 02:03:26 +09:00
bbfccb0bbf 🎨 2018-06-12 02:03:18 +09:00
c89eb5d69f Merge pull request #1705 from syuilo/l10n_master
New Crowdin translations
2018-06-12 02:03:06 +09:00
ebde84214e Improve hashtag trend detection 2018-06-12 02:00:05 +09:00
03fbae7b6d 変数調整 2018-06-12 01:51:51 +09:00
f90e9596d4 Fix bug 2018-06-12 01:48:29 +09:00
944f9524e2 Fix bug 2018-06-12 01:45:58 +09:00
c61050244e 変数調整 2018-06-12 01:43:56 +09:00
90337adbbc Improve hashtag trend detection 2018-06-12 01:41:17 +09:00
7b67e41c5b New translations ja.yml (Polish) 2018-06-11 19:22:16 +09:00
91db24fcfc 2.37.3 2018-06-11 14:16:46 +09:00
bb53db905f ✌️ 2018-06-11 14:16:21 +09:00
0e9a1efe46 Merge pull request #1704 from syuilo/l10n_master
New Crowdin translations
2018-06-11 14:11:33 +09:00
289cd3e200 New translations ja.yml (English) 2018-06-11 14:10:47 +09:00
e0f847e539 ✌️ 2018-06-11 14:06:23 +09:00
c2842b486e New translations ja.yml (Portuguese) 2018-06-11 13:51:15 +09:00
7235ade42f New translations ja.yml (Korean) 2018-06-11 13:51:13 +09:00
850be2df1d New translations ja.yml (Polish) 2018-06-11 13:51:11 +09:00
d504501440 New translations ja.yml (Chinese Simplified) 2018-06-11 13:51:09 +09:00
208392f12c New translations ja.yml (Italian) 2018-06-11 13:51:07 +09:00
0fe036c640 New translations ja.yml (Russian) 2018-06-11 13:51:05 +09:00
a40c41f0b0 New translations ja.yml (English) 2018-06-11 13:51:03 +09:00
4affa5b710 New translations ja.yml (Spanish) 2018-06-11 13:51:01 +09:00
4eb574d991 New translations ja.yml (German) 2018-06-11 13:50:59 +09:00
2c1577ea24 New translations ja.yml (French) 2018-06-11 13:50:56 +09:00
b87e7e50b6 Clean up 2018-06-11 13:49:53 +09:00
59 changed files with 1812 additions and 640 deletions

View File

@ -227,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "Serverinformationen"
toggle: "Sicht umschalten"

View File

@ -227,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "Toggle views"
common/views/widgets/hashtags.vue:
title: "Hashtags"
count: "{} users mentioned"
common/views/widgets/server.vue:
title: "Server info"
toggle: "Toggle views"

View File

@ -227,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"

View File

@ -227,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "Info sur le serveur"
toggle: "Afficher les vues"

View File

@ -227,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"

View File

@ -258,6 +258,7 @@ common/views/widgets/posts-monitor.vue:
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
empty: "トレンドなし"
common/views/widgets/server.vue:
title: "サーバー情報"

View File

@ -227,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"

View File

@ -70,7 +70,7 @@ common:
donation: "Dotacje"
nav: "Nawigacja"
tips: "Wskazówki"
hashtags: "ハッシュタグ"
hashtags: "Hashtagi"
deck:
widgets: "Widżety"
home: "Strona główna"
@ -226,7 +226,8 @@ common/views/widgets/posts-monitor.vue:
title: "Wykres wpisów"
toggle: "Przełącz widok"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
title: "Hashtagi"
count: "Wspomniany przez {} użytkowników"
common/views/widgets/server.vue:
title: "Informacje o serwerze"
toggle: "Przełącz widok"

View File

@ -227,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"

View File

@ -227,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"

View File

@ -227,6 +227,7 @@ common/views/widgets/posts-monitor.vue:
toggle: "表示を切り替え"
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
count: "{}人が投稿"
common/views/widgets/server.vue:
title: "サーバー情報"
toggle: "表示を切り替え"

View File

@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "2.37.2",
"clientVersion": "1.0.6437",
"version": "2.40.0",
"clientVersion": "1.0.6504",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@ -211,7 +211,6 @@
"vue-js-modal": "1.3.13",
"vue-json-tree-view": "2.1.4",
"vue-loader": "15.2.1",
"vue-material": "^1.0.0-beta-10.2",
"vue-router": "3.0.1",
"vue-template-compiler": "2.5.16",
"vuedraggable": "2.16.0",

View File

@ -7,11 +7,6 @@ html
cursor progress !important
body
// for md
font-size 16px !important
line-height initial !important
letter-spacing initial !important
overflow-wrap break-word
#error

View File

@ -29,6 +29,14 @@ import fileTypeIcon from './file-type-icon.vue';
import Switch from './switch.vue';
import Othello from './othello.vue';
import welcomeTimeline from './welcome-timeline.vue';
import uiInput from './ui/input.vue';
import uiButton from './ui/button.vue';
import uiCard from './ui/card.vue';
import uiForm from './ui/form.vue';
import uiTextarea from './ui/textarea.vue';
import uiSwitch from './ui/switch.vue';
import uiRadio from './ui/radio.vue';
import uiSelect from './ui/select.vue';
Vue.component('mk-analog-clock', analogClock);
Vue.component('mk-menu', menu);
@ -59,3 +67,11 @@ Vue.component('mk-file-type-icon', fileTypeIcon);
Vue.component('mk-switch', Switch);
Vue.component('mk-othello', Othello);
Vue.component('mk-welcome-timeline', welcomeTimeline);
Vue.component('ui-input', uiInput);
Vue.component('ui-button', uiButton);
Vue.component('ui-card', uiCard);
Vue.component('ui-form', uiForm);
Vue.component('ui-textarea', uiTextarea);
Vue.component('ui-switch', uiSwitch);
Vue.component('ui-radio', uiRadio);
Vue.component('ui-select', uiSelect);

View File

@ -40,6 +40,17 @@ export default Vue.component('mk-note-html', {
ast = this.ast;
}
if (ast.filter(x => x.type != 'hashtag').length == 0) {
return;
}
while (ast[ast.length - 1] && (
ast[ast.length - 1].type == 'hashtag' ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n'))) {
ast.pop();
}
// Parse ast to DOM
const els = flatten(ast.map(token => {
switch (token.type) {
@ -92,7 +103,7 @@ export default Vue.component('mk-note-html', {
case 'hashtag':
return createElement('a', {
attrs: {
href: `${url}/search?q=${token.content}`,
href: `${url}/tags/${token.hashtag}`,
target: '_blank'
}
}, token.content);

View File

@ -1,60 +1,58 @@
<template>
<form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off">
<label class="username">
<p class="caption">%fa:at%%i18n:@username%</p>
<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,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:@checking%</p>
<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:@available%</p>
<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@unavailable%</p>
<p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@error%</p>
<p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@invalid-format%</p>
<p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-short%</p>
<p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@too-long%</p>
</label>
<label class="password">
<p class="caption">%fa:lock%%i18n:@password%</p>
<input v-model="password" type="password" placeholder="%i18n:@password-placeholder%" autocomplete="off" required @input="onChangePassword"/>
<div class="meter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
<div class="value" ref="passwordMetar"></div>
<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" required @input="onChangeUsername">
<span>%i18n:@username%</span>
<span slot="prefix">@</span>
<span slot="suffix">@{{ host }}</span>
<p slot="text" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw% %i18n:@checking%</p>
<p slot="text" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw% %i18n:@available%</p>
<p slot="text" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@unavailable%</p>
<p slot="text" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@error%</p>
<p slot="text" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@invalid-format%</p>
<p slot="text" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-short%</p>
<p slot="text" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-long%</p>
</ui-input>
<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true">
<span>%i18n:@password%</span>
<span slot="prefix">%fa:lock%</span>
<div slot="text">
<p slot="text" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@weak-password%</p>
<p slot="text" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw% %i18n:@normal-password%</p>
<p slot="text" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw% %i18n:@strong-password%</p>
</div>
<p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@weak-password%</p>
<p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:@normal-password%</p>
<p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:@strong-password%</p>
</label>
<label class="retype-password">
<p class="caption">%fa:lock%%i18n:@password%(%i18n:@retype%)</p>
<input v-model="retypedPassword" type="password" placeholder="%i18n:@retype-placeholder%" autocomplete="off" required @input="onChangePasswordRetype"/>
<p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:@password-matched%</p>
<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:@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:@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/>
</ui-input>
<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype">
<span>%i18n:@password% (%i18n:@retype%)</span>
<span slot="prefix">%fa:lock%</span>
<div slot="text">
<p slot="text" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw% %i18n:@password-matched%</p>
<p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@password-not-matched%</p>
</div>
</ui-input>
<div class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div>
<label class="agree-tou" style="display: block; margin: 16px 0;">
<input name="agree-tou" type="checkbox" required/>
<p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p>
</label>
<button type="submit">%i18n:@create%</button>
<ui-button type="submit">%i18n:@create%</ui-button>
</form>
</template>
<script lang="ts">
import Vue from 'vue';
const getPasswordStrength = require('syuilo-password-strength');
import { url, docsUrl, lang, recaptchaSitekey } from '../../../config';
import { host, url, docsUrl, lang, recaptchaSitekey } from '../../../config';
export default Vue.extend({
data() {
return {
host,
username: '',
password: '',
retypedPassword: '',
url,
touUrl: `${docsUrl}/${lang}/tou`,
recaptchaSitekey,
recaptchaed: false,
usernameState: null,
passwordStrength: '',
passwordRetypeState: null
@ -104,7 +102,6 @@ export default Vue.extend({
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 == '') {
@ -130,19 +127,9 @@ export default Vue.extend({
alert('%i18n:@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');
@ -158,100 +145,6 @@ export default Vue.extend({
.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(#000, 0.1)
border-radius 4px
box-shadow 0 0 0 114514px #fff inset
transition all .3s ease
&:hover
border-color rgba(#000, 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
@ -269,19 +162,4 @@ export default Vue.extend({
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>

View File

@ -0,0 +1,82 @@
<template>
<div class="ui-button" :class="[styl]">
<button :type="type" @click="$emit('click')">
<slot></slot>
</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
type: {
type: String,
required: false
}
},
data() {
return {
styl: 'fill'
};
},
inject: {
isCardChild: { default: false }
},
created() {
if (this.isCardChild) {
this.styl = 'line';
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark, fill)
> button
display block
width 100%
margin 0
padding 0
font-weight bold
font-size 16px
line-height 44px
border none
border-radius 6px
outline none
box-shadow none
if fill
color $theme-color-foreground
background $theme-color
&:hover
background lighten($theme-color, 5%)
&:active
background darken($theme-color, 5%)
else
color $theme-color
background none
&:hover
color darken($theme-color, 5%)
&:active
background rgba($theme-color, 0.3)
.ui-button[data-darkmode]
&.fill
root(true, true)
&:not(.fill)
root(true, false)
.ui-button:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
</style>

View File

@ -0,0 +1,46 @@
<template>
<div class="ui-card">
<header>
<slot name="title"></slot>
</header>
<slot></slot>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
provide() {
return {
isCardChild: true
};
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
margin 16px
padding 16px
color isDark ? #fff : #000
background isDark ? #282C37 : #fff
box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
@media (min-width 500px)
padding 32px
> header
font-weight normal
font-size 24px
color isDark ? #fff : #444
.ui-card[data-darkmode]
root(true)
.ui-card:not([data-darkmode])
root(false)
</style>

View File

@ -0,0 +1,30 @@
<template>
<div class="ui-form">
<fieldset :disabled="disabled">
<slot></slot>
</fieldset>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
disabled: {
type: Boolean,
required: false
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.ui-form
> fieldset
margin 0
padding 0
border none
</style>

View File

@ -0,0 +1,322 @@
<template>
<div class="ui-input" :class="[{ focused, filled }, styl]">
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input" @click="focus" @mousedown="focus">
<div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
<div class="value" ref="passwordMetar"></div>
</div>
<span class="label" ref="label"><slot></slot></span>
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<template v-if="type != 'file'">
<input ref="input"
:type="type"
:value="v"
:required="required"
:readonly="readonly"
:pattern="pattern"
:autocomplete="autocomplete"
@input="$emit('input', $event.target.value)"
@focus="focused = true"
@blur="focused = false">
</template>
<template v-else>
<input ref="input"
type="text"
:value="placeholder"
readonly
@click="chooseFile">
<input ref="file"
type="file"
:value="value"
@change="onChangeFile">
</template>
<div class="suffix"><slot name="suffix"></slot></div>
</div>
<div class="text"><slot name="text"></slot></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
const getPasswordStrength = require('syuilo-password-strength');
export default Vue.extend({
props: {
value: {
required: false
},
type: {
type: String,
required: false
},
required: {
type: Boolean,
required: false
},
readonly: {
type: Boolean,
required: false
},
pattern: {
type: String,
required: false
},
autocomplete: {
required: false
},
withPasswordMeter: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
v: this.value,
focused: false,
passwordStrength: '',
styl: 'fill'
};
},
computed: {
filled(): boolean {
return this.v != '' && this.v != null;
},
placeholder(): string {
if (this.type != 'file') return null;
if (this.v == null) return null;
if (typeof this.v == 'string') return this.v;
if (Array.isArray(this.v)) {
return this.v.map(file => file.name).join(', ');
} else {
return this.v.name;
}
}
},
watch: {
value(v) {
this.v = v;
},
v(v) {
if (this.withPasswordMeter) {
if (v == '') {
this.passwordStrength = '';
return;
}
const strength = getPasswordStrength(v);
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
}
}
},
inject: {
isCardChild: { default: false }
},
created() {
if (this.isCardChild) {
this.styl = 'line';
}
},
mounted() {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
}
},
methods: {
focus() {
this.$refs.input.focus();
},
chooseFile() {
this.$refs.file.click();
},
onChangeFile() {
this.v = Array.from((this.$refs.file as any).files);
this.$emit('input', this.v);
this.$emit('change', this.v);
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark, fill)
margin 32px 0
> .icon
position absolute
top 0
left 0
width 24px
text-align center
line-height 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
&:not(:empty) + .input
margin-left 28px
> .input
display flex
cursor text
if fill
padding 6px 12px
background rgba(#000, 0.035)
border-radius 6px
else
&:before
content ''
display block
position absolute
bottom 0
left 0
right 0
height 1px
background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
&:after
content ''
display block
position absolute
bottom 0
left 0
right 0
height 2px
background $theme-color
opacity 0
transform scaleX(0.12)
transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
will-change border opacity transform
> .password-meter
position absolute
top 0
left 0
width 100%
height 100%
border-radius 6px
overflow hidden
opacity 0.3
&[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 6px
transition all 0.1s ease
> .label
position absolute
top fill ? 6px : 0
left 0
pointer-events none
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
transition-duration 0.3s
font-size 16px
line-height 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
pointer-events none
//will-change transform
transform-origin top left
transform scale(1)
> input
display block
flex 1
width 100%
margin 0
padding 0
font inherit
font-weight fill ? bold : normal
font-size 16px
line-height 32px
color isDark ? #fff : #000
background transparent
border none
border-radius 0
outline none
box-shadow none
&[type='file']
display none
> .prefix
> .suffix
display block
align-self center
justify-self center
font-size 16px
line-height 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
pointer-events none
> *
display block
min-width 16px
> .prefix
padding-right 4px
> .suffix
padding-left 4px
> .text
margin 6px 0
font-size 13px
*
margin 0
&.focused
> .input
if fill
background rgba(#000, 0.05)
else
&:after
opacity 1
transform scaleX(1)
> .label
color $theme-color
&.focused
&.filled
> .input
> .label
top fill ? -24px : -17px
left 0 !important
transform scale(0.75)
.ui-input[data-darkmode]
&.fill
root(true, true)
&:not(.fill)
root(true, false)
.ui-input:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
</style>

View File

@ -0,0 +1,120 @@
<template>
<div
class="ui-radio"
:class="{ disabled, checked }"
:aria-checked="checked"
:aria-disabled="disabled"
@click="toggle"
>
<input type="radio"
:disabled="disabled"
>
<span class="button">
<span></span>
</span>
<span class="label"><slot></slot></span>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
model: {
prop: 'model',
event: 'change'
},
props: {
model: {
type: String,
required: false
},
value: {
type: String,
required: false
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
checked(): boolean {
return this.model === this.value;
}
},
methods: {
toggle() {
this.$emit('change', this.value);
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
display inline-block
margin 32px 32px 32px 0
cursor pointer
transition all 0.3s
> *
user-select none
&.disabled
opacity 0.6
cursor not-allowed
&.checked
> .button
border-color $theme-color
&:after
background-color $theme-color
transform scale(1)
opacity 1
> input
position absolute
width 0
height 0
opacity 0
margin 0
> .button
position absolute
width 20px
height 20px
background none
border solid 2px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
border-radius 100%
transition inherit
&:after
content ''
display block
position absolute
top 3px
right 3px
bottom 3px
left 3px
border-radius 100%
opacity 0
transform scale(0)
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
> .label
margin-left 28px
display block
font-size 16px
line-height 20px
cursor pointer
.ui-radio[data-darkmode]
root(true)
.ui-radio:not([data-darkmode])
root(false)
</style>

View File

@ -0,0 +1,215 @@
<template>
<div class="ui-select" :class="[{ focused, filled }, styl]">
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input" @click="focus">
<span class="label" ref="label"><slot name="label"></slot></span>
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<select ref="input"
:value="v"
:required="required"
@input="$emit('input', $event.target.value)"
@focus="focused = true"
@blur="focused = false">
<slot></slot>
</select>
<div class="suffix"><slot name="suffix"></slot></div>
</div>
<div class="text"><slot name="text"></slot></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: false
},
required: {
type: Boolean,
required: false
}
},
data() {
return {
v: this.value,
focused: false,
styl: 'fill'
};
},
computed: {
filled(): boolean {
return this.v != '' && this.v != null;
}
},
watch: {
value(v) {
this.v = v;
}
},
inject: {
isCardChild: { default: false }
},
created() {
if (this.isCardChild) {
this.styl = 'line';
}
},
mounted() {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
}
},
methods: {
focus() {
this.$refs.input.focus();
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark, fill)
margin 32px 0
> .icon
position absolute
top 0
left 0
width 24px
text-align center
line-height 32px
color rgba(#000, 0.54)
&:not(:empty) + .input
margin-left 28px
> .input
display flex
if fill
padding 6px 12px
background rgba(#000, 0.035)
border-radius 6px
else
&:before
content ''
display block
position absolute
bottom 0
left 0
right 0
height 1px
background isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
&:after
content ''
display block
position absolute
bottom 0
left 0
right 0
height 2px
background $theme-color
opacity 0
transform scaleX(0.12)
transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
will-change border opacity transform
> .label
position absolute
top fill ? 6px : 0
left 0
pointer-events none
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
transition-duration 0.3s
font-size 16px
line-height 32px
color rgba(#000, 0.54)
pointer-events none
//will-change transform
transform-origin top left
transform scale(1)
> select
display block
flex 1
width 100%
padding 0
font inherit
font-weight fill ? bold : normal
font-size 16px
height 32px
color isDark ? #fff : #000
background transparent
border none
border-radius 0
outline none
box-shadow none
*
color #000
> .prefix
> .suffix
display block
align-self center
justify-self center
font-size 16px
line-height 32px
color rgba(#000, 0.54)
pointer-events none
> *
display block
min-width 16px
> .prefix
padding-right 4px
> .suffix
padding-left 4px
> .text
margin 6px 0
font-size 13px
*
margin 0
&.focused
> .input
if fill
background rgba(#000, 0.05)
else
&:after
opacity 1
transform scaleX(1)
> .label
color $theme-color
&.focused
&.filled
> .input
> .label
top fill ? -24px : -17px
left 0 !important
transform scale(0.75)
.ui-select[data-darkmode]
&.fill
root(true, true)
&:not(.fill)
root(true, false)
.ui-select:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
</style>

View File

@ -0,0 +1,135 @@
<template>
<div
class="ui-switch"
:class="{ disabled, checked }"
role="switch"
:aria-checked="checked"
:aria-disabled="disabled"
@click="toggle"
>
<input
type="checkbox"
ref="input"
:disabled="disabled"
@keydown.enter="toggle"
>
<span class="button">
<span></span>
</span>
<span class="label">
<span :aria-hidden="!checked"><slot></slot></span>
<p :aria-hidden="!checked">
<slot name="text"></slot>
</p>
</span>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
model: {
prop: 'value',
event: 'change'
},
props: {
value: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
checked(): boolean {
return this.value;
}
},
methods: {
toggle() {
this.$emit('change', !this.checked);
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark)
display flex
margin 32px 0
cursor pointer
transition all 0.3s
> *
user-select none
&.disabled
opacity 0.6
cursor not-allowed
&.checked
> .button
background-color rgba($theme-color, 0.4)
border-color rgba($theme-color, 0.4)
> *
background-color $theme-color
transform translateX(14px)
> input
position absolute
width 0
height 0
opacity 0
margin 0
> .button
display inline-block
margin 3px 0 0 0
width 34px
height 14px
background isDark ? rgba(#fff, 0.15) : rgba(#000, 0.25)
outline none
border-radius 14px
transition inherit
> *
position absolute
top -3px
left 0
border-radius 100%
transition background-color 0.3s, transform 0.3s
width 20px
height 20px
background-color #fff
box-shadow 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12)
> .label
margin-left 8px
display block
font-size 16px
cursor pointer
transition inherit
> span
display block
line-height 20px
color isDark ? #c4ccd2 : rgba(#000, 0.75)
transition inherit
> p
margin 0
//font-size 90%
color isDark ? #78858e : #9daab3
.ui-switch[data-darkmode]
root(true)
.ui-switch:not([data-darkmode])
root(false)
</style>

View File

@ -0,0 +1,174 @@
<template>
<div class="ui-textarea" :class="{ focused, filled }">
<div class="input">
<span class="label" ref="label"><slot></slot></span>
<textarea ref="input"
:value="value"
:required="required"
:readonly="readonly"
:pattern="pattern"
:autocomplete="autocomplete"
@input="$emit('input', $event.target.value)"
@focus="focused = true"
@blur="focused = false">
</textarea>
</div>
<div class="text"><slot name="text"></slot></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
const getPasswordStrength = require('syuilo-password-strength');
export default Vue.extend({
props: {
value: {
required: false
},
required: {
type: Boolean,
required: false
},
readonly: {
type: Boolean,
required: false
},
pattern: {
type: String,
required: false
},
autocomplete: {
type: String,
required: false
}
},
data() {
return {
focused: false,
passwordStrength: ''
}
},
computed: {
filled(): boolean {
return this.value != '' && this.value != null;
}
},
methods: {
focus() {
this.$refs.input.focus();
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
root(isDark, fill)
margin 32px 0
> .input
padding 12px
if fill
background rgba(#000, 0.035)
border-radius 6px
else
&:before
content ''
display block
position absolute
top 0
bottom 0
left 0
right 0
background none
border solid 1px isDark ? rgba(#fff, 0.7) : rgba(#000, 0.42)
border-radius 3px
pointer-events none
&:after
content ''
display block
position absolute
top 0
bottom 0
left 0
right 0
background none
border solid 2px $theme-color
border-radius 3px
opacity 0
transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)
pointer-events none
> .label
position absolute
top 6px
left 12px
pointer-events none
transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
transition-duration 0.3s
font-size 16px
line-height 32px
color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.54)
pointer-events none
//will-change transform
transform-origin top left
transform scale(1)
> textarea
display block
width 100%
min-height 100px
padding 0
font inherit
font-weight fill ? bold : normal
font-size 16px
color isDark ? #fff : #000
background transparent
border none
border-radius 0
outline none
box-shadow none
> .text
margin 6px 0
font-size 13px
*
margin 0
&.focused
> .input
if fill
background rgba(#000, 0.05)
else
&:after
opacity 1
> .label
color $theme-color
&.focused
&.filled
> .input
> .label
top -24px
left 0 !important
transform scale(0.75)
.ui-textarea[data-darkmode]
&.fill
root(true, true)
&:not(.fill)
root(true, false)
.ui-textarea:not([data-darkmode])
&.fill
root(false, true)
&:not(.fill)
root(false, false)
</style>

View File

@ -49,7 +49,8 @@ export default Vue.extend({
polylinePoints: '',
polygonPoints: '',
headX: null,
headY: null
headY: null,
clock: null
};
},
watch: {
@ -59,6 +60,12 @@ export default Vue.extend({
},
created() {
this.draw();
// Vueが何故かWatchを発動させない場合があるので
this.clock = setInterval(this.draw, 1000);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
draw() {

View File

@ -5,15 +5,16 @@
<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
<div v-else>
<p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
<transition-group v-else tag="div" name="chart">
<div v-for="stat in stats" :key="stat.tag">
<div class="tag">
<router-link :to="`/tags/${ stat.tag }`">#{{ stat.tag }}</router-link>
<router-link :to="`/tags/${ stat.tag }`" :title="stat.tag">#{{ stat.tag }}</router-link>
<p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
</div>
<x-chart class="chart" :src="stat.chart"/>
</div>
</div>
</transition-group>
</div>
</mk-widget-container>
</div>
@ -41,7 +42,7 @@ export default define({
},
mounted() {
this.fetch();
this.clock = setInterval(this.fetch, 1000 * 60 * 10);
this.clock = setInterval(this.fetch, 1000 * 60);
},
beforeDestroy() {
clearInterval(this.clock);
@ -65,8 +66,9 @@ export default define({
root(isDark)
.mkw-hashtags--body
> .fetching
> .empty
margin 0
padding 12px 16px
padding 16px
text-align center
color #aaa
@ -74,20 +76,29 @@ root(isDark)
margin-right 4px
> div
.chart-move
transition transform 1s ease
> div
display flex
align-items center
padding 16px
padding 14px 16px
&:not(:last-child)
border-bottom solid 1px isDark ? #393f4f : #eee
> .tag
flex 1
overflow hidden
font-size 14px
color isDark ? #9baec8 : #65727b
> a
display block
width 100%
white-space nowrap
overflow hidden
text-overflow ellipsis
color inherit
> p

View File

@ -1,6 +1,8 @@
declare const _HOST_: string;
declare const _HOSTNAME_: string;
declare const _URL_: string;
declare const _NAME_: string;
declare const _DESCRIPTION_: string;
declare const _API_URL_: string;
declare const _WS_URL_: string;
declare const _DOCS_URL_: string;
@ -21,6 +23,8 @@ declare const _GOOGLE_MAPS_API_KEY_: string;
export const host = _HOST_;
export const hostname = _HOSTNAME_;
export const url = _URL_;
export const name = _NAME_;
export const description = _DESCRIPTION_;
export const apiUrl = _API_URL_;
export const wsUrl = _WS_URL_;
export const docsUrl = _DOCS_URL_;

View File

@ -145,7 +145,7 @@ export default Vue.extend({
(this as any).api('drive/files/update', {
fileId: this.file.id,
name: name
})
});
});
},
@ -173,7 +173,9 @@ export default Vue.extend({
},
deleteFile() {
alert('not implemented yet');
(this as any).api('drive/files/delete', {
fileId: this.file.id
});
}
}
});

View File

@ -118,6 +118,7 @@ export default Vue.extend({
this.connection.on('file_created', this.onStreamDriveFileCreated);
this.connection.on('file_updated', this.onStreamDriveFileUpdated);
this.connection.on('file_deleted', this.onStreamDriveFileDeleted);
this.connection.on('folder_created', this.onStreamDriveFolderCreated);
this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
@ -130,6 +131,7 @@ export default Vue.extend({
beforeDestroy() {
this.connection.off('file_created', this.onStreamDriveFileCreated);
this.connection.off('file_updated', this.onStreamDriveFileUpdated);
this.connection.off('file_deleted', this.onStreamDriveFileDeleted);
this.connection.off('folder_created', this.onStreamDriveFolderCreated);
this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
(this as any).os.streams.driveStream.dispose(this.connectionId);
@ -167,6 +169,10 @@ export default Vue.extend({
}
},
onStreamDriveFileDeleted(fileId) {
this.removeFile(fileId);
},
onStreamDriveFolderCreated(folder) {
this.addFolder(folder, true);
},

View File

@ -50,6 +50,7 @@ import * as XDraggable from 'vuedraggable';
import getKao from '../../../common/scripts/get-kao';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import parse from '../../../../../text/parse';
import { host } from '../../../config';
export default Vue.extend({
components: {
@ -129,6 +130,7 @@ export default Vue.extend({
// 自分は除外
if (this.$store.state.i.username == x.username && x.host == null) return;
if (this.$store.state.i.username == x.username && x.host == host) return;
// 重複は除外
if (this.text.indexOf(`${mention} `) != -1) return;

View File

@ -5,7 +5,7 @@
<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
<template v-if="edit">
<header>
<select v-model="widgetAdderSelected">
<select v-model="widgetAdderSelected" @change="addWidget">
<option value="profile">%i18n:common.widgets.profile%</option>
<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
<option value="calendar">%i18n:common.widgets.calendar%</option>
@ -30,20 +30,15 @@
<option value="nav">%i18n:common.widgets.nav%</option>
<option value="tips">%i18n:common.widgets.tips%</option>
</select>
<button @click="addWidget">%i18n:@add%</button>
</header>
<x-draggable
:list="column.widgets"
:options="{ handle: '.handle', animation: 150 }"
:options="{ animation: 150 }"
@sort="onWidgetSort"
>
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id">
<header>
<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
</header>
<div @click="widgetFunc(widget.id)">
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
</div>
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)">
<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
</div>
</x-draggable>
</template>
@ -142,6 +137,13 @@ export default Vue.extend({
root(isDark)
.gqpwvtwtprsbmnssnbicggtwqhmylhnq
> header
padding 16px
> *
width 100%
padding 4px
.widget, .customize-container
margin 8px
@ -149,7 +151,21 @@ root(isDark)
margin-top 0
.customize-container
background #fff
cursor move
> *:not(.remove)
pointer-events none
> .remove
position absolute
z-index 1
top 8px
right 8px
width 32px
height 32px
color #fff
background rgba(#000, 0.7)
border-radius 4px
> header
color isDark ? #fff : #000

View File

@ -2,17 +2,11 @@
* Mobile Client
*/
import Vue from 'vue';
import VueRouter from 'vue-router';
import { MdCard, MdButton, MdField, MdMenu, MdList, MdSwitch, MdSubheader, MdDialog, MdDialogAlert, MdRadio } from 'vue-material/dist/components';
import 'vue-material/dist/vue-material.min.css';
import 'vue-material/dist/theme/default.css';
// Style
import './style.styl';
import '../../element.scss';
import '../../md.scss';
import init from '../init';
@ -42,17 +36,7 @@ import MkUserLists from './views/pages/user-lists.vue';
import MkUserList from './views/pages/user-list.vue';
import MkSettings from './views/pages/settings.vue';
import MkOthello from './views/pages/othello.vue';
Vue.use(MdCard);
Vue.use(MdButton);
Vue.use(MdField);
Vue.use(MdMenu);
Vue.use(MdList);
Vue.use(MdSwitch);
Vue.use(MdSubheader);
Vue.use(MdDialog);
Vue.use(MdDialogAlert);
Vue.use(MdRadio);
import MkTag from './views/pages/tag.vue';
/**
* init
@ -88,6 +72,7 @@ init((launch) => {
{ path: '/i/drive/file/:file', component: MkDrive },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
{ path: '/tags/:tag', component: MkTag },
{ path: '/othello', name: 'othello', component: MkOthello },
{ path: '/othello/:game', component: MkOthello },
{ path: '/@:user', component: MkUser },

View File

@ -10,9 +10,6 @@ html
height 100%
background #ececed !important
// for md
transition none !important
&[data-darkmode]
background #191B22 !important

View File

@ -34,15 +34,10 @@
</div>
<div class="menu">
<div>
<a :href="`${file.url}?download`" :download="file.name">
%fa:download%%i18n:@download%
</a>
<button @click="rename">
%fa:pencil-alt%%i18n:@rename%
</button>
<button @click="move">
%fa:R folder-open%%i18n:@move%
</button>
<a :href="`${file.url}?download`" :download="file.name">%fa:download%%i18n:@download%</a>
<button @click="rename">%fa:pencil-alt%%i18n:@rename%</button>
<button @click="move">%fa:R folder-open%%i18n:@move%</button>
<button @click="del">%fa:trash-alt R%%i18n:@delete%</button>
</div>
</div>
<div class="exif" v-show="exif">
@ -112,6 +107,13 @@ export default Vue.extend({
});
});
},
del() {
(this as any).api('drive/files/delete', {
fileId: this.file.id
}).then(() => {
this.browser.cd(this.file.folderId, true);
});
},
showCreatedAt() {
alert(new Date(this.file.createdAt).toLocaleString());
},

View File

@ -100,6 +100,7 @@ export default Vue.extend({
this.connection.on('file_created', this.onStreamDriveFileCreated);
this.connection.on('file_updated', this.onStreamDriveFileUpdated);
this.connection.on('file_deleted', this.onStreamDriveFileDeleted);
this.connection.on('folder_created', this.onStreamDriveFolderCreated);
this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
@ -118,6 +119,7 @@ export default Vue.extend({
beforeDestroy() {
this.connection.off('file_created', this.onStreamDriveFileCreated);
this.connection.off('file_updated', this.onStreamDriveFileUpdated);
this.connection.off('file_deleted', this.onStreamDriveFileDeleted);
this.connection.off('folder_created', this.onStreamDriveFolderCreated);
this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
(this as any).os.streams.driveStream.dispose(this.connectionId);
@ -136,6 +138,10 @@ export default Vue.extend({
}
},
onStreamDriveFileDeleted(fileId) {
this.removeFile(fileId);
},
onStreamDriveFolderCreated(folder) {
this.addFolder(folder, true);
},

View File

@ -46,6 +46,7 @@ import * as XDraggable from 'vuedraggable';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import getKao from '../../../common/scripts/get-kao';
import parse from '../../../../../text/parse';
import { host } from '../../../config';
export default Vue.extend({
components: {
@ -123,6 +124,7 @@ export default Vue.extend({
// 自分は除外
if (this.$store.state.i.username == x.username && x.host == null) return;
if (this.$store.state.i.username == x.username && x.host == host) return;
// 重複は除外
if (this.text.indexOf(`${mention} `) != -1) return;

View File

@ -1,132 +1,84 @@
<template>
<mk-ui>
<span slot="header">%fa:cog%%i18n:@settings%</span>
<main>
<p v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p>
<main :data-darkmode="$store.state.device.darkmode">
<div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></div>
<div>
<x-profile/>
<md-card>
<md-card-header>
<div class="md-title">%fa:palette% %i18n:@design%</div>
</md-card-header>
<ui-card>
<div slot="title">%fa:palette% %i18n:@design%</div>
<md-card-content>
<div>
<md-switch v-model="darkmode">%i18n:@dark-mode%</md-switch>
</div>
<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
<ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch>
<div>
<md-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</md-switch>
</div>
<div>
<div>%i18n:@timeline%</div>
<ui-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</ui-switch>
<ui-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</ui-switch>
<ui-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
</div>
<div>
<div class="md-body-2">%i18n:@timeline%</div>
<div>
<div>%i18n:@post-style%</div>
<ui-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</ui-radio>
<ui-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</ui-radio>
</div>
</ui-card>
<div>
<md-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</md-switch>
</div>
<ui-card>
<div slot="title">%fa:cog% %i18n:@behavior%</div>
<ui-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch>
<ui-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</ui-switch>
<ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch>
<ui-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</ui-switch>
<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
</ui-card>
<div>
<md-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</md-switch>
</div>
<ui-card>
<div slot="title">%fa:language% %i18n:@lang%</div>
<div>
<md-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</md-switch>
</div>
</div>
<ui-select v-model="lang" placeholder="%i18n:@auto%">
<optgroup label="%i18n:@recommended%">
<option value="">%i18n:@auto%</option>
</optgroup>
<div>
<div class="md-body-2">%i18n:@post-style%</div>
<optgroup label="%i18n:@specify-language%">
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
</optgroup>
</ui-select>
<span>%fa:info-circle% %i18n:@lang-tip%</span>
</ui-card>
<md-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</md-radio>
<md-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</md-radio>
</div>
</md-card-content>
</md-card>
<ui-card>
<div slot="title">%fa:B twitter% %i18n:@twitter%</div>
<md-card>
<md-card-header>
<div class="md-title">%fa:cog% %i18n:@behavior%</div>
</md-card-header>
<p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
<p>
<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
<span v-if="$store.state.i.twitter"> or </span>
<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
</p>
</ui-card>
<md-card-content>
<div>
<md-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</md-switch>
</div>
<ui-card>
<div slot="title">%fa:sync-alt% %i18n:@update%</div>
<div>
<md-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</md-switch>
</div>
<div>
<md-switch v-model="loadRawImages">%i18n:@load-raw-images%</md-switch>
</div>
<div>
<md-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</md-switch>
</div>
<div>
<md-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</md-switch>
</div>
</md-card-content>
</md-card>
<md-card>
<md-card-header>
<div class="md-title">%fa:language% %i18n:@lang%</div>
</md-card-header>
<md-card-content>
<md-field>
<md-select v-model="lang" placeholder="%i18n:@auto%">
<md-optgroup label="%i18n:@recommended%">
<md-option value="">%i18n:@auto%</md-option>
</md-optgroup>
<md-optgroup label="%i18n:@specify-language%">
<md-option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</md-option>
</md-optgroup>
</md-select>
</md-field>
<span class="md-helper-text">%fa:info-circle% %i18n:@lang-tip%</span>
</md-card-content>
</md-card>
<md-card>
<md-card-header>
<div class="md-title">%fa:B twitter% %i18n:@twitter%</div>
</md-card-header>
<md-card-content>
<p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
<p>
<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
<span v-if="$store.state.i.twitter"> or </span>
<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
</p>
</md-card-content>
</md-card>
<md-card>
<md-card-header>
<div class="md-title">%fa:sync-alt% %i18n:@update%</div>
</md-card-header>
<md-card-content>
<div>%i18n:@version% <i>{{ version }}</i></div>
<template v-if="latestVersion !== undefined">
<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
</template>
<md-button class="md-raised md-primary" @click="checkForUpdate" :disabled="checkingForUpdate">
<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
<template v-else>%i18n:@check-for-updates%</template>
</md-button>
</md-card-content>
</md-card>
<div>%i18n:@version% <i>{{ version }}</i></div>
<template v-if="latestVersion !== undefined">
<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
</template>
<ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
<template v-else>%i18n:@check-for-updates%</template>
</ui-button>
</ui-card>
</div>
<p><small>ver {{ version }} ({{ codename }})</small></p>
<footer>
<small>ver {{ version }} ({{ codename }})</small>
</footer>
</main>
</mk-ui>
</template>
@ -267,20 +219,22 @@ export default Vue.extend({
<style lang="stylus" scoped>
root(isDark)
padding 0 16px
margin 0 auto
max-width 500px
width 100%
> div
> *
margin-bottom 16px
> p
display block
margin 24px
> .signin-as
margin 16px
padding 16px
text-align center
color isDark ? #cad2da : #a2a9b1
color isDark ? #49ab63 : #2c662d
background isDark ? #273c34 : #fcfff5
box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
> footer
margin 16px
text-align center
color isDark ? #c9d2e0 : #888
main[data-darkmode]
root(true)

View File

@ -1,62 +1,49 @@
<template>
<md-card>
<md-card-header>
<div class="md-title">%fa:pencil-alt% %i18n:@title%</div>
</md-card-header>
<ui-card>
<div slot="title">%fa:user% %i18n:@title%</div>
<md-card-content>
<md-field>
<label>%i18n:@name%</label>
<md-input v-model="name" :disabled="saving" md-counter="30"/>
</md-field>
<ui-form :disabled="saving">
<ui-input v-model="name" :max="30">
<span>%i18n:@name%</span>
</ui-input>
<md-field>
<label>%i18n:@account%</label>
<span class="md-prefix">@</span>
<md-input v-model="username" readonly></md-input>
<span class="md-suffix">@{{ host }}</span>
</md-field>
<ui-input v-model="username" readonly>
<span>%i18n:@account%</span>
<span slot="prefix">@</span>
<span slot="suffix">@{{ host }}</span>
</ui-input>
<md-field>
<md-icon>%fa:map-marker-alt%</md-icon>
<label>%i18n:@location%</label>
<md-input v-model="location" :disabled="saving"/>
</md-field>
<ui-input v-model="location">
<span>%i18n:@location%</span>
<span slot="prefix">%fa:map-marker-alt%</span>
</ui-input>
<md-field>
<md-icon>%fa:birthday-cake%</md-icon>
<label>%i18n:@birthday%</label>
<md-input type="date" v-model="birthday" :disabled="saving"/>
</md-field>
<ui-input v-model="birthday" type="date">
<span>%i18n:@birthday%</span>
<span slot="prefix">%fa:birthday-cake%</span>
</ui-input>
<md-field>
<label>%i18n:@description%</label>
<md-textarea v-model="description" :disabled="saving" md-counter="500"/>
</md-field>
<ui-textarea v-model="description" :max="500">
<span>%i18n:@description%</span>
</ui-textarea>
<md-field>
<label>%i18n:@avatar%</label>
<md-file @md-change="onAvatarChange"/>
</md-field>
<ui-input type="file" @change="onAvatarChange">
<span>%i18n:@avatar%</span>
<span slot="icon">%fa:image%</span>
<span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span>
</ui-input>
<md-field>
<label>%i18n:@banner%</label>
<md-file @md-change="onBannerChange"/>
</md-field>
<ui-input type="file" @change="onBannerChange">
<span>%i18n:@banner%</span>
<span slot="icon">%fa:image%</span>
<span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span>
</ui-input>
<md-dialog-alert
:md-active.sync="uploading"
md-content="%18n:!@uploading%"/>
<ui-switch v-model="isCat">%i18n:@is-cat%</ui-switch>
<div>
<md-switch v-model="isCat">%i18n:@is-cat%</md-switch>
</div>
</md-card-content>
<md-card-actions>
<md-button class="md-primary" :disabled="saving" @click="save">%i18n:@save%</md-button>
</md-card-actions>
</md-card>
<ui-button @click="save">%i18n:@save%</ui-button>
</ui-form>
</ui-card>
</template>
<script lang="ts">
@ -77,7 +64,8 @@ export default Vue.extend({
isBot: false,
isCat: false,
saving: false,
uploading: false
avatarUploading: false,
bannerUploading: false
};
},
@ -95,7 +83,7 @@ export default Vue.extend({
methods: {
onAvatarChange([file]) {
this.uploading = true;
this.avatarUploading = true;
const data = new FormData();
data.append('file', file);
@ -108,16 +96,16 @@ export default Vue.extend({
.then(response => response.json())
.then(f => {
this.avatarId = f.id;
this.uploading = false;
this.avatarUploading = false;
})
.catch(e => {
this.uploading = false;
this.avatarUploading = false;
alert('%18n:!@upload-failed%');
});
},
onBannerChange([file]) {
this.uploading = true;
this.bannerUploading = true;
const data = new FormData();
data.append('file', file);
@ -130,10 +118,10 @@ export default Vue.extend({
.then(response => response.json())
.then(f => {
this.bannerId = f.id;
this.uploading = false;
this.bannerUploading = false;
})
.catch(e => {
this.uploading = false;
this.bannerUploading = false;
alert('%18n:!@upload-failed%');
});
},

View File

@ -1,57 +1,26 @@
<template>
<div class="signup">
<h1>Misskeyをはじめる</h1>
<p>いつでもどこからでもMisskeyを利用できますもちろん無料です</p>
<div class="form">
<p>新規登録</p>
<div>
<mk-signup/>
</div>
</div>
<mk-signup/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
mounted() {
document.documentElement.style.background = '#293946';
}
});
export default Vue.extend({});
</script>
<style lang="stylus" scoped>
.signup
padding 16px
padding 32px
margin 0 auto
max-width 500px
h1
margin 0
padding 8px
padding 8px 0 0 0
font-size 1.5em
font-weight normal
color #c3c6ca
& + p
margin 0 0 16px 0
padding 0 8px 0 8px
color #949fa9
.form
background #fff
border solid 1px rgba(#000, 0.2)
border-radius 8px
overflow hidden
> p
margin 0
padding 12px 20px
color #555
background #f5f5f5
border-bottom solid 1px #ddd
> div
padding 16px
font-weight bold
color #444
</style>

View File

@ -0,0 +1,81 @@
<template>
<mk-ui>
<span slot="header">%fa:hashtag%{{ $route.params.tag }}</span>
<main>
<p v-if="!fetching && empty">%fa:search%{{ q }}に関する投稿は見つかりませんでした</p>
<mk-notes ref="timeline" :more="existMore ? more : null"/>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
const limit = 20;
export default Vue.extend({
data() {
return {
fetching: true,
moreFetching: false,
existMore: false,
offset: 0,
empty: false
};
},
watch: {
$route: 'fetch'
},
mounted() {
this.$nextTick(() => {
this.fetch();
});
},
methods: {
fetch() {
this.fetching = true;
Progress.start();
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('notes/search_by_tag', {
limit: limit + 1,
offset: this.offset,
tag: this.$route.params.tag
}).then(notes => {
if (notes.length == 0) this.empty = true;
if (notes.length == limit + 1) {
notes.pop();
this.existMore = true;
}
res(notes);
this.fetching = false;
Progress.done();
}, rej);
}));
},
more() {
this.offset += limit;
const promise = (this as any).api('notes/search_by_tag', {
limit: limit + 1,
offset: this.offset,
tag: this.$route.params.tag
});
promise.then(notes => {
if (notes.length == limit + 1) {
notes.pop();
} else {
this.existMore = false;
}
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
return promise;
}
}
});
</script>

View File

@ -1,29 +1,31 @@
<template>
<div class="welcome">
<div>
<h1><b>Misskey</b>へようこそ</h1>
<p>Twitter風ミニブログSNSMisskeyへようこそ共有したいことを投稿したりタイムラインでみんなの投稿を読むこともできます<br><a href="/signup">アカウントを作成する</a></p>
<div class="form">
<p>%fa:lock% ログイン</p>
<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" alt="Misskey">
<p class="host">{{ host }}</p>
<div class="about">
<h2>{{ name || 'unidentified' }}</h2>
<p v-html="description || '%i18n:common.about%'"></p>
<router-link class="signup" to="/signup">新規登録</router-link>
</div>
<div class="login">
<form @submit.prevent="onSubmit">
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" autofocus required @change="onUsernameChange">
<span>ユーザー名</span>
<span slot="prefix">@</span>
<span slot="suffix">@{{ host }}</span>
</ui-input>
<ui-input v-model="password" type="password" required>
<span>パスワード</span>
<span slot="prefix">%fa:lock%</span>
</ui-input>
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/>
<ui-button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</ui-button>
</form>
<div>
<form @submit.prevent="onSubmit">
<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/>
<input v-model="password" type="password" placeholder="パスワード" required/>
<input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/>
<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
</form>
<div>
<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
</div>
<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
</div>
</div>
<div class="tl">
<p>%fa:comments R% タイムラインを見てみる</p>
<mk-welcome-timeline/>
</div>
<div class="users">
<mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/>
</div>
<footer>
<small>{{ copyright }}</small>
</footer>
@ -33,7 +35,7 @@
<script lang="ts">
import Vue from 'vue';
import { apiUrl, copyright } from '../../../config';
import { apiUrl, copyright, host, name, description } from '../../../config';
export default Vue.extend({
data() {
@ -45,7 +47,10 @@ export default Vue.extend({
token: '',
apiUrl,
copyright,
users: []
users: [],
host,
name,
description
};
},
mounted() {
@ -84,112 +89,74 @@ export default Vue.extend({
<style lang="stylus" scoped>
.welcome
background linear-gradient(to bottom, #1e1d65, #bd6659)
text-align center
//background #fff
> div
padding 16px
padding 32px
margin 0 auto
max-width 500px
h1
margin 0
padding 8px
font-size 1.5em
font-weight normal
color #cacac3
> img
display block
max-width 200px
margin 0 auto
& + p
margin 0 0 16px 0
padding 0 8px 0 8px
color #949fa9
> .host
display block
text-align center
padding 6px 12px
line-height 32px
font-weight bold
color #333
background rgba(#000, 0.035)
border-radius 6px
.form
margin-bottom 16px
> .about
margin-top 16px
padding 16px
color #555
background #fff
border solid 1px rgba(#000, 0.2)
border-radius 8px
overflow hidden
border-radius 6px
> h2
margin 0
> p
margin 0
padding 12px 20px
color #555
background #f5f5f5
border-bottom solid 1px #ddd
margin 8px
> div
> .signup
font-weight bold
> form
padding 16px
border-bottom solid 1px #ddd
> .login
margin 16px 0
input
display block
padding 12px
margin 0 0 16px 0
width 100%
font-size 1em
color rgba(#000, 0.7)
background #fff
outline none
border solid 1px #ddd
border-radius 4px
> form
button
display block
width 100%
padding 10px
margin 0
color #333
font-size 1em
text-align center
text-decoration none
text-shadow 0 1px 0 rgba(255, 255, 255, 0.9)
background-image linear-gradient(#fafafa, #eaeaea)
border 1px solid #ddd
border-bottom-color #cecece
border-radius 4px
&:active
background-color #767676
background-image none
border-color #444
box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
> div
padding 16px
button
display block
width 100%
padding 10px
margin 0
color #333
font-size 1em
text-align center
text-decoration none
text-shadow 0 1px 0 rgba(255, 255, 255, 0.9)
background-image linear-gradient(#fafafa, #eaeaea)
border 1px solid #ddd
border-bottom-color #cecece
border-radius 4px
> .tl
background #fff
border solid 1px rgba(#000, 0.2)
border-radius 8px
overflow hidden
> p
margin 0
padding 12px 20px
color #555
background #f5f5f5
border-bottom solid 1px #ddd
> .mk-welcome-timeline
max-height 300px
overflow auto
> .users
margin 12px 0 0 0
> *
display inline-block
margin 4px
width 38px
height 38px
border-radius 6px
&:active
background-color #767676
background-image none
border-color #444
box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
> footer
text-align center
color #fff
color #444
> small
display block

View File

@ -56,7 +56,7 @@ export default define({
left 92px
margin 0
line-height 100px
color #fff !important // !important is for md
color #fff
font-weight bold
text-shadow 0 0 8px rgba(#000, 0.5)

View File

@ -1,13 +0,0 @@
/* SEE: https://vuematerial.io/themes/configuration */
@import '../const.json';
@import "~vue-material/dist/theme/engine";
@include md-register-theme("default", (
primary: $themeColor,
accent: $themeColor
));
@import "~vue-material/dist/components/MdButton/theme";
@import "~vue-material/dist/components/MdField/theme";

View File

@ -15,6 +15,8 @@ export type Source = {
*/
url: string;
};
name?: string;
description?: string;
url: string;
port: number;
https?: { [x: string]: string };

View File

@ -48,6 +48,11 @@ export type INote = {
repliesCount: number;
reactionCounts: any;
mentions: mongo.ObjectID[];
mentionedRemoteUsers: Array<{
uri: string;
username: string;
host: string;
}>;
/**
* public ... 公開
@ -289,7 +294,7 @@ export const pack = async (
// Poll
if (meId && _note.poll && !hide) {
_note.poll = (async (poll) => {
_note.poll = (async poll => {
const vote = await PollVote
.findOne({
userId: meId,

View File

@ -15,6 +15,11 @@ const log = debug('misskey:activitypub');
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> {
const uri = activity.id || activity;
// アナウンサーが凍結されていたらスキップ
if (actor.isSuspended) {
return;
}
if (typeof uri !== 'string') {
throw new Error('invalid announce');
}

View File

@ -1,6 +1,6 @@
import config from '../../../config';
export default tag => ({
export default (tag: string) => ({
type: 'Hashtag',
href: `${config.url}/tags/${encodeURIComponent(tag)}`,
name: '#' + tag

View File

@ -0,0 +1,9 @@
export default (mention: {
uri: string;
username: string;
host: string;
}) => ({
type: 'Mention',
href: mention.uri,
name: `@${mention.username}@${mention.host}`
});

View File

@ -1,5 +1,6 @@
import renderDocument from './document';
import renderHashtag from './hashtag';
import renderMention from './mention';
import config from '../../../config';
import DriveFile from '../../../models/drive-file';
import Note, { INote } from '../../../models/note';
@ -45,6 +46,18 @@ export default async function renderNote(note: INote, dive = true) {
const attributedTo = `${config.url}/users/${user._id}`;
const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0
? note.mentionedRemoteUsers.map(x => x.uri)
: [];
const cc = ['public', 'home', 'followers'].includes(note.visibility)
? [`${attributedTo}/followers`].concat(mentions)
: [];
const hashtagTags = (note.tags || []).map(renderHashtag);
const mentionTags = (note.mentionedRemoteUsers || []).map(renderMention);
const tag = hashtagTags.concat(mentionTags)
return {
id: `${config.url}/notes/${note._id}`,
type: 'Note',
@ -52,9 +65,9 @@ export default async function renderNote(note: INote, dive = true) {
content: toHtml(note),
published: note.createdAt.toISOString(),
to: 'https://www.w3.org/ns/activitystreams#Public',
cc: `${attributedTo}/followers`,
cc,
inReplyTo,
attachment: (await promisedFiles).map(renderDocument),
tag: (note.tags || []).map(renderHashtag)
tag
};
}

View File

@ -37,10 +37,13 @@ module.exports = async (params, user, app) => {
const sort = {
_id: -1
};
const query = {
'metadata.userId': user._id,
'metadata.folderId': folderId
'metadata.folderId': folderId,
'metadata.deletedAt': { $exists: false }
} as any;
if (sinceId) {
sort._id = 1;
query._id = {
@ -51,6 +54,7 @@ module.exports = async (params, user, app) => {
$lt: untilId
};
}
if (type) {
query.contentType = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
}

View File

@ -0,0 +1,32 @@
import $ from 'cafy'; import ID from '../../../../../cafy-id';
import DriveFile from '../../../../../models/drive-file';
import del from '../../../../../services/drive/delete-file';
import { publishDriveStream } from '../../../../../publishers/stream';
/**
* Delete a file
*/
module.exports = async (params, user) => {
// Get 'fileId' parameter
const [fileId, fileIdErr] = $.type(ID).get(params.fileId);
if (fileIdErr) throw 'invalid fileId param';
// Fetch file
const file = await DriveFile
.findOne({
_id: fileId,
'metadata.userId': user._id
});
if (file === null) {
throw 'file-not-found';
}
// Delete
await del(file);
// Publish file_deleted event
publishDriveStream(user._id, 'file_deleted', file._id);
return;
};

View File

@ -1,13 +1,26 @@
import Note from '../../../../models/note';
/*
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる
*/
const rangeA = 1000 * 60 * 30; // 30分
const rangeB = 1000 * 60 * 120; // 2時間
const coefficient = 1.5; // 「n倍」の部分
const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
const max = 5;
/**
* Get trends of hashtags
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
module.exports = () => new Promise(async (res, rej) => {
//#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計
const data = await Note.aggregate([{
$match: {
createdAt: {
$gt: new Date(Date.now() - 1000 * 60 * 60)
$gt: new Date(Date.now() - rangeA)
},
tags: {
$exists: true,
@ -26,6 +39,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
userId: any;
}
}>;
//#endregion
if (data.length == 0) {
return res([]);
@ -33,6 +47,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const tags = [];
// カウント
data.map(x => x._id).forEach(x => {
const i = tags.findIndex(tag => tag.name == x.tags);
if (i != -1) {
@ -45,11 +60,45 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
}
});
const hots = tags
// 最低要求投稿者数を下回るならカットする
const limitedTags = tags.filter(tag => tag.count >= requiredUsers);
//#region 2. 1で取得したそれぞれのタグについて、「直近a分間のユニーク投稿数が今からa分前今からb分前の間のユニーク投稿数のn倍以上」かどうかを判定する
const hotsPromises = limitedTags.map(async tag => {
const passedCount = (await Note.distinct('userId', {
tags: tag.name,
createdAt: {
$lt: new Date(Date.now() - rangeA),
$gt: new Date(Date.now() - rangeB)
}
}) as any).length;
if (tag.count >= (passedCount * coefficient)) {
return tag;
} else {
return null;
}
});
//#endregion
// タグを人気順に並べ替え
let hots = (await Promise.all(hotsPromises))
.filter(x => x != null)
.sort((a, b) => b.count - a.count)
.map(tag => tag.name)
.slice(0, 5);
.slice(0, max);
//#region 3. もし上記の方法でのトレンド抽出の結果、求められているタグ数に達しなければ「ただ単に現在投稿数が多いハッシュタグ」に切り替える
if (hots.length < max) {
hots = hots.concat(tags
.filter(tag => hots.indexOf(tag.name) == -1)
.sort((a, b) => b.count - a.count)
.map(tag => tag.name)
.slice(0, max - hots.length));
}
//#endregion
//#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する
const countPromises: Array<Promise<any[]>> = [];
const range = 20;
@ -58,7 +107,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const interval = 1000 * 60 * 10;
for (let i = 0; i < range; i++) {
countPromises.push(Promise.all(hots.map(tag => Note.distinct('userId', {
tags: tag,
createdAt: {
@ -76,6 +124,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
$gt: new Date(Date.now() - (interval * range))
}
})));
//#endregion
const stats = hots.map((tag, i) => ({
tag,

View File

@ -4,7 +4,7 @@ import * as debug from 'debug';
import User, { IUser } from '../../../models/user';
import Mute from '../../../models/mute';
import { pack as packNote } from '../../../models/note';
import { pack as packNote, pack } from '../../../models/note';
import readNotification from '../common/read-notification';
import call from '../call';
import { IApp } from '../../../models/app';
@ -48,6 +48,14 @@ export default async function(
}
//#endregion
// Renoteなら再pack
if (x.type == 'note' && x.body.renoteId != null) {
x.body.renote = await pack(x.body.renoteId, user, {
detail: true
});
data = JSON.stringify(x);
}
connection.send(data);
} catch (e) {
connection.send(data);

View File

@ -3,6 +3,7 @@ import * as redis from 'redis';
import { IUser } from '../../../models/user';
import Mute from '../../../models/mute';
import { pack } from '../../../models/note';
export default async function(
request: websocket.request,
@ -31,6 +32,13 @@ export default async function(
}
//#endregion
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, user, {
detail: true
});
}
connection.send(JSON.stringify({
type: 'note',
body: note

View File

@ -9,13 +9,14 @@ import * as debug from 'debug';
import fileType = require('file-type');
import prominence = require('prominence');
import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile, DriveFileChunk } from '../../models/drive-file';
import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file';
import DriveFolder from '../../models/drive-folder';
import { pack } from '../../models/drive-file';
import event, { publishDriveStream } from '../../publishers/stream';
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
import DriveFileThumbnail, { getDriveFileThumbnailBucket, DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
import genThumbnail from '../../drive/gen-thumbnail';
import delFile from './delete-file';
const gm = _gm.subClass({
imageMagick: true
@ -58,31 +59,7 @@ async function deleteOldFile(user: IRemoteUser) {
});
if (oldFile) {
// チャンクをすべて削除
DriveFileChunk.remove({
files_id: oldFile._id
});
DriveFile.update({ _id: oldFile._id }, {
$set: {
'metadata.deletedAt': new Date(),
'metadata.isExpired': true
}
});
//#region サムネイルもあれば削除
const thumbnail = await DriveFileThumbnail.findOne({
'metadata.originalId': oldFile._id
});
if (thumbnail) {
DriveFileThumbnailChunk.remove({
files_id: thumbnail._id
});
DriveFileThumbnail.remove({ _id: thumbnail._id });
}
//#endregion
delFile(oldFile, true);
}
}

View File

@ -0,0 +1,30 @@
import DriveFile, { DriveFileChunk, IDriveFile } from "../../models/drive-file";
import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
export default async function(file: IDriveFile, isExpired = false) {
// チャンクをすべて削除
await DriveFileChunk.remove({
files_id: file._id
});
await DriveFile.update({ _id: file._id }, {
$set: {
'metadata.deletedAt': new Date(),
'metadata.isExpired': isExpired
}
});
//#region サムネイルもあれば削除
const thumbnail = await DriveFileThumbnail.findOne({
'metadata.originalId': file._id
});
if (thumbnail) {
await DriveFileThumbnailChunk.remove({
files_id: thumbnail._id
});
await DriveFileThumbnail.remove({ _id: thumbnail._id });
}
//#endregion
}

View File

@ -204,6 +204,62 @@ export default async (user: IUser, data: {
return packAp(content);
};
//#region メンション
if (data.text) {
// TODO: Drop dupulicates
const mentionTokens = tokens
.filter(t => t.type == 'mention');
// TODO: Drop dupulicates
const mentionedUsers = (await Promise.all(mentionTokens.map(async m => {
try {
return await resolveUser(m.username, m.host);
} catch (e) {
return null;
}
}))).filter(x => x != null);
// Append mentions data
if (mentionedUsers.length > 0) {
const set = {
mentions: mentionedUsers.map(u => u._id),
mentionedRemoteUsers: mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({
uri: (u as IRemoteUser).uri,
username: u.username,
host: u.host
}))
};
Note.update({ _id: note._id }, {
$set: set
});
Object.assign(note, set);
}
mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
event(u, 'mention', noteObj);
// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
if (data.reply && data.reply.userId.equals(u._id)) return;
if (data.renote && data.renote.userId.equals(u._id)) return;
// Create notification
notify(u._id, user._id, 'mention', {
noteId: note._id
});
nm.push(u._id, 'mention');
});
if (isLocalUser(user)) {
mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => {
deliver(user, await render(), (u as IRemoteUser).inbox);
});
}
}
//#endregion
if (!silent) {
if (isLocalUser(user)) {
if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') {
@ -287,55 +343,6 @@ export default async (user: IUser, data: {
}
//#endergion
//#region メンション
if (data.text) {
// TODO: Drop dupulicates
const mentions = tokens
.filter(t => t.type == 'mention');
let mentionedUsers = await Promise.all(mentions.map(async m => {
try {
return await resolveUser(m.username, m.host);
} catch (e) {
return null;
}
}));
// TODO: Drop dupulicates
mentionedUsers = mentionedUsers.filter(x => x != null);
mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
event(u, 'mention', noteObj);
// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
if (data.reply && data.reply.userId.equals(u._id)) return;
if (data.renote && data.renote.userId.equals(u._id)) return;
// Create notification
notify(u._id, user._id, 'mention', {
noteId: note._id
});
nm.push(u._id, 'mention');
});
if (isLocalUser(user)) {
mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => {
deliver(user, await render(), (u as IRemoteUser).inbox);
});
}
// Append mentions data
if (mentionedUsers.length > 0) {
Note.update({ _id: note._id }, {
$set: {
mentions: mentionedUsers.map(u => u._id)
}
});
}
}
//#endregion
// If has in reply to note
if (data.reply) {
// Increment replies count

View File

@ -20,6 +20,7 @@ export default async function(user: IUser, note: INote) {
$set: {
deletedAt: new Date(),
text: null,
tags: [],
mediaIds: [],
poll: null
}

View File

@ -79,6 +79,8 @@ const consts = {
_DEV_URL_: config.dev_url,
_LANG_: '%lang%',
_LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]),
_NAME_: config.name,
_DESCRIPTION_: config.description,
_HOST_: config.host,
_HOSTNAME_: config.hostname,
_URL_: config.url,